使用 NavigationUI 将 UI 组件连接到 NavController

导航组件包含一个 NavigationUI 类。此类包含管理与应用顶部应用栏、导航抽屉和底部导航的导航的静态方法。

顶部应用栏

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

screen displaying a top app bar
图 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创建工具栏,首先在您的主活动中定义该栏,如下所示

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

接下来,从主活动的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。例如,如果您有第二个活动,它应该在所有目的地上的Toolbar中显示向上按钮,这将很有用。这允许用户在后退栈上没有其他目的地时导航回父活动。您可以使用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与您的工具栏一起包含,首先在您的活动中定义工具栏和周围的布局,如下所示

<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>

接下来,从主活动的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);
}

操作栏

要向默认操作栏添加导航支持,请从主活动的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();
}

支持应用栏变体

当应用栏的布局在应用程序中的每个目的地都类似时,将顶部应用栏添加到您的活动中效果很好。但是,如果您的顶部应用栏在各个目的地之间发生了重大变化,则考虑从您的活动中删除顶部应用栏,而是在每个目的地片段中定义它。

例如,您的其中一个目的地可能使用标准Toolbar,而另一个使用AppBarLayout来创建具有选项卡的更复杂的应用栏,如图 2 所示。

two top app bar variations; a standard toolbar on the left, and an
            appbarlayout with a toolbar and tabs on the right
图 2. 两个应用栏变体。左侧是标准Toolbar。右侧是带Toolbar和选项卡的AppBarLayout

要使用NavigationUI在您的目的地片段中实现此示例,首先在每个片段布局中定义应用栏,从使用标准工具栏的目的地片段开始

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

接下来,定义使用带选项卡的应用栏的目的地片段

<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>

这两个片段的导航配置逻辑相同,只是您应该从每个片段的onViewCreated()方法中调用setupWithNavController(),而不是从活动中初始化它们

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还提供帮助程序,用于将目的地绑定到菜单驱动的 UI 组件。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的相应目的地。

添加导航抽屉

导航抽屉是一个 UI 面板,显示应用程序的主导航菜单。当用户点击应用栏中的抽屉图标或用户从屏幕左侧边缘滑动手指时,抽屉会出现。

an open drawer displaying a navigation menu
图 3. 显示导航菜单的打开的抽屉。

抽屉图标显示在所有使用DrawerLayout顶级目的地上。

要添加导航抽屉,首先将DrawerLayout声明为根视图。在DrawerLayout内部,添加主 UI 内容的布局以及包含导航抽屉内容的另一个视图。

例如,以下布局使用一个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>

接下来,通过将其传递给AppBarConfigurationDrawerLayout连接到您的导航图,如下例所示

Kotlin

val appBarConfiguration = AppBarConfiguration(navController.graph, drawerLayout)

Java

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

接下来,在您的主活动类中,从主活动的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()并自动更新底部导航栏中的所选项。

bottom navigation bar
图 4. 底部导航栏。

要在您的应用程序中创建底部导航栏,首先在您的主活动中定义该栏,如下所示

<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>

接下来,在您的主活动类中,从主活动的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的内容。在许多情况下,UI 元素(例如顶部应用栏或其他持久导航控件,如BottomNavigationBar)位于NavHost之外,并且需要在您在目的地之间导航时进行更新。

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

NavigationUI 使用 OnDestinationChangedListener 使这些常见的 UI 组件能够感知导航。但是请注意,您也可以单独使用 OnDestinationChangedListener 使任何自定义 UI 或业务逻辑感知导航事件。

例如,您可能有一些常见的 UI 元素,您希望在应用程序的某些区域显示它们,而在其他区域隐藏它们。使用您自己的 OnDestinationChangedListener,您可以根据目标目的地有选择地显示或隐藏这些 UI 元素,如下例所示

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);
       }
   }
});

基于参数的监听器

或者,您也可以在导航图中使用带有默认值的 arguments,相应的 UI 控制器可以使用这些 arguments 更新其状态。例如,与其像前面的示例那样基于 OnDestinationChangedListener 中的目的地 ID 来编写逻辑,不如在 NavGraph 中创建一个 argument。

<?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>

导航到目的地时不会使用此 argument,而是作为一种通过使用 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 可以根据在回调中接收到的 arguments 更新其拥有的 UI 组件的状态或可见性。

这种方法的一个优点是,Activity 只会看到导航图中的 arguments,并且不知道各个 Fragment 的角色和职责。类似地,各个 Fragment 也不知道包含它们的 Activity 及其拥有的 UI 组件。

其他资源

要了解有关导航的更多信息,请参阅以下其他资源。

示例

Codelabs

博文

视频