Tworzenie układów adaptacyjnych

Interfejs aplikacji powinien dostosowywać się do różnych rozmiarów, orientacji i formatów ekranów. Układ adaptacyjny zmienia się w zależności od dostępnego miejsca na ekranie. Zmiany te obejmują proste dostosowywanie układu, przez wypełnienie przestrzeni, po całkowite modyfikowanie układów, by wykorzystać dodatkowe miejsce.

Jetpack Compose to zestaw narzędzi interfejsu, który doskonale sprawdza się przy projektowaniu i wdrażaniu układów, które dostosowują się do różnych rozmiarów i wyświetlają treści w różny sposób. Ten dokument zawiera wskazówki, jak korzystać z funkcji tworzenia, aby interfejs użytkownika był elastyczny.

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. Podejmowanie decyzji na podstawie ustalonej wartości materialnej może być kuszące (czy urządzenie to tablet? Czy fizyczny ekran ma określony współczynnik proporcji?), ale odpowiedzi na te pytania mogą nie być przydatne przy określaniu miejsca, w jakim można pracować za pomocą interfejsu użytkownika.

Schemat przedstawiający kilka formatów urządzeń – telefon, składany, tablet i laptop.

Na tablecie aplikacja może działać w trybie wielu okien, co oznacza, że prawdopodobnie dzieli 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. Rozmiary są grupowane w standardowe zasobniki rozmiarów, czyli punkty przerwania, które zostały zaprojektowane 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 przy podejmowaniu decyzji dotyczących układu, 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 będzie przekazywany do zagnieżdżonych funkcji kompozycyjnych.

class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            val windowSizeClass = calculateWindowSizeClass(this)
            MyApp(windowSizeClass)
        }
    }
}
@Composable
fun MyApp(windowSizeClass: WindowSizeClass) {
    // Perform logic on the size class to decide whether to show
    // the top app bar.
    val showTopAppBar = windowSizeClass.heightSizeClass != 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 powoduje utworzenie stanu, 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 innej lokalizacji lub z inną ilością dostępnego miejsca. Oznacza to również, że pojedyncze elementy kompozycyjne wielokrotnego użytku nie powinny domyślnie korzystać z globalnych informacji o rozmiarze.

Oto przykład: wyobraźmy sobie zagnieżdżony funkcja kompozycyjna, w której jest zaimplementowany układ szczegółów listy. Może on wyświetlać 1 panel lub 2 panele obok siebie.

Zrzut ekranu aplikacji z 2 panelami obok siebie

Rysunek 1. Zrzut ekranu aplikacji z typowym układem listy lub szczegółów. 1 to obszar listy, a 2 to 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 w zależności od dostępnego miejsca? Może to być na przykład karta, która chce wyświetlić dodatkowe szczegóły, jeśli jest na nie miejsce. Chcemy wykonać pewne obliczenia na podstawie dostępnego rozmiaru, ale który konkretnie?

Przykłady 2 różnych kart: wąska karta zawierająca tylko ikonę i tytuł oraz szersza z ikoną, tytułem i krótkim opisem

Jak już wspomnieliśmy, nie należy próbować korzystać z rozmiaru rzeczywistego ekranu urządzenia. Nie będzie to dokładne, jeśli aplikacja nie jest wyświetlana na pełnym ekranie.

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ć elastyczność układu. 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 zapewnia ograniczenia pomiarów, których można używać do wywoływania różnych elementów kompozycyjnych w zależności od dostępnego miejsca. Wiąże się to jednak z pewnymi kosztami, ponieważ funkcja BoxWithConstraints opóźnia kompozycję do fazy układu, gdy te ograniczenia są znane, co oznacza, ż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 którym dane można przenosić i w prosty sposób przekazywać do komponentów kompozycyjnych w celu odpowiedniego renderowania. Elementowi kompozycyjnemu należy dostarczyć wystarczającą ilość danych, aby zawsze miał to, co musi się wyświetlić w dowolnym rozmiarze, nawet jeśli część danych nie zawsze była 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. Przenoszenie informacji, które mogą nie być używane w dowolnym rozmiarze, pozwala zachować stan użytkownika wraz ze zmianą rozmiaru układu. Możemy na przykład umieścić flagę wartości logicznej showMore, aby zachować stan użytkownika, gdy zmiana rozmiaru spowoduje przełączanie 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 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 wykorzystuje adaptacyjne układy do obsługi różnych rozmiarów ekranu

Filmy