Przejdź do głównej zawartości

Ekran startowy aplikacji mobilnej

Od dotknięcia ikony na pulpicie do pierwszego ekranu aplikacji mija chwila: Android musi uruchomić proces, otworzyć przeglądarkę w trybie aplikacji i pobrać stronę z serwera. Ekran startowy (splash) wypełnia ten czas logo AcePark na ciemnym tle, żeby oczekiwanie nie wyglądało jak awaria.

👤 Co widzi użytkownik

  1. Dotknięcie ikony — od razu ciemne tło z logo AcePark.
  2. Logo zostaje na ekranie przez czas ładowania (zwykle ułamek sekundy, przy słabym zasięgu dłużej).
  3. Pojawia się właściwy ekran: pulpit dla zalogowanych, ekran logowania dla pozostałych.

Na całej tej ścieżce nie powinno być białego mignięcia.

🔧 Jak to działa

Ekran startowy nie jest jednym elementem — to cztery warstwy, które przekazują sobie kontrolę:

WarstwaKiedy widocznaGdzie zdefiniowana
Tło okna LauncherActivityOd startu procesu do narysowania splashuapp/src/main/res/drawable/launch_splash.xml + app/src/main/res/values/styles.xml
Splash TWAPodczas otwierania karty przeglądarkibackgroundColor w twa-manifest.json i app/build.gradle
Strona startowaPrzez cały czas ładowania pulpitupublic/app-start.html
Splash webowyOd pierwszego renderu HTML do wczytania uprawnień do tras.splash-screen w app/globals.css

Wszystkie używają tego samego odcienia #1a1a1a i tego samego logo, żeby przejścia między nimi były niewidoczne.

⚠️ Dlaczego istnieje strona startowa

To jest sedno całego mechanizmu i najłatwiejsza rzecz do przypadkowego zepsucia, więc warto rozumieć, skąd się wzięła.

Biblioteka TWA potrafi przekazać splash przeglądarce-gospodarzowi, żeby ta trzymała go na ekranie przez cały czas ładowania strony. Wymaga to jednak, by przeglądarka deklarowała kategorię androidx.browser.trusted.category.TrustedWebActivitySplashScreensV1. Chrome ją deklaruje. Samsung Internet nie. A na telefonach Samsunga to właśnie Samsung Internet jest domyślną przeglądarką, więc to on hostuje aplikację.

Bez przekazania splashu logo znika razem z LauncherActivity, a użytkownik patrzy na pustą kartę przeglądarki, dopóki pulpit się nie wyrenderuje. Pomiar na Galaxy S21 (Android 15) przed poprawką:

CzasCo na ekranie
0–600 mslogo (splash natywny)
600 ms – 2,5 sbiały ekran
3,2 spulpit

Kluczowe: w tej dziurze nie ma jeszcze żadnego dokumentu, więc nie da się jej zamalować ze strony webowej. Ani background_color z manifestu, ani reguły CSS nie mają czego dotyczyć. Skrócenie łańcucha też nie pomaga — puste przekierowanie z / kosztuje około 190 ms z tych dwóch sekund, resztę zjada renderowanie pulpitu po stronie serwera.

Rozwiązaniem jest danie przeglądarce czegokolwiek do namalowania, byle szybko. Adres startowy TWA prowadzi więc na public/app-start.html: statyczny plik bez sprawdzania sesji i bez zapytań do bazy, z logo wklejonym jako data URI, przez co całość to jedno żądanie bez zależnych pobrań. Przychodzi w 150–250 ms i od razu przekierowuje na /.

Działa to dlatego, że przeglądarka trzyma poprzedni dokument na ekranie, dopóki następny się nie wyrenderuje. Ciemna strona zostaje więc widoczna przez cały czas, w którym / sprawdza sesję, a pulpit się renderuje. Dziura nie została skrócona — została zapełniona.

Splash natywny pokazuje ten sam napis co warstwy webowe, a nie ikonę aplikacji. Bubblewrap generuje splash.png z iconUrl, czyli z ikony — przez co start zaczynał się od żółtej piłki, a zaraz potem przeskakiwał na napis AcePark. Pliki w app/src/main/res/drawable-*/splash.png są więc podmienione ręcznie: napis o szerokości 180dp z przezroczystym marginesem 72dp pod spodem, który odwzorowuje odstęp i loader ze strony startowej, dzięki czemu wyśrodkowany obrazek stawia napis dokładnie tam, gdzie pojawi się chwilę później. bubblewrap update nadpisze te pliki ikoną — trzeba je wtedy wygenerować ponownie.

