使用 NavigationUI 将界面组件连接到 NavController

Navigation 组件包含 NavigationUI 类。此类包含多种静态方法,可帮助您使用顶部应用栏、抽屉式导航栏和底部导航栏来管理导航。

顶部应用栏

顶部应用栏在应用顶部提供了一个固定位置,用于显示当前屏幕的信息和操作。

显示顶部应用栏的屏幕
图 1. 显示顶部应用栏的屏幕。

利用 NavigationUI 包含的方法,您可以在用户浏览应用的过程中自动更新顶部应用栏中的内容。例如,NavigationUI 可使用导航图中的目的地标签及时更新顶部应用栏的标题。

<navigation>
    <fragment ...
              android:label="Page title">
      ...
    </fragment>
</navigation>

如果您按下面介绍的顶部应用栏实现方法使用 NavigationUI,您可以在标签中使用 {argName} 格式,从而根据提供给相应目的地的参数来自动填充您附加到目的地的标签。

NavigationUI 支持以下顶部应用栏类型:

如需详细了解应用栏,请参阅设置应用栏

AppBarConfiguration

NavigationUI 使用 AppBarConfiguration 对象管理在应用显示区域左上角的导航按钮的行为。导航按钮的行为会根据用户是否位于顶层目的地而变化。

顶层目的地是一组存在层次关系的目的地中的根级或最高级目的地。顶层目的地不会在顶部应用栏中显示“向上”按钮,因为不存在更高等级的目的地。默认情况下,应用的起始目的地是唯一的顶层目的地。

当用户位于顶层目的地时,如果目的地使用了 DrawerLayout,导航按钮会变为抽屉式导航栏图标 。如果目的地没有使用 DrawerLayout,导航按钮处于隐藏状态。当用户位于任何其他目的地时,导航按钮会显示为向上按钮 。在配置导航按钮时,如需将起始目的地用作唯一顶层目的地,请创建 AppBarConfiguration 对象并传入相应的导航图,如下所示:

Kotlin

val appBarConfiguration = AppBarConfiguration(navController.graph)

Java

AppBarConfiguration appBarConfiguration =
        new AppBarConfiguration.Builder(navController.getGraph()).build();

在某些情况下,您可能需要定义多个顶层目的地,而不是使用默认的起始目的地。这种情况的一种常见用例是 BottomNavigationView,在此场景中,同级屏幕可能彼此之间并不存在层次关系,并且可能各自有一组相关的目的地。对于这样的情况,您可以改为将一组目的地 ID 传递给构造函数,如下所示:

Kotlin

val appBarConfiguration = AppBarConfiguration(setOf(R.id.main, R.id.profile))

Java

AppBarConfiguration appBarConfiguration =
        new AppBarConfiguration.Builder(R.id.main, R.id.profile).build();

创建工具栏

如需使用 NavigationUI 创建工具栏,请先在主 activity 中定义该工具栏,如下所示:

<LinearLayout>
    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar" />
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment"
        ... />
    ...
</LinearLayout>

接下来,从主 activity 的 onCreate() 方法中调用 setupWithNavController(),如以下示例所示:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    setContentView(R.layout.activity_main)

    ...

    val navController = findNavController(R.id.nav_host_fragment)
    val appBarConfiguration = AppBarConfiguration(navController.graph)
    findViewById<Toolbar>(R.id.toolbar)
        .setupWithNavController(navController, appBarConfiguration)
}

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
    setContentView(R.layout.activity_main);

    ...

    NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
    AppBarConfiguration appBarConfiguration =
            new AppBarConfiguration.Builder(navController.getGraph()).build();
    Toolbar toolbar = findViewById(R.id.toolbar);
    NavigationUI.setupWithNavController(
            toolbar, navController, appBarConfiguration);
}

