使用 NavigationUI 將 UI 元件連結至 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 建立工具列,請先在主要活動中定義該工具列,如下所示:

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

將導覽按鈕設為以向上導覽按鈕顯示, 到達網頁時,請傳遞一組空白的目的地 ID 給頂層 是建立 AppBarConfiguration 目的地的目的地。非常有用 例如,您有第二個活動應顯示「向上」按鈕 在所有目的地的 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);
}

動作列

如要在預設動作列中新增導覽支援功能,請呼叫 setupActionBarWithNavController()敬上 來自主要活動的 onCreate() 方法,如下所示。請注意,您必須在 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 所示

兩個頂端應用程式列變化版本,左側是標準工具列,右側是包含工具列和分頁的 appbarlayout
圖 2. 兩個應用程式列變化版本。左側是 Toolbar,右側是包含 AppBarLayoutToolbar 和分頁。

如要在目的地片段中實作這個範例,請使用 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>

這兩個片段的導覽設定邏輯都相同, 但你應該直接呼叫 setupWithNavController()敬上 透過每個片段的 onViewCreated() 方法 (而非初始化) 這些方法:

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。如果 MenuItemidid 的 目的地,NavController 就可以前往該目的地。

比方說,下方的 XML 程式碼片段透過 details_page_fragment 這個常用 id 定義了選單項目和目的地:

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

舉例來說,如果選單是透過活動的 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 面板。當使用者輕觸導覽匣圖示時,畫面上就會顯示導覽匣 或是在使用者從裝置列左側滑動手指時 。

開啟的導覽匣顯示導覽選單
圖 3. 開啟的導覽匣顯示導覽選單。

所有使用 DrawerLayout頂層目的地中都會顯示導覽匣圖示。

如要新增導覽匣,請先將 DrawerLayout 宣告為根檢視。在 DrawerLayout 中,新增主要 UI 內容的版面配置,以及另一個包含導覽匣內容的檢視。

舉例來說,以下版面配置使用包含兩個子檢視的 DrawerLayoutNavHostFragment 包含主要內容,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();

接著在主要活動類別中,透過主要活動的 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. 底部導覽列。

如要在應用程式中建立底部導覽列,請先在主要的 活動,如下所示:

<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 時還原狀態。

如需包含底部導覽的完整範例,請參閱 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);
       }
   }
});

以引數為基礎的事件監聽器

您也可以在導覽圖中使用含預設值的引數,讓適當的 UI 控制器用來更新其狀態。舉例來說,我們可以不要依照前述範例,以目的地 ID 做為 OnDestinationChangedListener 中的邏輯基礎,而改為在 NavGraph 中建立引數:

<?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 現可根據回呼中收到的引數,更新其所擁有 UI 元件的狀態或顯示設定。

這個做法的一個優點,在於 Activity 只會查看導覽圖中的引數,且並不知道個別 Fragment 的角色和責任。同樣地,個別片段不知道 瞭解其擁有的 Activity 及其擁有的 UI 元件。

其他資源

如要進一步瞭解導覽,請參閱下列其他資源。

範例

程式碼研究室

網誌文章

影片