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

.

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

Aby obsługiwać jak najwięcej rozmiarów wyświetlacza – zarówno różnych ekranów urządzeń, jak i różnych okien aplikacji w trybie wielu okien – zaprojektuj układy aplikacji tak, aby były elastyczne i dostosowywały się do różnych rozmiarów. Elastyczne i adaptacyjne układy zapewniają optymalne wrażenia użytkownika niezależnie od rozmiaru wyświetlacza. Dzięki temu aplikacja może działać na telefonach, tabletach, urządzeniach składanych, urządzeniach z ChromeOS, w orientacji pionowej i poziomej oraz w konfiguracjach z możliwością zmiany rozmiaru wyświetlacza, takich jak tryb podzielonego ekranu i tryb okien na pulpicie.

Układy elastyczne/adaptacyjne zmieniają się w zależności od dostępnej przestrzeni wyświetlania. Zmiany mogą obejmować niewielkie korekty układu, które wypełniają przestrzeń (projektowanie elastyczne), lub całkowite zastąpienie jednego układu innym, aby aplikacja mogła jak najlepiej dostosować się do różnych rozmiarów wyświetlacza (projektowanie adaptacyjne).

Jetpack Compose to deklaratywny zestaw narzędzi do interfejsu, który idealnie nadaje się do projektowania i wdrażania układów, które dynamicznie się zmieniają, aby inaczej renderować treści na ekranach o różnych rozmiarach.

Wyraźne wprowadzanie dużych zmian w układzie w przypadku komponentów kompozycyjnych na poziomie treści

Komponenty na poziomie aplikacji i treści zajmują całą przestrzeń wyświetlania dostępną dla aplikacji. W przypadku tych typów komponentów może być przydatna zmiana ogólnego układu aplikacji na dużych wyświetlaczach.

Unikaj używania wartości sprzętu fizycznego do podejmowania decyzji dotyczących układu. Może się wydawać, że warto podejmować decyzje na podstawie stałej, konkretnej wartości (czy urządzenie to tablet? Czy ekran fizyczny ma określony współczynnik proporcji?), ale odpowiedzi na te pytania mogą nie być przydatne do określenia miejsca dostępnego dla interfejsu.

Rysunek 1. Telefony, urządzenia składane, tablety i laptopy

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

Zamiast tego podejmuj decyzje na podstawie rzeczywistej części ekranu przydzielonej do aplikacji, opisanej przez bieżące dane okna dostarczane przez bibliotekę Jetpack WindowManager. Przykład użycia WindowManager w aplikacji napisanej w Compose znajdziesz w przykładowej aplikacji JetNews.

Dostosowywanie układów do dostępnej przestrzeni wyświetlania zmniejsza też ilość specjalnych działań potrzebnych do obsługi platform takich jak ChromeOS i formatów takich jak tablety i urządzenia składane.

Gdy określisz dane przestrzeni dostępnej dla aplikacji, przekonwertuj surowy rozmiar na klasę rozmiaru okna zgodnie z opisem w artykule Używanie klas rozmiaru okna. Klasy rozmiaru okna to punkty przerwania zaprojektowane tak, aby zachować równowagę między prostotą logiki aplikacji a elastycznością optymalizacji aplikacji pod kątem większości rozmiarów wyświetlaczy.

Klasy rozmiaru okna odnoszą się do ogólnego okna aplikacji, więc używaj ich do podejmowania decyzji dotyczących układu, które wpływają na ogólny układ aplikacji. Klasy rozmiaru okna możesz przekazywać jako stan lub możesz wykonać dodatkową logikę, aby utworzyć stan pochodny, który będzie przekazywany do zagnieżdżonych funkcji kompozycyjnych.

