Obsługa różnych rozmiarów ekranu

Obsługa różnych rozmiarów ekranów umożliwia dostęp do aplikacji z największej liczby urządzeń i dla największej liczby użytkowników.

Aby obsługiwać jak najwięcej rozmiarów ekranów, zaprojektuj układy aplikacji tak, aby były elastyczne i dostosowywały się do ekranu. Elastyczne układy zapewniają optymalne wrażenia użytkownika niezależnie od rozmiaru ekranu, dzięki czemu aplikacja może działać na telefonach, tabletach, urządzeniach składanych, urządzeniach z ChromeOS, w orientacji pionowej i poziomej oraz w konfiguracjach umożliwiających zmianę rozmiaru, np. trybie wielookiennym.

Elastyczne/adaptacyjne układy zmieniają się w zależności od dostępnej przestrzeni wyświetlania. Zmiany te obejmują od drobnych korekt układu, które wypełniają wolną przestrzeń (projekt elastyczny), po całkowite zastąpienie jednego układu innym, aby aplikacja mogła lepiej dostosować się do różnych rozmiarów ekranów (projekt adaptacyjny).

Jako deklaratywny zestaw narzędzi do tworzenia interfejsu Jetpack Compose doskonale nadaje się do projektowania i wdrażania układów, które dynamicznie zmieniają się, aby renderować zawartość w różny sposób na ekranach o różnych rozmiarach.

Umożliwienie wprowadzania dużych zmian układu w komponentach na poziomie ekranu

Gdy używasz Compose do układania całej aplikacji, komponenty na poziomie aplikacji i poziomu ekranu zajmują całą przestrzeń przeznaczoną do renderowania przez aplikację. Na tym poziomie projektu warto zmienić ogólny układ ekranu, aby wykorzystać większy ekran.

Unikaj korzystania z wartości sprzętowych podczas podejmowania decyzji dotyczących układu. Możesz być skłonny podejmować decyzje na podstawie stałej wartości (czy urządzenie to tablet? Czy fizyczny ekran ma określony format obrazu?), ale odpowiedzi na te pytania mogą nie być przydatne do określenia przestrzeni, w której może działać interfejs użytkownika.

Diagram pokazujący różne formaty urządzeń, w tym telefon, urządzenie składane, tablet i laptop.
Rysunek 1. Formaty: telefon, urządzenie składane, tablet i laptop.

Na tabletach aplikacja może działać w trybie wielookiennym, co oznacza, że może dzielić ekran z inną aplikacją. W ChromeOS aplikacja może być w oknie, którego rozmiar można zmieniać. Może być nawet więcej niż 1 ekran fizyczny, np. w przypadku urządzenia składanego. We wszystkich tych przypadkach fizyczny rozmiar ekranu nie ma znaczenia przy podejmowaniu decyzji o sposobie wyświetlania treści.

Zamiast tego należy podejmować decyzje na podstawie rzeczywistej części ekranu, która jest przypisana do aplikacji, np. bieżące dane okna udostępniane przez bibliotekę WindowManager z Jetpacka. Aby dowiedzieć się, jak używać WindowManager w aplikacji Compose, zapoznaj się ze samplami JetNews.

Dzięki temu Twoja aplikacja będzie bardziej elastyczna, ponieważ będzie działać prawidłowo we wszystkich opisanych powyżej scenariuszach. Sprawianie, że układy dostosowują się do dostępnej przestrzeni na ekranie, zmniejsza też ilość specjalnego przetwarzania, aby obsługiwać platformy takie jak ChromeOS oraz formaty takie jak tablety i urządzenia składane.

Gdy już określisz odpowiednią ilość miejsca dla swojej aplikacji, warto przekształcić rozmiar surowy w klasę rozmiaru, jak opisano w artykule Używanie klas rozmiarów okien. Ta funkcja grupowanie rozmiarów w standardowe przedziały rozmiarów, czyli punkty przecięcia, które mają na celu zapewnienie równowagi między prostotą a elastycznością, aby optymalizować aplikację pod kątem większości wyjątkowych przypadków. Te klasy rozmiarów odnoszą się do całego okna aplikacji, więc używaj ich do podejmowania decyzji dotyczących układu, które wpływają na ogólny układ ekranu. Możesz przekazywać te klasy rozmiarów jako stan lub wykonać dodatkową logikę, aby utworzyć stan pochodzenia, który zostanie przekazany do zagnieżdżonych komponentów.

