Obsługa różnych rozmiarów wyświetlaczy

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

Aby obsługiwać jak najwięcej rozmiarów ekranu – czy to ekranów na różnych urządzeniach, czy różnych okien aplikacji w trybie wielookienkowym – zaprojektuj układy aplikacji tak, aby były elastyczne i dostosowywały się do wyświetlacza. 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 z ChromeOS, w orientacji poziomej i pionowej oraz w konfiguracjach z możliwością zmiany rozmiaru ekranu, takich jak tryb podzielonego ekranu i okna na pulpicie.

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 zmieniają się dynamicznie, aby renderować zawartość w różny sposób na ekranach o różnych rozmiarach.

Wprowadzanie dużych zmian układu w komponowalnych elementach na poziomie treści

Komponenty na poziomie aplikacji i komponenty na poziomie treści zajmują całą dostępną przestrzeń wyświetlania aplikacji. W przypadku tego typu komponentów warto zmienić ogólny układ aplikacji na dużych ekranach.

Unikaj używania wartości sprzętowych do 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?), ale odpowiedzi na te pytania mogą nie być przydatne do określenia dostępnej przestrzeni dla interfejsu.

Rysunek 1. Formaty telefonu, urządzenia składanego, tabletu i laptopa

Na tabletach aplikacja może działać w trybie wielookiennym, co oznacza, że może dzielić ekran z inną aplikacją. W trybie okna na komputerze lub 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 przydzielonej aplikacji, opisanej przez bieżące dane o oknie udostępniane przez bibliotekę Jetpacka WindowManager. Przykład użycia WindowManager w aplikacji Compose znajdziesz w aplikacji JetNews.

Utworzenie układów dostosowanych do dostępnej przestrzeni wyświetlacza zmniejsza też ilość specjalnych operacji potrzebnych do obsługi platform takich jak ChromeOS oraz formatów takich jak tablety i urządzenia składane.

Po określeniu danych dotyczących przestrzeni dostępnej dla aplikacji przekonwertuj rozmiar surowy na klasę rozmiaru okna zgodnie z opisem w artykule Używanie klas rozmiaru okna. Klasy rozmiarów okna to punkty graniczne, które umożliwiają zachowanie równowagi między prostotą logiki aplikacji a elastycznością optymalizacji aplikacji pod kątem większości rozmiarów wyświetlacza. Klasy rozmiaru okna odnoszą się do całego okna aplikacji, dlatego używaj ich do podejmowania decyzji dotyczących układu, które wpływają na ogólny układ aplikacji. Klasy windowsize możesz przekazywać jako stan lub wykonać dodatkową logikę, aby utworzyć stan pochodzenia, który zostanie przekazany zagnieżdżonym elementom składanym.

@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,
        /* ... */
    )
}

Podejście oparte na warstwach ogranicza logikę rozmiaru wyświetlania do jednego miejsca, zamiast rozpraszać ją w wielu miejscach w aplikacji, które trzeba utrzymywać w zsynchronizowanym stanie. Pojedyncza lokalizacja generuje stan, który można jawnie przekazywać innym komponentom, tak jak każdy inny stan aplikacji. Przekazywanie stanu w wyraźny sposób upraszcza działanie poszczególnych komponentów, ponieważ wraz z innymi danymi przyjmują one klasę rozmiaru okna lub określoną konfigurację.

Elastyczne komponenty zagnieżdżone można ponownie wykorzystać

Elementy składane można wielokrotnie wykorzystywać, ponieważ można je umieszczać w różnych miejscach. Jeśli kompozyt musi być umieszczony w konkretnej lokalizacji o określonym rozmiarze, prawdopodobnie nie będzie można go używać w innych kontekstach. Oznacza to również, że pojedyncze komponenty wielokrotnego użytku nie powinny pośrednio zależeć od globalnych informacji o rozmiarze wyświetlacza.

Wyobraź sobie zagnieżdżony komponent, który implementuje układ listy i szczegółów. Może on wyświetlać jeden panel lub dwa panele obok siebie:

Aplikacja z 2 panelami wyświetlanymi obok siebie.
Rysunek 2. Aplikacja z typowym układem listy i szczegółów: 1 to obszar listy, a 2 – obszar szczegółów.

Decyzja o wyświetleniu listy czy szczegółów powinna być częścią ogólnego układu aplikacji, dlatego jest przekazywana z komponowalnych komponentów na poziomie treści:

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

