Obsługa różnych rozmiarów ekranu

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

Aby układy aplikacji były dostosowane do jak największej liczby rozmiarów ekranu, zaprojektuj je tak, aby były elastyczne i adaptacyjne. Układy elastyczne i adaptacyjne zapewniają wygodę użytkowników niezależnie od rozmiaru ekranu. Dzięki temu aplikacja może wyświetlać się na telefonach, tabletach, urządzeniach składanych i z ChromeOS oraz w orientacji pionowej i poziomej, a także w konfiguracji z możliwością zmiany rozmiaru, np. w trybie wielu okien.

Układy elastyczne/adaptacyjne zmieniają się w zależności od dostępnej przestrzeni w sieci reklamowej. Zmiany mogą być różne – od niewielkich korekt układu, które wypełniają przestrzeń (projektowanie responsywne), po całkowite zastąpienie jednego układu innym, by aplikacja lepiej dopasować się do różnych rozmiarów wyświetlacza (projekt adaptacyjny).

Jetpack Compose to pakiet narzędzi interfejsu i idealnie nadaje się do projektowania i implementowania układów, które dynamicznie się zmieniają, by renderować treści w odmienny sposób na ekranach o różnych rozmiarach.

Wprowadź duże zmiany układu elementów kompozycyjnych na poziomie ekranu

Gdy używasz opcji Utwórz do układania całej aplikacji, kompozycje na poziomie aplikacji i ekranu zajmują całe miejsce przeznaczone na renderowanie przez Twoją aplikację. Na tym poziomie projektu warto zmienić ogólny układ ekranu, aby wykorzystać większy ekran.

Unikaj podejmowania decyzji o układzie sprzętowym. Może być kuszące, aby podejmować decyzje na podstawie ustalonej wartości materialnej (czy urządzenie to tablet? Czy fizyczny ekran ma określony współczynnik proporcji?), ale odpowiedzi na te pytania mogą nie być przydatne podczas określania miejsca, w którym można pracować z interfejsem.

Schemat przedstawiający kilka formatów urządzeń, w tym telefon, urządzenie składane, tablet i laptop.
Rysunek 1. Modele telefonu, składanego, tabletu i laptopa

Na tablecie aplikacja może działać w trybie wielu okien, co oznacza, że może ona podzielić ekran na inną aplikację. W ChromeOS aplikacja może znajdować się w oknie z możliwością zmiany rozmiaru. Może istnieć nawet więcej niż jeden ekran fizyczny, na przykład składany. W takich przypadkach fizyczny rozmiar ekranu nie ma znaczenia przy podejmowaniu decyzji o wyświetlaniu treści.

Zamiast tego należy podejmować decyzje na podstawie rzeczywistej części ekranu przydzielonej do aplikacji, np. bieżące wskaźniki okien dostarczane przez bibliotekę WindowManager Jetpack. Aby dowiedzieć się, jak używać WindowManagera w aplikacji do tworzenia wiadomości, zobacz przykład JetNews.

Dzięki temu aplikacja będzie bardziej elastyczna, ponieważ będzie dobrze działać we wszystkich powyższych scenariuszach. Dostosowanie układów do miejsca na ekranie ogranicza też konieczność specjalnej obsługi platform takich jak ChromeOS i formatów takich jak tablety czy urządzenia składane.

Gdy zauważysz odpowiednią ilość miejsca dostępnego dla aplikacji, warto przekonwertować nieprzetworzony rozmiar na odpowiednią klasę rozmiaru zgodnie z opisem w sekcji Klasy rozmiaru okna. Pogrupuje on rozmiary w standardowe zasobniki rozmiarów, które są punktami przerwania zaprojektowanymi tak, aby zrównoważyć prostotę z elastycznością optymalizowania aplikacji pod kątem większości unikalnych przypadków. Te klasy rozmiaru odnoszą się do ogólnego okna aplikacji, więc używaj ich do podejmowania decyzji o układzie aplikacji, które mają wpływ na ogólny układ ekranu. Te klasy rozmiarów możesz przekazywać w trybie stanu lub wykonać dodatkową logikę, aby utworzyć stan zależny, który zostanie przekazany do zagnieżdżonych funkcji kompozycyjnych.

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@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,
        /* ... */
    )
}

Ta warstwowa metoda ogranicza logikę rozmiaru ekranu w jednym miejscu zamiast ją rozłożyć w wielu miejscach, które wymagają synchronizacji. Ta pojedyncza lokalizacja tworzy stan, który można bezpośrednio przekazać do innych funkcji kompozycyjnych, tak jak w przypadku każdego innego stanu aplikacji. Bezpośrednie przekazywanie stanu upraszcza poszczególne funkcje kompozycyjne, ponieważ są to zwykłe funkcje kompozycyjne, które przyjmują klasę rozmiaru lub określoną konfigurację wraz z innymi danymi.

Elastyczne zagnieżdżone elementy kompozycyjne można wykorzystywać wielokrotnie

Urządzenia kompozycyjne są bardziej przydatne, jeśli można umieścić je w różnych miejscach. Jeśli funkcja kompozycyjna zakłada, że będzie zawsze umieszczona w określonej lokalizacji o określonym rozmiarze, trudniej będzie użyć jej w innym miejscu w innej lokalizacji lub z inną ilością dostępnego miejsca. Oznacza to również, że pojedyncze elementy kompozycyjne wielokrotnego użytku powinny unikać pośredniego wykorzystania „globalnych” informacji o rozmiarze.

