Создайте адаптивную навигацию

Навигация — это взаимодействие пользователя с пользовательским интерфейсом приложения для доступа к местам назначения контента. Принципы навигации Android содержат рекомендации, которые помогут вам создать последовательную и интуитивно понятную навигацию по приложениям.

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

Компонент Jetpack Navigation реализует принципы навигации и облегчает разработку приложений с отзывчивым/адаптивным пользовательским интерфейсом.

Рис. 1. Расширенный, средний и компактный дисплеи с навигационным ящиком, направляющей и нижней панелью.

Адаптивная навигация пользовательского интерфейса

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

Класс размера окна Мало предметов Много предметов
компактная ширина нижняя панель навигации панель навигации (передний край или низ)
средняя ширина навигационный рельс навигационный ящик (передний край)
расширенная ширина навигационный рельс постоянный ящик навигации (передний край)

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

<!-- res/layout/main_activity.xml -->

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        ... />

    <!-- Content view(s) -->
</androidx.constraintlayout.widget.ConstraintLayout>

<!-- res/layout-w600dp/main_activity.xml -->

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.navigationrail.NavigationRailView
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        ... />

    <!-- Content view(s) -->
</androidx.constraintlayout.widget.ConstraintLayout>

<!-- res/layout-w1240dp/main_activity.xml -->

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.navigation.NavigationView
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        ... />

    <!-- Content view(s) -->
</androidx.constraintlayout.widget.ConstraintLayout>

Адаптивные места назначения контента

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

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

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

Навигация к местам назначения контента как побочный эффект изменения размера окна имеет следующие проблемы:

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

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

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

Назначения контента с альтернативными макетами

В рамках адаптивного дизайна один пункт назначения навигации может иметь альтернативные макеты в зависимости от размера окна приложения. Каждый макет занимает все окно, но для разных размеров окон представлены разные макеты (адаптивный дизайн).

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

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

<!-- Single destination for list and detail. -->

<navigation ...>

    <!-- Fragment that implements SlidingPaneLayout. -->
    <fragment
        android:id="@+id/article_two_pane"
        android:name="com.example.app.ListDetailTwoPaneFragment" />

    <!-- Other destinations... -->
</navigation>

Подробные сведения о реализации макета с подробностями списка с помощью SlidingPaneLayout см. в разделе Создание макета с двумя панелями .

Один навигационный график

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

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

Вложенный навигационный узел

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

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

<!-- layout/two_pane_fragment.xml -->

<androidx.slidingpanelayout.widget.SlidingPaneLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/sliding_pane_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list_pane"
        android:layout_width="280dp"
        android:layout_height="match_parent"
        android:layout_gravity="start"/>

    <!-- Detail pane is a nested navigation host. Its graph is not connected
         to the main graph that contains the two_pane_fragment destination. -->
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/detail_pane"
        android:layout_width="300dp"
        android:layout_weight="1"
        android:layout_height="match_parent"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:navGraph="@navigation/detail_pane_nav_graph" />
</androidx.slidingpanelayout.widget.SlidingPaneLayout>

Это отличается от вложенного графа навигации, поскольку граф навигации вложенного NavHost не связан с основным графом навигации; то есть вы не можете напрямую переходить от пунктов назначения на одном графике к пунктам назначения на другом.

Дополнительные сведения см. в разделе Вложенные навигационные графики .

Сохраненное состояние

Чтобы обеспечить адаптивные места назначения контента, ваше приложение должно сохранять свое состояние при повороте или складывании устройства или изменении размера окна приложения. По умолчанию подобные изменения конфигурации воссоздают действия, фрагменты и иерархию представлений приложения. Рекомендуемый способ сохранить состояние пользовательского интерфейса — использовать ViewModel , который сохраняется при изменениях конфигурации. (См. Сохранение состояний пользовательского интерфейса .)

Изменения размера должны быть обратимыми — например, когда пользователь поворачивает устройство, а затем поворачивает его обратно.

Отзывчивые/адаптивные макеты могут отображать разный контент в окнах разных размеров; поэтому адаптивным макетам часто необходимо сохранять дополнительное состояние, связанное с содержимым, даже если оно неприменимо к текущему размеру окна. Например, в макете может быть место для отображения дополнительного виджета прокрутки только при большей ширине окна. Если событие изменения размера приводит к тому, что ширина окна становится слишком маленькой, виджет скрывается. Когда приложение изменяет размер до прежних размеров, виджет прокрутки снова становится видимым, и исходное положение прокрутки должно быть восстановлено.

Области ViewModel

Руководство разработчика компонента «Миграция на навигацию» предписывает архитектуру с одним действием, в которой пункты назначения реализуются как фрагменты, а их модели данных реализуются с помощью ViewModel .

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

В простейшем случае каждый пункт назначения навигации представляет собой отдельный фрагмент с полностью изолированным состоянием пользовательского интерфейса; и поэтому каждый фрагмент может использовать делегат свойства viewModels() для получения ViewModel ограниченной этим фрагментом.

Чтобы разделить состояние пользовательского интерфейса между фрагментами, примените ViewModel к действию, вызвав activityViewModels() во фрагментах (эквивалентом Activity является просто viewModels() ). Это позволяет активности и любым присоединенным к ней фрагментам совместно использовать экземпляр ViewModel . Однако в архитектуре с одним действием эта область ViewModel эффективно действует до тех пор, пока приложение, поэтому ViewModel остается в памяти, даже если никакие фрагменты ее не используют.

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

Вместо этого примените ViewModel к графу навигации в текущем NavController . Создайте вложенный граф навигации, чтобы инкапсулировать пункты назначения, являющиеся частью процесса оформления заказа. Затем в каждом из этих мест назначения фрагментов используйте делегат свойства navGraphViewModels() и передайте идентификатор графа навигации, чтобы получить общую ViewModel . Это гарантирует, что как только пользователь выйдет из процесса оформления заказа и вложенный граф навигации выйдет за пределы области действия, соответствующий экземпляр ViewModel будет удален и не будет использоваться для следующего оформления заказа.

Объем Делегат свойства Можно поделиться ViewModel с
Фрагмент Fragment.viewModels() Только фрагмент
Активность Activity.viewModels() или Fragment.activityViewModels() Активность и все прикрепленные к ней фрагменты
График навигации Fragment.navGraphViewModels() Все фрагменты в одном навигационном графе

Обратите внимание: если вы используете вложенный узел навигации (см. раздел «Вложенный узел навигации» ), пункты назначения на этом узле не могут совместно использовать экземпляры ViewModel с пунктами назначения за пределами узла при использовании navGraphViewModels() , поскольку графики не связаны. В этом случае вместо этого вы можете использовать область действия.

Дополнительные ресурсы