深入淺出 NavigationUI | MAD Skills

語言: CN / TW / HK

這是第二個關於導航 (Navigation) 的 MAD Skills 系列,如果您想回顧過去發佈的內容,請參考下面鏈接查看:

今天為大家發佈本系列文章中的第一篇。在本文中,我們將為大家講解另外一個用例,即類似操作欄 (Action Bar)、底部標籤欄或者抽屜型導航欄之類的 UI 組件如何在應用中實現導航功能。如果您更傾向於觀看視頻而非閲讀文章,請查看以下視頻內容:

請查看 視頻 內容。

△ 深入瞭解 NavigationUI|video

△ 深入瞭解 NavigationUI

概述

在之前的 導航系列文章中,Chet 開發了一個用於 跟蹤甜甜圈的應用。知道什麼是甜甜圈的最佳搭檔嗎?(難道是另一個甜甜圈?) 當然是咖啡!所以我準備增加一個追蹤咖啡的功能。我需要在應用中增加一些頁面,所以有必要使用抽屜式導航欄或者底部標籤欄來輔助用户導航。但是我們該如何使用這些 UI 組件來集成導航功能呢?通過點擊監聽器手動觸發導航動作嗎?

不需要!無需任何監聽器。NavigationUI 類通過匹配目標頁面 id 與菜單 id 實現不同頁面之間的導航功能。讓我們深入探索一下它的內部機制吧。

添加咖啡追蹤器

△ 工程結構

△ 工程結構

首先我將與甜甜圈相關的類文件拷貝了一份到新的包下,並且將它們重命名。這樣的操作對於真正的應用來説也許不是最好的做法,但是在這裏可以快速幫助我們添加咖啡跟蹤功能到已有的應用中。如果您希望隨着文章內容同步操作,可以獲取 這裏的代碼,裏面包含了全部針對 Donut Tracker 應用的修改,可以基於該代碼瞭解 NavigationUI。

基於上面所做的修改,我更新了導航圖,新增了從 coffeeFragment 到 coffeeDialogFragment 以及從 selectionFragment 到 donutFragment 相關的目的頁面和操作。之後我會用到這些目的頁面的 id ;)

△ 帶有新的目的頁面的導航圖

△ 帶有新的目的頁面的導航圖

更新導航圖之後,我們可以開始將元素綁定起來,並且實現導航到 SelectionFragment。

選項菜單

應用的選項菜單現在尚未發揮作用。要啟用它,需要在 onOptionsItemSelected() 函數中,為被選擇的菜單項調用 onNavDestinationSelected() 函數,並傳入 navController。只要目的頁面的 idMenuItem 的 id 相匹配,該函數會導航到綁定在 MenuItem 上的目的頁面。

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

現在導航控制器可以 "支配" 菜單項了,我將 MenuItemid 與之前所創建的目的頁面的 id 進行了匹配。這樣,導航組件就可以將 MenuItem 與目的頁面進行關聯。

<menu 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"
   tools:context="com.android.samples.donuttracker.MainActivity">
   <item
       android:id="@+id/selectionFragment"
       android:orderInCategory="100"
       android:title="@string/action_settings"
       app:showAsAction="never" />
</menu>

Toolbar

現在應用可以導航到 selectionFragment,但是標題仍然保持原樣。當處於 selectionFragment 的時候,我們希望標題可以被更新並且顯示返回按鈕。

首先我需要添加一個 AppBarConfiguration 對象,NavigationUI 會使用該對象來管理應用左上角的導航按鈕的行為。

appBarConfiguration = AppBarConfiguration(navController.graph)

該按鈕會根據您的目的頁面的層級改變自身的行為。比如,當您在最頂層的目的頁面時,就不會顯示回退按鈕,因為沒有更高層級的頁面。

默認情況下,您應用的最初頁面是唯一的最頂層目的頁面,但是您也可以定義多個最頂層目的頁面。比如,在我們的應用中,我可以將 donutList coffeeList 的目的頁面都定義為最頂層的目的頁面。

接下來,在 MainActivity 類中,獲得 navControllertoolbar 的實例,並且驗證 setSupportActionBar() 是否被調用。這裏我還更新了傳入函數的 toolbar 的引用。

val navHostFragment = supportFragmentManager.findFragmentById(
   R.id.nav_host_fragment
) as NavHostFragment
navController = navHostFragment.navController
val toolbar = binding.toolbar

要在默認的操作欄 (Action Bar) 中添加導航功能,我在這裏使用了 setupActionBarWithNavController() 函數。該函數需要兩個參數: navControllerappBarConfiguration

setSupportActionBar(toolbar)
setupActionBarWithNavController(navController, appBarConfiguration)

接下來,根據目前的目的頁面,我覆寫了 onSupportNavigationUp() 函數,然後在 nav_host_fragment 上調用 navigateUp() 並傳入 appBarConfiguration 來支持回退導航或者顯示菜單圖標的功能。

override fun onSupportNavigateUp(): Boolean {
   return findNavController(R.id.nav_host_fragment).navigateUp(
       appBarConfiguration
   )
}

現在我可以導航到 selectionFragment,並且您可以看到標題已經更新,並且也顯示了返回按鈕,用户可以返回到之前的頁面。

△ 標題更新了並且也顯示了返回按鈕

