处理配置更改

响应式 UI 和导航

为了为您的用户提供最佳的导航体验,您应该提供一个根据用户设备的宽度、高度和最小宽度定制的导航 UI。您可能希望使用底部应用栏、始终显示或可折叠的导航抽屉轨道,或者根据可用屏幕空间和您应用的独特风格创建一些全新的内容。

examples of a rail, navigation drawers, and a bottom app bar
图 1. 轨道、导航抽屉和底部应用栏的示例。

Material Design 的产品架构指南提供了构建响应式 UI(即动态适应环境变化的 UI)的其他背景和注意事项。一些环境变化的示例包括对宽度、高度、方向和用户语言首选项的调整。这些环境属性统称为设备的配置

当这些属性中的一个或多个在运行时发生更改时,Android 操作系统会通过销毁然后重新创建您的应用的活动和片段来响应。因此,支持 Android 上响应式 UI 的最佳方法是确保您在适当的情况下使用资源配置限定符避免使用硬编码布局大小

在响应式 UI 中实现全局导航

将全局导航作为响应式 UI 的一部分实现始于托管导航图的活动。有关动手示例,请查看导航代码实验室。该代码实验室使用NavigationView显示导航菜单,如图 2 所示。在宽度至少为 960dp 的设备上运行时,此NavigationView始终显示在屏幕上。

the navigation codelab uses a navigation view that is always visible
            when device width is at least 960dp
图 2. 导航代码实验室使用NavigationView显示导航菜单。

其他设备尺寸和方向会根据需要动态切换DrawerLayoutBottomNavigationView

a bottomnavigationview and a drawerlayout, used for the navigation
            menu as needed in smaller device layouts
图 3. 导航代码实验室使用BottomNavigationViewDrawerLayout在较小的设备上显示导航菜单。

您可以通过创建三个不同的布局来实现此行为,其中每个布局根据当前设备配置定义所需的导航元素和视图层次结构。

每个布局适用的配置由放置布局文件的目录结构确定。例如,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)。

在处理导航事件时,您只需要连接对应于当前存在的窗口小部件的事件,如下例所示。

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 会销毁先前配置中的 Activity 及其关联的视图。然后,它会使用针对新配置设计的资源重新创建 Activity。Activity 被销毁和重新创建后,会在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" />
    ...

使用资源值,您的目标会在配置更改时自动应用最合适的资源。