@Composable
fun MyApp(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
    // Perform logic on the size class to decide whether to show the top app bar.
    val showTopAppBar = windowSizeClass.windowHeightSizeClass != WindowHeightSizeClass.COMPACT

    // MyScreen knows nothing about window sizes, and performs logic based on a Boolean flag.
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

Dzięki temu podejściu logika związana z rozmiarami ekranu jest ograniczona do jednego miejsca, zamiast być rozproszona w wielu miejscach w aplikacji, które muszą być synchronizowane. Ta pojedyncza lokalizacja wytwarza stan, który można jawnie przekazywać innym komponentom, tak jak w przypadku każdego innego stanu aplikacji. Wyraźne przekazywanie stanu upraszcza poszczególne komponenty, ponieważ będą one zwykłymi funkcjami komponentów, które przyjmują klasę rozmiaru lub określoną konfigurację wraz z innymi danymi.

Elastyczne składniki zagłębione można ponownie wykorzystać

Elementy składane można wielokrotnie wykorzystywać, ponieważ można je umieszczać w różnych miejscach. Jeśli komponent zakłada, że zawsze będzie umieszczany w określonym miejscu o określonym rozmiarze, będzie trudniej go wykorzystać w innej lokalizacji lub w innej ilości miejsca. Oznacza to też, że poszczególne komponenty wielokrotnego użytku nie powinny pośrednio zależeć od „globalnych” informacji o rozmiarze.

Rozważmy ten przykład: wyobraź sobie zagnieżdżony komponent, który implementuje układ listy z informacjami. Może on wyświetlać jedną lub 2 płytki obok siebie.

Zrzut ekranu aplikacji z 2 panelami obok siebie
Rysunek 2. Zrzut ekranu aplikacji pokazujący typowy układ listy i szczegółów. 1 to obszar listy, a 2 – obszar szczegółów.

Chcemy, aby ta decyzja była częścią ogólnego układu aplikacji, więc przekazujemy ją z komponowalnych elementów na poziomie ekranu, jak widać powyżej:

@Composable
fun AdaptivePane(
    showOnePane: Boolean,
    /* ... */
) {
    if (showOnePane) {
        OnePane(/* ... */)
    } else {
        TwoPane(/* ... */)
    }
}

Co zrobić, jeśli chcemy, aby kompozyt samodzielnie zmieniał układ na podstawie dostępnej przestrzeni? Na przykład karta, która ma wyświetlać dodatkowe informacje, jeśli pozwala na to miejsce. Chcemy wykonać pewną logikę na podstawie dostępnego rozmiaru, ale który rozmiar?

Przykłady 2 różnych kart
Rysunek 3. Węższa karta z tylko ikoną i tytułem oraz szersza karta z ikoną, tytułem i krótkim opisem.

Jak już wspomnieliśmy, nie należy używać rzeczywistego rozmiaru ekranu urządzenia. Nie będzie to dokładne w przypadku wielu ekranów ani w przypadku aplikacji, która nie jest wyświetlana na pełnym ekranie.

Ponieważ komponent nie jest komponentem na poziomie ekranu, nie powinniśmy też bezpośrednio używać danych bieżącego okna, aby zmaksymalizować możliwość ponownego użycia. Jeśli komponent jest umieszczany z odstępem (np. w przypadku wstawek) lub jeśli są komponenty takie jak szyny nawigacyjne czy paski aplikacji, ilość miejsca dostępna dla kompozytowego komponentu może się znacznie różnić od ogólnej ilości miejsca dostępnej dla aplikacji.

Dlatego powinniśmy użyć szerokości, która jest faktycznie przypisana do renderowania. Aby uzyskać taką szerokość, masz 2 opcje:

Jeśli chcesz zmienić gdziejak wyświetlane są treści, możesz użyć kolekcji modyfikatorów lub niestandardowego układu, aby uczynić układ elastycznym. Może to być tak proste, jak wypełnienie całej dostępnej przestrzeni przez jeden element potomny lub rozmieszczenie elementów potomnych w kilku kolumnach, jeśli jest na to miejsce.

Jeśli chcesz zmienić co wyświetlasz, możesz użyć BoxWithConstraints jako bardziej zaawansowanej alternatywy. Ten komponent udostępnia ograniczenia pomiarowe, których możesz używać do wywoływania różnych komponentów na podstawie dostępnej przestrzeni. Ma to jednak swoje minusy, ponieważ BoxWithConstraints odkłada kompozycję do fazy układu, gdy te ograniczenia są już znane, co powoduje, że podczas układu trzeba wykonać więcej pracy.

@Composable
fun Card(/* ... */) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(/* ... */)
                Title(/* ... */)
            }
        } else {
            Row {
                Column {
                    Title(/* ... */)
                    Description(/* ... */)
                }
                Image(/* ... */)
            }
        }
    }
}

