Navigation 元件含有 NavigationUI
類別。這個類別包含多種靜態方法,用於管理頂端應用程式列、導覽匣和底部導覽的導覽行為。
頂端應用程式列
頂端應用程式列會在應用程式頂端顯示一個一致的區域,用以顯示目前畫面上的資訊和動作。
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 所示。
如要使用 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
。如果 MenuItem
的 id
與目的地的 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 面板。如果使用者輕觸應用程式列中的導覽匣圖示 ,或是使用手指從螢幕左側邊緣滑動,畫面上就會顯示導覽匣。
所有使用 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>
接著將 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()
,並自動更新底部導覽列中的所選項目。
如要在應用程式中建立底部導覽列,請先在主要活動中定義該導覽列,如下所示:
<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 元件。
其他資源
如要進一步瞭解導覽,請參閱下列其他資源。
範例
程式碼研究室
網誌文章
影片
- 移至單一活動的 10 種最佳做法
- 單一活動:原因、時機和方式 (2018 年 Android 開發人員高峰會)
- Android Jetpack:使用導覽控制器管理 UI 導覽 (2018 年 Google I/O 大會)