Поддержка различных размеров экрана

Поддержка экранов разных размеров обеспечивает доступ к вашему приложению на самых разных устройствах и наибольшему количеству пользователей.

Чтобы поддерживать как можно больше размеров экрана, создавайте макеты приложений адаптивными. Адаптивные/адаптивные макеты обеспечивают оптимизированное взаимодействие с пользователем независимо от размера экрана, позволяя вашему приложению адаптироваться к телефонам, планшетам, складным устройствам, устройствам ChromeOS, книжной и альбомной ориентации, а также конфигурациям с изменяемым размером, например многооконному режиму .

Отзывчивые/адаптивные макеты меняются в зависимости от доступного места на дисплее. Изменения варьируются от небольших корректировок макета, которые заполняют пространство (адаптивный дизайн), до полной замены одного макета другим, чтобы ваше приложение могло наилучшим образом соответствовать разным размерам дисплеев (адаптивный дизайн).

Jetpack Compose как декларативный набор инструментов пользовательского интерфейса идеально подходит для проектирования и реализации макетов, которые динамически изменяются для различного отображения контента на экранах различных размеров.

Вносите явные большие изменения макета для составных элементов на уровне экрана.

При использовании Compose для компоновки всего приложения компонуемые элементы на уровне приложения и на уровне экрана занимают все пространство, отведенное для рендеринга вашему приложению. На этом уровне вашего дизайна может иметь смысл изменить общую компоновку экрана, чтобы воспользоваться преимуществами экранов большего размера.

Избегайте использования физических, аппаратных значений для принятия решений по компоновке. Может возникнуть соблазн принять решения, основанные на фиксированной материальной ценности (является ли устройство планшетом? Имеет ли физический экран определенное соотношение сторон?), но ответы на эти вопросы могут оказаться бесполезными для определения пространства, в котором может работать ваш пользовательский интерфейс. с.

На схеме показано несколько различных форм-факторов устройств, включая телефон, складной телефон, планшет и ноутбук.
Рис. 1. Форм-факторы телефона, складного устройства, планшета и ноутбука

На планшетах приложение может работать в многооконном режиме, то есть оно может разделять экран с другим приложением. В ChromeOS приложение может находиться в окне изменяемого размера. Физических экранов может быть даже несколько, например, в складном устройстве. Во всех этих случаях физический размер экрана не имеет значения для принятия решения о том, как отображать контент.

Вместо этого вам следует принимать решения на основе фактической части экрана, выделенной вашему приложению, например, текущих показателей окна, предоставляемых библиотекой Jetpack WindowManager . Чтобы узнать, как использовать WindowManager в приложении Compose, ознакомьтесь с примером JetNews .

Следование этому подходу сделает ваше приложение более гибким, поскольку оно будет хорошо себя вести во всех описанных выше сценариях. Адаптация макетов к доступному им экранному пространству также уменьшает объем специальной обработки для поддержки таких платформ, как ChromeOS, и форм-факторов, таких как планшеты и складные устройства.

После того, как вы определите, какое пространство доступно для вашего приложения, полезно преобразовать необработанный размер в значимый класс размеров, как описано в разделе Классы размеров окон . При этом размеры группируются в сегменты стандартного размера, которые представляют собой точки останова, предназначенные для обеспечения баланса между простотой и гибкостью оптимизации вашего приложения для большинства уникальных случаев. Эти классы размеров относятся к общему окну вашего приложения, поэтому используйте эти классы для решений по макету, которые влияют на общий макет экрана. Вы можете передать эти классы размеров как состояние или выполнить дополнительную логику для создания производного состояния для передачи вложенным составным объектам.

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

Этот многоуровневый подход ограничивает логику размера экрана одним местом вместо того, чтобы разбрасывать ее по всему приложению во многих местах, которые необходимо синхронизировать. Это единственное местоположение создает состояние, которое можно явно передать другим составным объектам, как и для любого другого состояния приложения. Явная передача состояния упрощает отдельные составные объекты, поскольку они будут обычными составными функциями, которые принимают класс размера или указанную конфигурацию вместе с другими данными.

Гибкие вложенные составные элементы можно использовать повторно.

Составные элементы более пригодны для повторного использования, если их можно разместить в самых разных местах. Если составной элемент предполагает, что он всегда будет размещаться в определенном месте с определенным размером, то его будет сложнее повторно использовать в другом месте или с другим объемом доступного пространства. Это также означает, что отдельные, многократно используемые составные элементы должны избегать неявной зависимости от «глобальной» информации о размере .

Рассмотрим следующий пример: представьте себе вложенный компонуемый объект, реализующий макет списка , который может отображать одну или две панели рядом.