Napis musi mieć tę samą geometrię we wszystkich trzech warstwach: 180dp szerokości i wysokość wynikającą z proporcji pliku (522×117, czyli 40dp). Splash w aplikacji miał zaszyte height={60} bez reguły CSS, która by to nadpisała — logo było rozciągnięte w pionie o połowę i przy przejściu ze strony startowej skakało. Logo nie ma też animacji pulsowania: przy zmianie dokumentu startowała ona od nowa, więc napis skakał jasnością i skalą dokładnie w tym miejscu, które miało być niewidoczne.

Logo musi też pochodzić z tego samego adresu w obu warstwach. Splash w aplikacji pobierał je przez next/image, czyli spod /_next/image?url=…, a strona startowa ma je wklejone jako data URI — dla przeglądarki to dwa różne zasoby, więc w momencie przejścia napisu przez chwilę nie było i widać było mignięcie. Dlatego splash ma unoptimized (zwykły /logo/logo-white.png), a strona startowa wczytuje ten plik z wyprzedzeniem przez rel="preload".

Dwie rzeczy, które przy edycji tego pliku łatwo zepsuć:

  • Przekierowanie musi nastąpić po pierwszym wyrenderowaniu. Wywołanie location.replace synchronicznie w <head> sprawi, że przeglądarka podmieni dokument, nigdy go nie pokazawszy — i biel wróci. Stąd podwójne requestAnimationFrame.
  • Żadnych zależnych zasobów. Logo jest wklejone w plik celowo. Odwołanie do zewnętrznego obrazka czy arkusza stylów dokłada podróż w obie strony dokładnie w tym momencie, który mieliśmy uratować.

Strona jest wykluczona z matchera w middleware.ts — przejście przez sprawdzanie sesji przekreślałoby jej sens.

Adres startowy to /app-start, bez rozszerzenia. Cloudflare Workers Assets domyślnie obcina .html i przekierowuje, więc wskazanie na /app-start.html dokładałoby jedną podróż w obie strony przy każdym starcie — czyli dokładnie to, co ta strona ma eliminować. Sam plik nazywa się app-start.html, bo tak wymaga katalog public/; wskazuje się na niego bez rozszerzenia.

Dodatkowo app/globals.css ustawia ciemne tło dokumentu w trybie display-mode: standalone, czyli wyłącznie wewnątrz zainstalowanej aplikacji — w zwykłej przeglądarce strony zachowują swoje jasne tło.

Android (TWA)

Aplikacja na Androida to Trusted Web Activity generowana przez Bubblewrap. Motyw Theme.AceParkLauncher dokłada do przezroczystego motywu Bubblewrapa windowBackground z logo na ciemnym tle, dzięki czemu pierwsza klatka po dotknięciu ikony jest już splashem, a nie pustym oknem.

Zmiany w app/src/main/AndroidManifest.xml, app/src/main/res/values/styles.xml i app/src/main/res/drawable/launch_splash.xml są nadpisywane przez bubblewrap update — po każdej aktualizacji projektu natywnego trzeba je przywrócić.

Weryfikacja, że zasoby się kompilują, bez pełnego wydania:

JAVA_HOME=$HOME/.bubblewrap/jdk/jdk-17.0.11+9/Contents/Home ANDROID_HOME=$HOME/.bubblewrap/android_sdk ./gradlew :app:processDebugResources

iOS (Capacitor)

iOS pokazuje LaunchScreen.storyboard natychmiast po dotknięciu ikony — tło jest tam ustawione na ten sam odcień #1a1a1a, podobnie jak backgroundColor wtyczki SplashScreen w capacitor.config.ts. Dlatego na iOS problem białego mignięcia nie występuje i te pliki nie wymagały zmian.

Zmiana kolorystyki

Przy zmianie koloru ekranu startowego trzeba zaktualizować komplet miejsc, inaczej wróci przebłysk:

  • public/manifest.jsonbackground_color
  • twa-manifest.json oraz app/build.gradlebackgroundColor
  • public/app-start.html → tło body oraz kolor piłki
  • app/globals.css → tło html w display-mode: standalone oraz gradient .splash-screen
  • capacitor.config.tsplugins.SplashScreen.backgroundColor
  • ios/App/App/Base.lproj/LaunchScreen.storyboardbackgroundColor widoku