Obsługa różnych rozmiarów ekranu

Obsługa różnych rozmiarów ekranów zapewnia dostęp do aplikacji na jak największej liczbie urządzeń i dla jak 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. tryb wielookienności.

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.

Wprowadzić duże zmiany układu w przypadku komponentów 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 używania fizycznych wartości sprzętowych przy podejmowaniu decyzji dotyczących układu. Podejmowanie decyzji na podstawie stałej wartości materialnej (np. tabletu) może być kuszące? Czy ekran fizyczny ma określony współczynnik proporcji?), ale odpowiedzi na te pytania mogą nie być przydatne przy określaniu przestrzeni, jaką zajmie 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, na przykład na urządzeniu składanym. 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, np. bieżących danych o oknie udostępnianych przez bibliotekę WindowManager z Jetpacka. Aby dowiedzieć się, jak używać WindowManager w aplikacji Compose, zapoznaj się ze samplami JetNews.

Takie podejście zwiększa elastyczność aplikacji, ponieważ będzie działać dobrze we wszystkich powyższych scenariuszach. Możliwość dostosowania układów do dostępnej przestrzeni ekranu ogranicza też specjalną obsługę na platformach takich jak ChromeOS i różnych formatach, takich jak tablety i urządzenia składane.

Po zaobserwowaniu odpowiedniej ilości miejsca dostępnego dla aplikacji warto przekonwertować nieprzetworzony rozmiar na odpowiednią klasę rozmiaru zgodnie z opisem w sekcji Używanie klas rozmiaru okna. 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 mają wpływ na ogólny układ ekranu. Możesz przekazać te klasy rozmiaru jako stan lub wykonać dodatkową logikę, aby utworzyć stan derwizowany, który zostanie przekazany do zagnieżdżonych funkcji kompozycyjnych.

@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 funkcja kompozycyjna zakłada, że zawsze będzie umieszczona w określonej lokalizacji o określonym rozmiarze, trudniej będzie jej użyć w innym miejscu w innej lokalizacji lub z inną ilością dostępnego miejsca. Oznacza to też, że pojedyncze elementy kompozycyjne wielokrotnego użytku nie powinny być domyślnie zależne od „globalnych” informacji o rozmiarze.

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

Zrzut ekranu aplikacji z 2 panelami obok siebie
Rysunek 2. Zrzut ekranu aplikacji z typowym układem listy szczegółów – obszar 1 to obszar listy, 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 przeprowadzić logikę na podstawie dostępnego rozmiaru, ale tylko pod kątem rozmiaru?

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 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ępnego dla 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. Dostępne są dwie opcje uzyskania tej szerokości:

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ć to, co wyświetlasz, użyj BoxWithConstraints – bardziej skutecznej 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.

Korzystając z dodatkowej przestrzeni 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 niezgodne z zasadami jednokierunkowego przepływu danych, w którym dane można wciągać i przekazywać do elementów kompozycyjnych w celu prawidłowego 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)
            }
        }
    }
}

Biorąc pod uwagę przykład Card, zanotuj, że zawsze przekazujemy 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ć na skutek zmiany rozmiaru okna, orientacji lub złożenia i rozłożenia urządzenia).

Ta zasada umożliwia też zachowanie stanu podczas zmian 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 przenieść flagę wartości logicznej showMore, aby zachować stan użytkownika, gdy zmiany rozmiaru powodują przełączanie się 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 funkcji tworzenia wiadomości znajdziesz w tych dodatkowych materiałach.

Przykładowe aplikacje

  • CanonicalLayouts to repozytorium sprawdzonych wzorców projektowych, które zapewniają optymalną wygodę użytkowników korzystających z urządzeń 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.
  • Teraz na Androida to aplikacja, która wykorzystuje układy adaptacyjne, aby obsługiwać różne rozmiary ekranu

Filmy