如需将导航按钮配置为在所有目的地都显示为向上按钮,那么在构建您的 AppBarConfiguration 时,请为顶层目的地传递一组空白目的地 ID。例如,如果您有第二个 activity,其应在所有目的地的 Toolbar 中显示向上按钮,这样做可能就很有用。这样一来,当返回堆栈上没有其他目的地时,用户便可导航回父 activity。您可以使用 setFallbackOnNavigateUpListener() 控制在 navigateUp() 不另行执行任何操作时的回退行为,如以下示例所示:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    ...

    val navHostFragment =
        supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
    val navController = navHostFragment.navController
    val appBarConfiguration = AppBarConfiguration(
        topLevelDestinationIds = setOf(),
        fallbackOnNavigateUpListener = ::onSupportNavigateUp
    )
    findViewById<Toolbar>(R.id.toolbar)
        .setupWithNavController(navController, appBarConfiguration)
}

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    NavHostFragment navHostFragment = (NavHostFragment) supportFragmentManager.findFragmentById(R.id.nav_host_fragment);
    NavController navController = navHostFragment.getNavController();
    AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder()
        .setFallbackOnNavigateUpListener(::onSupportNavigateUp)
        .build();
    Toolbar toolbar = findViewById(R.id.toolbar);
    NavigationUI.setupWithNavController(
            toolbar, navController, appBarConfiguration);
}

包含 CollapsingToolbarLayout

如需在工具栏中添加 CollapsingToolbarLayout,请先在 activity 中定义工具栏和周围布局,如下所示:

<LinearLayout>
    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="@dimen/tall_toolbar_height">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:contentScrim="?attr/colorPrimary"
            app:expandedTitleGravity="top"
            app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">

            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin"/>
        </com.google.android.material.appbar.CollapsingToolbarLayout>
    </com.google.android.material.appbar.AppBarLayout>

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment"
        ... />
    ...
</LinearLayout>

接着,通过主 activity 的 onCreate 方法调用 setupWithNavController(),如下所示:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    setContentView(R.layout.activity_main)

    ...

    val layout = findViewById<CollapsingToolbarLayout>(R.id.collapsing_toolbar_layout)
    val toolbar = findViewById<Toolbar>(R.id.toolbar)
    val navHostFragment =
        supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
    val navController = navHostFragment.navController
    val appBarConfiguration = AppBarConfiguration(navController.graph)
    layout.setupWithNavController(toolbar, navController, appBarConfiguration)
}

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
    setContentView(R.layout.activity_main);

    ...

    CollapsingToolbarLayout layout = findViewById(R.id.collapsing_toolbar_layout);
    Toolbar toolbar = findViewById(R.id.toolbar);
    NavHostFragment navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment);
    NavController navController = navHostFragment.getNavController();
    AppBarConfiguration appBarConfiguration =
            new AppBarConfiguration.Builder(navController.getGraph()).build();
    NavigationUI.setupWithNavController(layout, toolbar, navController, appBarConfiguration);
}

操作栏

如需向默认操作栏添加导航支持,请通过主 activity 的 onCreate() 方法调用 setupActionBarWithNavController(),如下所示。请注意,您需要在 onCreate() 之外声明 AppBarConfiguration,因为您在替换 onSupportNavigateUp() 时也使用该方法:

Kotlin

private lateinit var appBarConfiguration: AppBarConfiguration

...

override fun onCreate(savedInstanceState: Bundle?) {
    ...

    val navHostFragment =
        supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
    val navController = navHostFragment.navController
    appBarConfiguration = AppBarConfiguration(navController.graph)
    setupActionBarWithNavController(navController, appBarConfiguration)
}

Java

AppBarConfiguration appBarConfiguration;

...

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    NavHostFragment navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment);
    NavController navController = navHostFragment.getNavController();
    appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
    NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
}

接着,替换 onSupportNavigateUp() 以处理向上导航:

Kotlin

override fun onSupportNavigateUp(): Boolean {
    val navController = findNavController(R.id.nav_host_fragment)
    return navController.navigateUp(appBarConfiguration)
            || super.onSupportNavigateUp()
}

Java

@Override
public boolean onSupportNavigateUp() {
    NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
    return NavigationUI.navigateUp(navController, appBarConfiguration)
            || super.onSupportNavigateUp();
}