Co zrobić, jeśli chcesz, aby kompozyt samodzielnie zmieniał układ w zależności od dostępnej przestrzeni wyświetlania, na przykład aby karta wyświetlała dodatkowe szczegóły, jeśli pozwala na to miejsce? Chcesz wykonać pewną operację logiczną na podstawie dostępnych rozmiarów wyświetlania, ale których?

Rysunek 3. Węższa karta z tylko ikoną i tytułem oraz szersza karta z ikoną, tytułem i krótkim opisem.

Nie używaj rzeczywistego rozmiaru ekranu urządzenia. Nie będzie to dokładne w przypadku różnych typów ekranów ani wtedy, gdy aplikacja nie jest wyświetlana na pełnym ekranie.

Ponieważ komponent nie jest komponentem na poziomie treści, nie używaj bezpośrednio bieżących danych o oknie. Jeśli komponent jest umieszczony z odstępem (np. w przypadku wstawek) lub jeśli aplikacja zawiera komponenty takie jak paski nawigacyjne czy paski aplikacji, ilość miejsca wyświetlania dostępna dla kompozytowego komponentu może się znacznie różnić od ogólnej ilości miejsca dostępnej dla aplikacji.

Użyj szerokości, która jest faktycznie używana przez komponent do renderowania. Dostępne są 2 opcje uzyskania tej szerokości:

  • Jeśli chcesz zmienić gdzie lub jak wyświetlane są treści, użyj kolekcji modyfikatorów lub niestandardowego układu, aby układ był responsywny. Może to być tak proste, jak wypełnienie przez dziecko całej dostępnej przestrzeni lub rozmieszczenie elementów w kilku kolumnach, jeśli jest wystarczająco dużo miejsca.

  • Jeśli chcesz zmienić co wyświetlasz, użyj BoxWithConstraints jako bardziej zaawansowanej alternatywy. BoxWithConstraints udostępnia ograniczenia pomiarowe, których możesz używać do wywoływania różnych komponentów na podstawie dostępnej przestrzeni wyświetlania. 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 w różnych rozmiarach wyświetlacza

Podczas wdrażania komponentu, który korzysta z dodatkowej przestrzeni wyświetlania, możesz mieć pokusę, aby w celu zwiększenia wydajności wczytywać dane jako efekt uboczny bieżącego rozmiaru wyświetlania.

Jest to jednak sprzeczne z zasadą jednokierunkowego przepływu danych, w której dane mogą być przenoszone i przekazywane do komponentów w celu odpowiedniego renderowania. Do komponentu należy przekazać wystarczającą ilość danych, aby zawsze zawierał wystarczającą ilość treści dla dowolnego rozmiaru wyświetlacza, nawet jeśli część treści 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 description jest zawsze przekazywane do Card. Chociaż element description jest używany tylko wtedy, gdy szerokość pozwala na jego wyświetlenie, element Card zawsze wymaga elementu description, niezależnie od dostępnej szerokości.

Przekazywanie wystarczającej ilości treści upraszcza tworzenie układów dostosowanych do urządzenia, ponieważ powoduje, że mają one mniej stanów, a także zapobiega skutkom ubocznym podczas przełączania się między rozmiarami wyświetlacza (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órych nie można używać w wszystkich rozmiarach wyświetlania, możesz zachować stan aplikacji podczas zmiany rozmiaru układu. Możesz na przykład użyć flagi logicznej showMore, aby stan aplikacji był zachowany, gdy zmiana rozmiaru wyświetlacza powoduje przełączanie się układu między ukrywaniem a wyświetlaniem treści:

@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 dopasowywaniu układów w Compose znajdziesz w tych materiałach:

Przykładowe aplikacje

  • CanonicalLayouts to repozytorium sprawdzonych schematów projektowania, które zapewniają optymalne wrażenia użytkowników na dużych ekranach.
  • JetNews pokazuje, jak zaprojektować aplikację, która dostosowuje interfejs użytkownika do dostępnej przestrzeni wyświetlania.
  • Reply to elastyczna próbka obsługująca telefony, tablety i urządzenia składane.
  • Nowość na Androida: aplikacja, która korzysta z układów elastycznych, aby obsługiwać różne rozmiary wyświetlacza

Filmy