Rozważmy ten przykład: wyobraźmy sobie zagnieżdżony funkcja kompozycyjna, która implementuje układ szczegółów listy, który może wyświetlać 1 panel lub 2 panele obok siebie.

Zrzut ekranu aplikacji z 2 panelami obok siebie.
Rysunek 2. Zrzut ekranu aplikacji z typowym układem szczegółowym listy – 1 to obszar listy, a 2 – obszar szczegółów.

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

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

A co, jeśli zamiast tego chcemy, aby funkcja kompozycyjna zmieniała swój układ niezależnie od dostępnego miejsca? Może to być na przykład karta, która chce wyświetlać dodatkowe szczegóły, jeśli jest na to miejsce. Chcemy wykonać pewne logiki na podstawie dostępnego rozmiaru, ale jaki konkretnie?

Przykłady 2 różnych kart.
Rysunek 3. Wąska karta zawierająca tylko ikonę i tytuł oraz szersza karta zawierająca ikonę, tytuł i krótki opis.

Jak już wspomnieliśmy, nie należy próbować korzystać z rozmiaru rzeczywistego ekranu urządzenia. Nie będzie to dokładne w przypadku kilku ekranów ani też nie będzie dokładne, jeśli aplikacja nie jest w trybie pełnoekranowym.

Ponieważ funkcja kompozycyjna nie jest funkcją kompozycyjną na poziomie ekranu, nie należy też bezpośrednio używać bieżących danych dotyczących okien, aby zmaksymalizować możliwość wielokrotnego wykorzystania. Jeśli komponent jest umieszczany z dopełnieniem (np. jako wstawki) lub jeśli występują w nim takie komponenty jak linie nawigacyjne czy paski aplikacji, ilość miejsca dostępna w komponencie może znacznie się różnić od łącznej przestrzeni dostępnej dla aplikacji.

Dlatego na potrzeby renderowania należy użyć szerokości, jaką ma rzeczywiście funkcja kompozycyjna. Tę szerokość można ustawić na 2 sposoby:

Jeśli chcesz zmienić miejsce lub sposób wyświetlania treści, możesz użyć kolekcji modyfikatorów lub układu niestandardowego, aby zapewnić jego elastyczność. Może to być po prostu wypełnienie całego dostępnego miejsca przez dziecko lub układanie dzieci z kilkoma kolumnami, jeśli jest wystarczająco dużo miejsca.

Jeśli chcesz zmienić to, co wyświetlasz, możesz użyć BoxWithConstraints jako bardziej efektywnej alternatywy. Ten element kompozycyjny udostępnia ograniczenia pomiarów, których można używać do wywoływania różnych elementów kompozycyjnych w zależności od dostępnej przestrzeni. Wiąże się to jednak z pewnymi kosztami, ponieważ funkcja BoxWithConstraints opóźnia kompozycję do fazy układu, gdy te ograniczenia są 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(/* ... */)
            }
        }
    }
}

Sprawdź, czy wszystkie dane są dostępne dla różnych rozmiarów

Wykorzystując dodatkową przestrzeń, na dużym ekranie możesz mieć miejsce na wyświetlenie użytkownikowi większej ilości treści niż na małym ekranie. Podczas implementowania funkcji kompozycyjnej z tym zachowaniem kusząca może być efektywność, a wczytywanie danych jest efektem ubocznym obecnego rozmiaru.

Jest to jednak niezgodne z zasadami jednokierunkowego przepływu danych, w ramach którego dane można przenosić i przekazywać do komponentów kompozycyjnych w celu odpowiedniego renderowania. Element kompozycyjny powinien mieć wystarczająco dużo danych, aby element kompozycyjny zawsze miał to, co musi wyświetlać w dowolnych rozmiarach, nawet jeśli część danych nie zawsze będzie 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)
            }
        }
    }
}

Nawiązując do przykładu Card, pamiętaj, że zawsze przekazujemy właściwość description do funkcji Card. Chociaż właściwość description jest używana tylko wtedy, gdy szerokość pozwala na jej wyświetlenie, Card zawsze jej wymaga, niezależnie od dostępnej szerokości.

Zawsze przekazywane dane upraszczają układy adaptacyjne, ponieważ są mniej stanowe, i pozwalają uniknąć efektów ubocznych przy przełączaniu się między rozmiarami (które mogą wynikać ze zmiany rozmiaru okna, orientacji oraz złożenia i rozłożenia urządzenia).

Ta zasada umożliwia też zachowywanie stanu po zmianie układu. Przenosząc informacje, które mogą nie być używane w dowolnym rozmiarze, możemy zachować stan użytkownika wraz ze zmianą rozmiaru układu. Możemy na przykład umieścić flagę wartości logicznej showMore, żeby zachować stan użytkownika, gdy zmiany rozmiaru spowodują, że układ będzie się przełączał 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 układach niestandardowych w sekcji Utwórz znajdziesz w tych dodatkowych materiałach.

Przykładowe aplikacje

  • Układy kanoniczne na duże ekrany to repozytorium sprawdzonych wzorców projektowych, które zapewniają optymalne wrażenia użytkowników na urządzeniach z dużymi ekranami
  • JetNews pokazuje, jak zaprojektować aplikację, która dostosowuje interfejs do wykorzystania dostępnego miejsca.
  • Odpowiedź to adaptacyjna próbka treści na urządzenia mobilne, tablety i urządzenia składane
  • Now in Android to aplikacja, która korzysta z adaptacyjnych układów do obsługi różnych rozmiarów ekranu.

Filmy