处理配置变更

自适应界面和导航

为了尽可能向用户提供最佳导航体验,您应提供针对用户设备的宽度、高度和最小宽度量身定制的导航界面。根据可用的屏幕空间和应用的独特样式,您可能希望使用底部应用栏、始终呈现或可收起的抽屉式导航栏侧边栏,又或许是一些全新的元素。

侧边栏、抽屉式导航栏和底部应用栏的示例
图 1. 侧边栏、抽屉式导航栏和底部应用栏的示例。

Material Design 产品架构指南提供了有关构建自适应界面(即,动态适应环境变化的界面)的其他上下文和注意事项。例如,对宽度、高度、屏幕方向和用户语言偏好设置的调整就属于环境变化。这些环境属性统称为设备的配置。

当其中一个或多个属性在运行时发生更改时,Android OS 会通过先销毁再重新创建应用的 activity 和 fragment 做出响应。因此,为了在 Android 上支持自适应界面,最佳实践是在适当的情况下使用资源配置限定符,并避免使用硬编码的布局尺寸

在自适应界面中实现全局导航

为了将全局导航作为自适应界面的一部分实现,应从托管导航图的 activity 着手。如需查看实际操作示例,请参阅 Navigation Codelab。该 Codelab 使用 NavigationView 显示导航菜单,如图 2 所示。在呈现宽度至少为 960dp 的设备上运行时,此 NavigationView 始终在屏幕上。

Navigation Codelab 使用了一个导航视图,当设备宽度至少为 960dp 时,该视图始终可见
图 2. Navigation Codelab 使用 NavigationView 显示导航菜单。

如果采用其他设备尺寸和屏幕方向,系统会根据需要在 DrawerLayoutBottomNavigationView 之间动态切换。

bottomnavigationview 和 drawerlayout,用于在较小的设备布局中根据需要显示导航菜单
图 3. Navigation 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(即可用高度 >= 470dp)。如果设备配置不符合其中任一条件,则会使用默认布局资源 (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" />
    ...

借助资源值,每当配置发生更改时,目的地都会自动应用最合适的资源。