@Composable
fun MyApp(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
    // Decide whether to show the top app bar based on window size class.
    val showTopAppBar = windowSizeClass.isHeightAtLeastBreakpoint(WindowSizeClass.HEIGHT_DP_MEDIUM_LOWER_BOUND)

    // MyScreen logic is based on the showTopAppBar boolean flag.
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

Podejście warstwowe ogranicza logikę rozmiaru wyświetlania do jednego miejsca zamiast rozpraszać ją w wielu miejscach w aplikacji, które muszą być zsynchronizowane. Pojedyncza lokalizacja generuje stan, który można jawnie przekazywać do innych funkcji kompozycyjnych, tak jak każdy inny stan aplikacji. Jawne przekazywanie stanu upraszcza poszczególne funkcje kompozycyjne, ponieważ przyjmują one klasę rozmiaru okna lub określoną konfigurację wraz z innymi danymi.

Elastyczne zagnieżdżone komponenty kompozycyjne można ponownie wykorzystywać

Funkcje kompozycyjne są bardziej wielokrotnego użytku, gdy można je umieszczać w różnych miejscach. Jeśli komponent musi być umieszczony w określonym miejscu i mieć określony rozmiar, prawdopodobnie nie będzie można go użyć w innych kontekstach. Oznacza to również, że poszczególne komponenty wielokrotnego użytku nie powinny w sposób dorozumiany zależeć od globalnych informacji o rozmiarze wyświetlacza.

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

Aplikacja wyświetlająca 2 panele obok siebie.
Rysunek 2. Aplikacja z typowym układem lista–szczegóły: 1 to obszar listy, a 2 to obszar szczegółów.

Decyzja dotycząca listy i szczegółów powinna być częścią ogólnego układu aplikacji, więc jest przekazywana z komponentu na poziomie treści:

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

A co jeśli chcesz, aby komponent zmieniał układ niezależnie od dostępnej przestrzeni wyświetlania, np. karta, która wyświetla dodatkowe szczegóły, jeśli jest na to miejsce? Chcesz zastosować logikę na podstawie dostępnego rozmiaru wyświetlania, ale którego konkretnie?

Rysunek 3. Wąska karta z samą ikoną i tytułem oraz szersza karta z ikoną, tytułem i krótkim opisem.

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

Komponent nie jest komponentem na poziomie treści, więc nie używaj bezpośrednio danych z bieżącego okna.

Jeśli komponent jest umieszczony z dopełnieniem (np. z wcięciami) lub jeśli aplikacja zawiera komponenty takie jak paski nawigacyjne czy paski aplikacji, ilość miejsca na ekranie dostępnego dla funkcji kompozycyjnej może znacznie różnić się od ogólnej ilości miejsca dostępnego dla aplikacji.

Użyj szerokości, którą komponent otrzymał do renderowania. Szerokość możesz uzyskać na 2 sposoby:

  • Jeśli chcesz zmienić miejsce lub sposób wyświetlania treści, użyj kolekcji modyfikatorów lub niestandardowego układu, aby układ był elastyczny. Może to być tak proste, jak wypełnienie całego dostępnego miejsca przez dziecko lub ułożenie dzieci 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 funkcji kompozycyjnych w zależności od dostępnej przestrzeni wyświetlania. Ma to jednak pewne wady, ponieważ BoxWithConstraints odracza kompozycję do fazy układu, gdy znane są te ograniczenia, co powoduje, że podczas układu wykonywanych jest więcej działań.

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

Udostępnianie wszystkich danych w przypadku różnych rozmiarów wyświetlacza

Podczas implementowania komponentu, który wykorzystuje dodatkową przestrzeń wyświetlania, możesz ulec pokusie, aby zaoszczędzić czas i wczytywać dane jako efekt uboczny bieżącego rozmiaru wyświetlania.

Jest to jednak sprzeczne z zasadą jednokierunkowego przepływu danych, zgodnie z którą dane można przekazywać do funkcji kompozycyjnych, aby je odpowiednio renderować. Kompozycja powinna mieć wystarczającą ilość danych, aby zawsze zawierała 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)
            }
        }
    }
}

Na podstawie Card przykładu zauważ, że description jest zawsze przekazywany do description.Card Chociaż właściwość description jest używana tylko wtedy, gdy szerokość pozwala na jej wyświetlenie, właściwość Card zawsze wymaga właściwości description, niezależnie od dostępnej szerokości.

Przekazywanie zawsze wystarczającej ilości treści upraszcza układy adaptacyjne, ponieważ sprawia, że są one mniej zależne od stanu, i zapobiega wywoływaniu efektów ubocznych podczas przełączania między rozmiarami wyświetlacza (co może się zdarzyć z powodu zmiany rozmiaru okna, zmiany orientacji lub składania i rozkładania urządzenia).

Zasada ta umożliwia też zachowanie stanu podczas zmian układu. Przenosząc informacje, które mogą nie być używane przy wszystkich rozmiarach wyświetlacza, możesz zachować stan aplikacji, gdy zmienia się rozmiar układu.

Możesz na przykład podnieść showMore flagę logiczną, aby stan aplikacji był zachowywany, gdy zmiana rozmiaru ekranu powoduje przełączanie 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 układach adaptacyjnych w Compose znajdziesz w tych materiałach:

Przykładowe aplikacje

  • CanonicalLayouts to repozytorium sprawdzonych wzorców projektowych, które zapewniają optymalne wrażenia użytkownika na dużych wyświetlaczach.
  • JetNews pokazuje, jak zaprojektować aplikację, która dostosowuje interfejs, aby wykorzystać dostępną przestrzeń wyświetlania.
  • Reply to adaptacyjny przykład obsługujący urządzenia mobilne, tablety i urządzenia składane.
  • Now in Android to aplikacja, która korzysta z elastycznych układów, aby obsługiwać różne rozmiary wyświetlaczy.

Filmy