支持应用栏变体

如果对于应用中的每个目的地,应用栏的布局都类似,那么向 activity 添加顶部应用栏的效果很好。但是,如果顶部应用栏在不同目的地之间有很大变化,请考虑从 activity 中移除顶部应用栏,并改为在每个目的地 fragment 中进行定义。

例如,一个目的地可能使用标准 Toolbar,而另一个目的地则使用 AppBarLayout 创建带有标签页的更复杂的应用栏,如图 2 所示。

两个顶部应用栏变体;左侧为标准工具栏,右侧为带有工具栏和标签页的应用栏布局
图 2. 两个应用栏变体。左侧为标准 Toolbar。右侧为带有 Toolbar 和标签页的 AppBarLayout

如需使用 NavigationUI 在目的地 fragment 中实现此示例,首先请在每个 fragment 布局中定义应用栏,从使用标准工具栏的目的地 fragment 开始:

<LinearLayout>
    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        ... />
    ...
</LinearLayout>

接下来,定义使用带有标签页的应用栏的目的地 fragment:

<LinearLayout>
    <com.google.android.material.appbar.AppBarLayout
        ... />

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            ... />

        <com.google.android.material.tabs.TabLayout
            ... />

    </com.google.android.material.appbar.AppBarLayout>
    ...
</LinearLayout>

这两个 fragment 的导航配置逻辑相同,不过您应该在每个 fragment 的 onViewCreated() 方法中调用 setupWithNavController(),而不是通过 activity 对它们进行初始化:

Kotlin

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    val navController = findNavController()
    val appBarConfiguration = AppBarConfiguration(navController.graph)

    view.findViewById<Toolbar>(R.id.toolbar)
            .setupWithNavController(navController, appBarConfiguration)
}

Java

@Override
public void onViewCreated(@NonNull View view,
                          @Nullable Bundle savedInstanceState) {
    NavController navController = Navigation.findNavController(view);
    AppBarConfiguration appBarConfiguration =
            new AppBarConfiguration.Builder(navController.getGraph()).build();
    Toolbar toolbar = view.findViewById(R.id.toolbar);

    NavigationUI.setupWithNavController(
            toolbar, navController, appBarConfiguration);
}

将目的地关联到菜单项

NavigationUI 还提供帮助程序,用于将目的地绑定到基于菜单的界面组件。NavigationUI 包含一个帮助程序方法 onNavDestinationSelected(),该方法使用 MenuItem 以及托管关联目的地的 NavController。如果 MenuItemid 与目的地的 id 匹配,NavController 可以导航至该目的地。

例如,下面的 XML 代码段使用常见的 iddetails_page_fragment 定义菜单项及目的地:

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    ... >

    ...

    <fragment android:id="@+id/details_page_fragment"
         android:label="@string/details"
         android:name="com.example.android.myapp.DetailsFragment" />
</navigation>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    ...

    <item
        android:id="@+id/details_page_fragment"
        android:icon="@drawable/ic_details"
        android:title="@string/details" />
</menu>

例如,如果通过 activity 的 onCreateOptionsMenu() 添加菜单,则可以通过替换 activity 的 onOptionsItemSelected() 以调用 onNavDestinationSelected(),从而将菜单项与目的地相关联,如以下示例所示:

Kotlin

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    val navController = findNavController(R.id.nav_host_fragment)
    return item.onNavDestinationSelected(navController) || super.onOptionsItemSelected(item)
}

Java

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
    return NavigationUI.onNavDestinationSelected(item, navController)
            || super.onOptionsItemSelected(item);
}

现在,当用户点击 details_page_fragment 菜单项时,应用会自动使用相同的 id 导航到相应目的地。

添加抽屉式导航栏

抽屉式导航栏是显示应用主导航菜单的界面面板。当用户触摸应用栏中的抽屉式导航栏图标 或用户从屏幕的左边缘滑动手指时,就会显示抽屉式导航栏。

