处理配置更改

响应式 UI 和导航

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

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

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

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

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

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

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

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

a bottomnavigationview and a drawerlayout, used for the navigation
            menu as needed in smaller device layouts
图 3. 导航 Codelab 使用 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 会销毁先前配置中的活动及其关联的视图。然后,它会使用为新配置设计的资源重新创建活动。活动被销毁并重新创建后,会自动在 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" />
    ...

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