△ 標題更新了並且也顯示了返回按鈕

底部標籤欄

目前為止還算順利,但是應用還不能導航到 coffeeList Fragment。接下來我們將解決這個問題。

我們從添加底部標籤欄入手。首先添加 bottom_nav_menu.xml 文件並且聲明兩個菜單元素。NavigationUI 依賴 MenuItemid,用它與導航圖中目的頁面的 id 進行匹配。我還為每個目的頁面設置了圖標和標題。

<menu xmlns:android="http://schemas.android.com/apk/res/android">
   <item
       android:id="@id/donutList"
       android:icon="@drawable/donut_with_sprinkles"
       android:title="@string/donut_name" />
   <item
       android:id="@id/coffeeList"
       android:icon="@drawable/coffee_cup"
       android:title="@string/coffee_name" />
</menu>

現在 MenuItem 已經就緒,我在 mainActivity 的佈局中添加了 BottomNavigationView,並且將 bottom_nav_menu 設置為 BottomNavigationViewmenu 屬性。

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

要使底部標籤欄發揮作用,這裏調用 setupWithNavController() 函數將 navController 傳入 BottomNavigationView

private fun setupBottomNavMenu(navController: NavController) {
   val bottomNav = findViewById<BottomNavigationView>(
       R.id.bottom_nav_view
   )
   bottomNav?.setupWithNavController(navController)
}

請注意我並沒有從導航圖中調用任何導航操作。實際上導航圖中甚至沒有前往 coffeeList Fragment 的路徑。和之前對 ActionBar 所做的操作一樣,BottomNavigationView 通過匹配 MenuItemid 和導航目的頁面的 id 來自動響應導航操作。

抽屜式導航欄

雖然看上去不錯,但是如果您設備的屏幕尺寸較大,那麼底部標籤欄恐怕無法提供最佳的用户體驗。要解決這個問題,我會使用另外一個佈局文件,它帶有 w960dp 限定符,表明它適用於屏幕更大、更寬的設備。

這個佈局文件與默認的 activity_main 佈局相類似,其中已經包含了 ToolbarFragmentContainerView。我需要添加 NavigationView,並且將 nav_drawer_menu 設置為 NavigationViewmenu 屬性。接下來,我將在 NavigationViewFragmentContainerView 之間添加分隔符。

<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.android.samples.donuttracker.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: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:background="@color/colorPrimary"
       android:layout_toEndOf="@id/nav_view"
       android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" />
   <androidx.fragment.app.FragmentContainerView
       android:id="@+id/nav_host_fragment"
       android:name="androidx.navigation.fragment.NavHostFragment"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:layout_below="@id/toolbar"
       app:defaultNavHost="true"
       android:layout_toEndOf="@id/nav_view"
       app:navGraph="@navigation/nav_graph" />
</RelativeLayout>

如此一來,在寬屏幕設備上,NavigationView 會代替 BottomNavigationView 顯示在屏幕上。現在佈局文件已經就緒,我再創建一個 nav_drawer_menu.xml,並且將 donutListcoffeeList 作為主要的分組添加為目的頁面。對於 MenuItem,我添加了 selectionFragment 作為它的目的頁面。

<menu xmlns:android="http://schemas.android.com/apk/res/android">
   <group android:id="@+id/primary">
       <item
           android:id="@id/donutList"
           android:icon="@drawable/donut_with_sprinkles"
           android:title="@string/donut_name" />
       <item
           android:id="@id/coffeeList"
           android:icon="@drawable/coffee_cup"
           android:title="@string/coffee_name" />
   </group>
   <item
       android:id="@+id/selectionFragment"
       android:title="@string/action_settings" />
</menu>

現在所有佈局已經就緒,我們回到 MainActivity,設置抽屜式導航欄,使其能夠與 NavigationController 協作。和之前針對 BottomNavigationView 所做的相類似,這裏創建一個新的方法,並且調用 setupWithNavController() 函數將 navController 傳入 NavigationView。為了使代碼保持整潔、各個元素之間更加清晰,我們會在新的方法中實現相關操作,並且在 onCreate() 中調用該方法。

private fun setupNavigationMenu(navController: NavController){
   val sideNavView = findViewById<NavigationView>(R.id.nav_view)
   sideNavView?.setupWithNavController(navController)
}

現在當我在屏幕較寬的設備上運行應用時,可以看到抽屜式導航欄已經設置了 MenuItem,並且在導航圖中,MenuItem 和目的頁面的 id 是相匹配的。

△ 在屏幕較寬的設備上運行 Donut Tracker

△ 在屏幕較寬的設備上運行 Donut Tracker

請注意,當我切換頁面的時候返回按鈕會自動顯示在左上角。如果您想這麼做,還可以修改 AppBarConfiguration 來將 CoffeeList 添加為最頂層的目的頁面。

小結

本次分享的內容就是這些了。Donut Tracker 應用並不需要底部標籤欄或者抽屜式導航欄,但是添加了新的功能和目的頁面後,NavigationUI 可以很大程度上幫助我們處理應用中的導航功能。

我們無需進行多餘的操作,僅需添加 UI 組件,並且匹配 MenuItem 的 id 和目的頁面的 id。您可以查閲 完整代碼,並且通過 main 與 starter 分支的 比較,觀察代碼的變化。