显示导航菜单的打开的抽屉式导航栏
图 3. 打开的抽屉式导航栏,其中显示了导航菜单。

抽屉式导航栏图标会显示在使用 DrawerLayout 的所有顶层目的地上。

如需添加抽屉式导航栏,请先声明 DrawerLayout 为根视图。在 DrawerLayout 内,为主界面内容以及包含抽屉式导航栏内容的其他视图添加布局。

例如,以下布局使用含有两个子视图的 DrawerLayout:包含主内容的 NavHostFragment 和适用于抽屉式导航栏内容的 NavigationView

<?xml version="1.0" encoding="utf-8"?>
<!-- Use DrawerLayout as root container for activity -->
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <!-- Layout to contain contents of main body of screen (drawer will slide over this) -->
    <androidx.fragment.app.FragmentContainerView
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:id="@+id/nav_host_fragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph" />

    <!-- Container for contents of drawer - use NavigationView to make configuration easier -->
    <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"
        android:fitsSystemWindows="true" />

</androidx.drawerlayout.widget.DrawerLayout>

接下来,将 DrawerLayout 传递给 AppBarConfiguration,以将其连接到导航图,如以下示例所示:

Kotlin

val appBarConfiguration = AppBarConfiguration(navController.graph, drawerLayout)

Java

AppBarConfiguration appBarConfiguration =
        new AppBarConfiguration.Builder(navController.getGraph())
            .setDrawerLayout(drawerLayout)
            .build();

接着,在您的主 activity 类中,通过主 activity 的 onCreate() 方法调用 setupWithNavController(),如下所示:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    setContentView(R.layout.activity_main)

    ...

    val navHostFragment =
        supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
    val navController = navHostFragment.navController
    findViewById<NavigationView>(R.id.nav_view)
        .setupWithNavController(navController)
}

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
    setContentView(R.layout.activity_main);

    ...

    NavHostFragment navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment);
    NavController navController = navHostFragment.getNavController();
    NavigationView navView = findViewById(R.id.nav_view);
    NavigationUI.setupWithNavController(navView, navController);
}

Navigation 2.4.0-alpha01 开始,系统会在您使用 setupWithNavController 时保存并恢复每个菜单项的状态。

底部导航栏

NavigationUI 也可以处理底部导航。当用户选择某个菜单项时,NavController 会调用 onNavDestinationSelected() 并自动更新底部导航栏中的所选项目。

底部导航栏
图 4. 底部导航栏。

如需在应用中创建底部导航栏,请先在主 activity 中定义底部导航栏,如下所示:

<LinearLayout>
    ...
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment"
        ... />
    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_nav"
        app:menu="@menu/menu_bottom_nav" />
</LinearLayout>

接着,在您的主 activity 类中,通过主 activity 的 onCreate() 方法调用 setupWithNavController(),如下所示:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    setContentView(R.layout.activity_main)

    ...

    val navHostFragment =
        supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
    val navController = navHostFragment.navController
    findViewById<BottomNavigationView>(R.id.bottom_nav)
        .setupWithNavController(navController)
}

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
    setContentView(R.layout.activity_main);

    ...

    NavHostFragment navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment);
    NavController navController = navHostFragment.getNavController();
    BottomNavigationView bottomNav = findViewById(R.id.bottom_nav);
    NavigationUI.setupWithNavController(bottomNav, navController);
}

Navigation 2.4.0-alpha01 开始,系统会在您使用 setupWithNavController 时保存并恢复每个菜单项的状态。

有关包含底部导航栏的综合示例,请参阅 GitHub 上的 Android 架构组件高级导航示例

监听导航事件

NavController 进行交互是在不同目的地之间导航的主要方法。NavController 负责将 NavHost 的内容替换为新目的地。在大多数情况下,界面元素(如顶部应用栏或 BottomNavigationBar 等其他持续性导航控件)位于 NavHost 之外,并且随您在各个目的地之间导航进行更新。

