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

如要針對所有目的地將導覽按鈕設為以向上導覽按鈕的形式顯示,請在建構 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 所示。

兩個頂端應用程式列變化版本,左側是標準工具列,右側是包含工具列和分頁的 appbarlayout
圖 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 程式碼片段透過 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() 新增,您可以覆寫活動的 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 時還原狀態。

監聽導覽事件

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 元件。

其他資源

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

範例

程式碼研究室

網誌文章

影片