Обработка изменений конфигурации

Адаптивный интерфейс и навигация

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

Примеры панели навигации, боковых панелей навигации и нижней панели приложения
Рисунок 1. Примеры панели навигации, боковых панелей навигации и нижней панели приложения.

В руководстве по архитектуре продуктов Material Design вы найдете дополнительную информацию и рекомендации по созданию адаптивного интерфейса, который динамически подстраивается под изменения среды. Примеры изменений среды: корректировка ширины, высоты, ориентации и языковых настроек пользователя. Эти свойства среды в совокупности называются конфигурацией устройства.

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

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

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

В практической работе по навигации используется панель навигации, которая всегда видна, если ширина экрана устройства составляет не менее 960 dp.
Рисунок 2. В практической работе по Navigation для отображения меню навигации используется NavigationView.

На устройствах других размеров и ориентаций по мере необходимости динамически переключаются макеты DrawerLayout и BottomNavigationView.

BottomNavigationView и DrawerLayout, которые используются для меню навигации в макетах для небольших устройств.
Рисунок 3. В практической работе по Navigation используются BottomNavigationView и DrawerLayout, чтобы показывать меню навигации на устройствах с небольшим экраном.

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

Конфигурация, к которой применяется каждый макет, определяется структурой каталога, в котором размещен файл макета. Например, файл макета NavigationView находится в каталоге res/layout-w960dp.

<!-- res/layout-w960dp/navigation_activity.xml -->
<RelativeLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context="com.example.android.codelabs.navigation.MainActivity">

   <com.google.android.material.navigation.NavigationView
       android:id="@+id/nav_view"
       android:layout_width="wrap_content"
       android:layout_height="match_parent"
       android:layout_alignParentStart="true"
       app:elevation="0dp"
       app:headerLayout="@layout/nav_view_header"
       app:menu="@menu/nav_drawer_menu" />

   <View
       android:layout_width="1dp"
       android:layout_height="match_parent"
       android:layout_toEndOf="@id/nav_view"
       android:background="?android:attr/listDivider" />

   <androidx.appcompat.widget.Toolbar
       android:id="@+id/toolbar"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_alignParentTop="true"
       android:layout_toEndOf="@id/nav_view"
       android:background="@color/colorPrimary"
       android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" />

   <androidx.fragment.app.FragmentContainerView
       android:id="@+id/my_nav_host_fragment"
       android:name="androidx.navigation.fragment.NavHostFragment"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:layout_below="@id/toolbar"
       android:layout_toEndOf="@id/nav_view"
       app:defaultNavHost="true"
       app:navGraph="@navigation/mobile_navigation" />
</RelativeLayout>

Нижняя панель навигации находится в каталоге res/layout-h470dp:

<!-- res/layout-h470dp/navigation_activity.xml -->
<LinearLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical"
   tools:context="com.example.android.codelabs.navigation.MainActivity">

   <androidx.appcompat.widget.Toolbar
       android:id="@+id/toolbar"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:background="@color/colorPrimary"
       android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" />

   <androidx.fragment.app.FragmentContainerView
       android:id="@+id/my_nav_host_fragment"
       android:name="androidx.navigation.fragment.NavHostFragment"
       android:layout_width="match_parent"
       android:layout_height="0dp"
       android:layout_weight="1"
       app:defaultNavHost="true"
       app:navGraph="@navigation/mobile_navigation" />

   <com.google.android.material.bottomnavigation.BottomNavigationView
       android:id="@+id/bottom_nav_view"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       app:menu="@menu/bottom_nav_menu" />
</LinearLayout>

Макет бокового меню находится в каталоге res/layout. Используйте этот каталог для макетов по умолчанию без модификаторов, связанных с конфигурацией:

<!-- res/layout/navigation_activity.xml -->
<androidx.drawerlayout.widget.DrawerLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:id="@+id/drawer_layout"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context="com.example.android.codelabs.navigation.MainActivity">

   <LinearLayout
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:orientation="vertical">

       <androidx.appcompat.widget.Toolbar
           android:id="@+id/toolbar"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:background="@color/colorPrimary"
           android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" />

       <androidx.fragment.app.FragmentContainerView
           android:id="@+id/my_nav_host_fragment"
           android:name="androidx.navigation.fragment.NavHostFragment"
           android:layout_width="match_parent"
           android:layout_height="match_parent"
           app:defaultNavHost="true"
           app:navGraph="@navigation/mobile_navigation" />
   </LinearLayout>

   <com.google.android.material.navigation.NavigationView
       android:id="@+id/nav_view"
       android:layout_width="wrap_content"
       android:layout_height="match_parent"
       android:layout_gravity="start"
       app:menu="@menu/nav_drawer_menu" />
</androidx.drawerlayout.widget.DrawerLayout>

При определении того, какие ресурсы следует применить, Android следует порядку приоритета. В этом примере -w960dp (или доступная ширина >= 960 dp) имеет приоритет над -h470dp (или доступная высота >= 470). Если конфигурация устройства не соответствует ни одному из этих условий, используется ресурс разметки по умолчанию (res/layout/navigation_activity.xml).

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

Kotlin

class MainActivity : AppCompatActivity() {

   private lateinit var appBarConfiguration : AppBarConfiguration

   override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      setContentView(R.layout.navigation_activity)
      val drawerLayout : DrawerLayout? = findViewById(R.id.drawer_layout)
      appBarConfiguration = AppBarConfiguration(
                  setOf(R.id.home_dest, R.id.deeplink_dest),
                  drawerLayout)

      ...

      // Initialize the app bar with the navigation drawer if present.
      // If the drawerLayout is not null here, a Navigation button will be added
      // to the app bar whenever the user is on a top-level destination.
      setupActionBarWithNavController(navController, appBarConfig)

      // Initialize the NavigationView if it is present,
      // so that clicking an item takes
      // the user to the appropriate destination.
      val sideNavView = findViewById<NavigationView>(R.id.nav_view)
      sideNavView?.setupWithNavController(navController)

      // Initialize the BottomNavigationView if it is present,
      // so that clicking an item takes
      // the user to the appropriate destination.
      val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_nav_view)
      bottomNav?.setupWithNavController(navController)

      ...
    }

    ...
}

Java

public class MainActivity extends AppCompatActivity {

   private AppBarConfiguration appBarConfiguration;

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.navigation_activity);
       NavHostFragment host = (NavHostFragment) getSupportFragmentManager()
               .findFragmentById(R.id.my_nav_host_fragment);
       NavController navController = host.getNavController();

       DrawerLayout drawerLayout = findViewById(R.id.drawer_layout);
       appBarConfiguration = new AppBarConfiguration.Builder(
               R.id.home_dest, R.id.deeplink_dest)
               .setDrawerLayout(drawerLayout)
               .build();

       // Initialize the app bar with the navigation drawer if present.
       // If the drawerLayout is not null here, a Navigation button will be added to
       // the app bar whenever the user is on a top-level destination.
       NavigationUI.setupActionBarWithNavController(
               this, navController, appBarConfiguration);


       // Initialize the NavigationView if it is present,
       // so that clicking an item takes
       // the user to the appropriate destination.
       NavigationView sideNavView = findViewById(R.id.nav_view);
       if(sideNavView != null) {
           NavigationUI.setupWithNavController(sideNavView, navController);
       }

       // Initialize the BottomNavigationView if it is present,
       // so that clicking an item takes
       // the user to the appropriate destination.
       BottomNavigationView bottomNav = findViewById(R.id.bottom_nav_view);
       if(bottomNav != null) {
           NavigationUI.setupWithNavController(bottomNav, navController);
       }

   }
}

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

Рассмотрите альтернативы макетам с разделенным экраном

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

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

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

Названия пунктов назначения

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

<navigation ...>
    <fragment
        android:id="@+id/my_dest"
        android:name="com.example.MyFragment"
        android:label="@string/my_dest_label"
        tools:layout="@layout/my_fragment" />
    ...

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