Снимок экрана приложения, на котором показаны две панели рядом.
Рис. 2. Снимок экрана приложения, показывающий типичный макет списка: 1 — область списка; 2 , область детализации.

Мы хотим, чтобы это решение было частью общего макета приложения, поэтому мы передаем решение из композиции на уровне экрана, как мы видели выше:

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

Что, если вместо этого мы хотим, чтобы компонуемый элемент самостоятельно менял свой макет в зависимости от доступного пространства? Например, карточка, на которой нужно показать дополнительную информацию, если позволяет место. Мы хотим выполнить некоторую логику, основанную на некотором доступном размере, но какой конкретно размер?

Примеры двух разных карт.
Рис. 3. Узкая карточка, показывающая только значок и заголовок, и более широкая карточка, показывающая значок, заголовок и краткое описание.

Как мы видели выше, нам следует избегать попыток использовать размер фактического экрана устройства. Это не будет точным для нескольких экранов, а также не будет точным, если приложение не полноэкранное.

Поскольку компонуемый объект не является компонуемым на уровне экрана, нам также не следует напрямую использовать метрики текущего окна, чтобы максимизировать возможность повторного использования. Если компонент размещается с отступом (например, для вставок) или если есть такие компоненты, как направляющие навигации или панели приложений, объем пространства, доступного для составного элемента, может значительно отличаться от общего пространства, доступного приложению.

Следовательно, мы должны использовать ширину, которую на самом деле присваивает составному объекту, для визуализации самого себя. У нас есть два варианта получить эту ширину:

Если вы хотите изменить место и способ отображения контента, вы можете использовать набор модификаторов или собственный макет, чтобы сделать макет адаптивным. Это может быть так же просто, как заставить какого-либо дочернего элемента заполнить все доступное пространство или расположить дочерние элементы в нескольких столбцах, если места достаточно.

Если вы хотите изменить то, что показываете, вы можете использовать BoxWithConstraints как более мощную альтернативу. Этот составной объект предоставляет ограничения измерения , которые можно использовать для вызова различных составных объектов в зависимости от доступного пространства. Однако это требует определенных затрат, поскольку BoxWithConstraints откладывает композицию до этапа макета, когда эти ограничения известны, что приводит к увеличению объема работы во время макета.

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

Убедитесь, что все данные доступны для разных размеров.

Если вы воспользуетесь дополнительным пространством экрана, на большом экране у вас может быть достаточно места, чтобы показать пользователю больше контента, чем на маленьком экране. При реализации составного объекта с таким поведением может возникнуть соблазн быть эффективным и загружать данные как побочный эффект текущего размера.

Однако это противоречит принципам однонаправленного потока данных, согласно которому данные можно поднимать и предоставлять компонуемым объектам для надлежащего рендеринга. Компонуемому объекту должно быть предоставлено достаточно данных, чтобы он всегда имел то, что ему необходимо отображать, в любом размере, даже если некоторая часть данных не всегда может использоваться.

@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)
            }
        }
    }
}

Опираясь на пример Card , обратите внимание, что мы всегда передаем description в Card . Несмотря на то, description используется только тогда, когда ширина позволяет его отобразить, Card всегда требует его, независимо от доступной ширины.

Постоянная передача данных упрощает адаптивные макеты, делая их менее отслеживающими состояние, и позволяет избежать побочных эффектов при переключении между размерами (которые могут возникнуть из-за изменения размера окна, изменения ориентации или складывания и раскладывания устройства).

Этот принцип также позволяет сохранять состояние при изменении макета. Поднимая информацию, которая не может быть использована во всех размерах, мы можем сохранить состояние пользователя при изменении размера макета. Например, мы можем поднять логический флаг showMore , чтобы состояние пользователя сохранялось, когда изменение размера приводит к переключению макета между скрытием и отображением описания:

@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)
            }
        }
    }
}

Узнать больше

Чтобы узнать больше о пользовательских макетах в Compose, обратитесь к следующим дополнительным ресурсам.

Примеры приложений

  • Канонические макеты для большого экрана — это хранилище проверенных шаблонов проектирования, которые обеспечивают оптимальное взаимодействие с пользователем на устройствах с большим экраном.
  • JetNews показывает, как разработать приложение, которое адаптирует свой пользовательский интерфейс для использования доступного пространства.
  • Ответ – это адаптивный образец для поддержки мобильных устройств, планшетов и складных устройств.
  • Теперь в Android есть приложение, использующее адаптивные макеты для поддержки экранов разных размеров.

Видео

{% дословно %} {% дословно %} {% дословно %} {% дословно %}