NavController 提供 OnDestinationChangedListener 接口,该接口在 NavController当前目的地或其参数发生更改时调用。可以通过 addOnDestinationChangedListener() 方法注册新监听器。请注意,调用 addOnDestinationChangedListener() 时,如果当前目的地存在,则会立即被发送到您的监听器。

NavigationUI 使用 OnDestinationChangedListener 让这些常见界面组件具备导航感知功能。不过请注意,您也可以单独使用 OnDestinationChangedListener,使任何自定义界面或业务逻辑感知导航事件。

举例来说,您可能会在应用的一些区域显示常见界面元素,而在另外一些区域隐藏这些元素。使用您自己的 OnDestinationChangedListener,您可以根据目标目的地选择性地显示或隐藏这些界面元素,如下例所示:

Kotlin

navController.addOnDestinationChangedListener { _, destination, _ ->
   if(destination.id == R.id.full_screen_destination) {
       toolbar.visibility = View.GONE
       bottomNavigationView.visibility = View.GONE
   } else {
       toolbar.visibility = View.VISIBLE
       bottomNavigationView.visibility = View.VISIBLE
   }
}

Java

navController.addOnDestinationChangedListener(new NavController.OnDestinationChangedListener() {
   @Override
   public void onDestinationChanged(@NonNull NavController controller,
           @NonNull NavDestination destination, @Nullable Bundle arguments) {
       if(destination.getId() == R.id.full_screen_destination) {
           toolbar.setVisibility(View.GONE);
           bottomNavigationView.setVisibility(View.GONE);
       } else {
           toolbar.setVisibility(View.VISIBLE);
           bottomNavigationView.setVisibility(View.VISIBLE);
       }
   }
});

基于参数的监听器

作为替代方案,您还可以将参数与导航图中的默认值结合使用,相应的界面控制器可以使用这些值来更新其状态。例如,我们可以在 NavGraph 中创建参数,而不是根据上一个示例,使 OnDestinationChangedListener 中的逻辑基于目的地 ID:

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/navigation\_graph"
    app:startDestination="@id/fragmentOne">
    <fragment
        android:id="@+id/fragmentOne"
        android:name="com.example.android.navigation.FragmentOne"
        android:label="FragmentOne">
        <action
            android:id="@+id/action\_fragmentOne\_to\_fragmentTwo"
            app:destination="@id/fragmentTwo" />
    </fragment>
    <fragment
        android:id="@+id/fragmentTwo"
        android:name="com.example.android.navigation.FragmentTwo"
        android:label="FragmentTwo">
        <argument
            android:name="ShowAppBar"
            android:defaultValue="true" />
    </fragment>
</navigation>

此参数在导航到目的地时不使用,而是用作使用 defaultValue 向目的地附加其他信息的一种方法。在本例中,该值指示在此目的地上是否应该显示应用栏。

现在,我们可以在 Activity 中添加 OnDestinationChangedListener

Kotlin

navController.addOnDestinationChangedListener { _, _, arguments ->
    appBar.isVisible = arguments?.getBoolean("ShowAppBar", false) == true
}

Java

navController.addOnDestinationChangedListener(
        new NavController.OnDestinationChangedListener() {
            @Override
            public void onDestinationChanged(
                    @NonNull NavController controller,
                    @NonNull NavDestination destination,
                    @Nullable Bundle arguments
            ) {
                boolean showAppBar = false;
                if (arguments != null) {
                    showAppBar = arguments.getBoolean("ShowAppBar", false);
                }
                if(showAppBar) {
                    appBar.setVisibility(View.VISIBLE);
                } else {
                    appBar.setVisibility(View.GONE);
                }
            }
        }
);

每当导航目的地发生更改时,NavController 就会调用此回调。Activity 现在可以根据回调中收到的参数,更新其拥有的界面组件的状态或可见性。

此方法的一个优点在于 Activity 只能查看导航图中的参数,而不知道各个 Fragment 角色和职责。同样,各个 fragment 也不知道包含的 Activity 及其拥有的界面组件。

其他资源

如需详细了解 Navigation,请参阅下面列出的其他资源:

示例

Codelab

博文

视频