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

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

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

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

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

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

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

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

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

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

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

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

Конфигурация, к которой применяется каждый макет, определяется структурой каталогов, в которой находится файл макета. Например, файл макета 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 (или доступная ширина >= 960dp) имеет приоритет над -h470dp (или доступная высота >= 470). Если конфигурация устройства не соответствует ни одному из этих условий, используется ресурс макета по умолчанию ( res/layout/navigation_activity.xml ).

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

Котлин

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)

      ...
    }

    ...
}

Ява

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. Это значительно повышает акцент на отзывчивости каждого экрана вашего приложения, а не на изменении структуры навигации в зависимости от размера экрана.

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

Названия мест назначения

Если вы указываете имена мест назначения в своем графике с помощью атрибута 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" />
    ...

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

,

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

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

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

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

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

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

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

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

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

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

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

Конфигурация, к которой применяется каждый макет, определяется структурой каталогов, в которой находится файл макета. Например, файл макета 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 (или доступная ширина >= 960dp) имеет приоритет над -h470dp (или доступная высота >= 470). Если конфигурация устройства не соответствует ни одному из этих условий, используется ресурс макета по умолчанию ( res/layout/navigation_activity.xml ).

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

Котлин

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)

      ...
    }

    ...
}

Ява

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. Это значительно повышает акцент на отзывчивости каждого экрана вашего приложения, а не на изменении структуры навигации в зависимости от размера экрана.

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

Названия мест назначения

Если вы указываете имена мест назначения в своем графике с помощью атрибута 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" />
    ...

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