Upewnij się, że wszystkie dane są dostępne dla różnych rozmiarów

Wykorzystując dodatkową przestrzeń na dużym ekranie, możesz wyświetlić użytkownikowi więcej treści niż na małym ekranie. Podczas implementowania komponentu z takim zachowaniem może pojawić się pokusa, by działać wydajnie i ładować dane jako efekt uboczny bieżącego rozmiaru.

Jest to jednak sprzeczne z zasadami jednokierunkowego przepływu danych, w których przypadku dane mogą być przenoszone i przekazywane do komponentów w celu odpowiedniego renderowania. Do komponentu należy przekazać wystarczającą ilość danych, aby zawsze miał on wszystko, czego potrzebuje do wyświetlania w dowolnym rozmiarze, nawet jeśli część danych może nie być zawsze używana.

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(description)
                }
                Image(imageUrl)
            }
        }
    }
}

W przypadku przykładu Card zawsze przekazujemy parametr description do funkcji Card. Chociaż element description jest używany tylko wtedy, gdy szerokość pozwala na wyświetlenie, element Card wymaga zawsze szerokości, niezależnie od dostępnej szerokości.

Przekazywanie danych zawsze upraszcza elastyczne układy, ponieważ powoduje, że mają one mniej stanów. Zapobiega to też wywoływaniu efektów ubocznych podczas przełączania się między rozmiarami (co może nastąpić w wyniku zmiany rozmiaru okna, orientacji lub złożenia i rozłożenia urządzenia).

Ta zasada umożliwia też zachowanie stanu przy zmianach układu. Dzięki przenoszeniu informacji, które mogą nie być używane we wszystkich rozmiarach, możemy zachować stan użytkownika, gdy zmienia się rozmiar układu. Możemy na przykład ustawić flagę logiczną showMore, aby stan użytkownika był zachowany, gdy zmiana rozmiaru powoduje przełączanie się układu między ukrywaniem a wyświetlaniem opisu:

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    var showMore by remember { mutableStateOf(false) }

    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(
                        description = description,
                        showMore = showMore,
                        onShowMoreToggled = { newValue ->
                            showMore = newValue
                        }
                    )
                }
                Image(imageUrl)
            }
        }
    }
}

Więcej informacji

Więcej informacji o niestandardowych układach w edytorze kompozycji znajdziesz w tych dodatkowych materiałach.

Przykładowe aplikacje

  • CanonicalLayouts to repozytorium sprawdzonych wzorów projektowych, które zapewniają optymalne wrażenia użytkowników na urządzeniach z dużym ekranem.
  • JetNews pokazuje, jak zaprojektować aplikację, która dostosowuje interfejs do dostępnej przestrzeni.
  • Reply to elastyczna próbka obsługująca urządzenia mobilne, tablety i urządzenia składane.
  • Nowości na Androida to aplikacja, która korzysta z układów elastycznych, aby obsługiwać różne rozmiary ekranu.

Filmy