Navigation組件的出現(xiàn),避免了我們自己去管理Fragment的遷移棧
1、在Gradle中配置Navigation組件依賴
參考:https://developer.android.com/jetpack/androidx/releases/navigation#declaring_dependencies
dependencies {
...
...
// For Kotlin use navigation-fragment-ktx
implementation "android.arch.navigation:navigation-fragment-ktx:1.0.0"
// For Kotlin use navigation-ui-ktx
implementation "android.arch.navigation:navigation-ui-ktx:1.0.0"
}
2、創(chuàng)建導(dǎo)航圖(Navigation Graph)
在項(xiàng)目窗口,右鍵點(diǎn)擊項(xiàng)目目錄,選擇 New > Android Resource File:

這時(shí)會(huì)彈出添加新資源的對(duì)話框,輸入文件名,例如“nav_graph”,選擇 Resource Type > Navigation:

點(diǎn)擊OK,導(dǎo)航圖文件就創(chuàng)建好了:


3、在activity_main.xml中添加NavHost
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph"/>
</android.support.constraint.ConstraintLayout>
-
android:name
配置 NavHost 的實(shí)現(xiàn)類,使用默認(rèn)配置的即可。 -
app:defaultNavHost="true"
確保 NavHostFragment 能攔截系統(tǒng)返回按鈕。
點(diǎn)擊返回鍵時(shí),頁(yè)面會(huì)按進(jìn)入棧的順序退出棧。
如果在同一個(gè)布局中配置了多個(gè) NavHost 實(shí)現(xiàn),只能有一個(gè) NavHost 實(shí)現(xiàn)可以把 defaultNavHost 設(shè)置為 true。 -
app:navGraph
該屬性將 NavHostFragment 與導(dǎo)航圖關(guān)聯(lián)起來(lái)。
添加完之后,在res / navigation / nav_graph.xml 中,HOST 下會(huì)顯示已添加的 NavHost :

4、在導(dǎo)航圖(Navigation Graph)中添加目標(biāo)Fragment(Destinations)
有3中創(chuàng)建 Destinations 的方式:

-
方式1:Create new destination
該方式會(huì)創(chuàng)建一個(gè)新的Fragment,創(chuàng)建完之后會(huì)自動(dòng)把創(chuàng)建的Fragment添加到導(dǎo)航圖中。
image.png -
方式2:Placeholder destinations
該方式會(huì)使用占位符來(lái)表示未實(shí)現(xiàn)的 Destinations。
image.png 方式3:直接選中已創(chuàng)建的Fragment
該方式會(huì)把選中的Fragment作為 Destinations。
Destinations 添加完成后,res / navigation / nav_graph.xml 中顯示如下:
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/fragment1">
<fragment
android:id="@+id/fragment1"
android:name="com.example.navigationdemo.Fragment1"
android:label="fragment1"
tools:layout="@layout/fragment1"/>
</navigation>
-
app:startDestination
該參數(shù)指定了啟動(dòng) App 后默認(rèn)顯示的 Destinations(即 Fragment)
5、連接目標(biāo) Fragment(Destinations)
點(diǎn)擊其中一個(gè) Fragment 的右側(cè)邊框中間的小圓點(diǎn),然后拖拽到另外一個(gè) Fragment 上,即可完成連接。

連接完成后,res / navigation / nav_graph.xml 中顯示如下:

-
android:id 屬性
我們?cè)诖a里可以通過(guò)該 id 完成兩個(gè) Fragment 之前的遷移。 -
app:destination 屬性
該屬性指定了當(dāng)前 action 所在的 Fragment 能遷移到的目的地 (Destinations)。
這里表示使用該 action ,可以從 fragment1 遷移到 fragment2
還可以給 action 配置遷移動(dòng)畫(huà),只需要指定動(dòng)畫(huà)xml即可:
<action
android:id="@+id/action_fragment1_to_fragment2"
app:destination="@id/fragment2"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
action 還有兩個(gè)參數(shù)需要注意:
-
app:popUpTo
指定要返回到的目標(biāo)頁(yè)面的 destinationId -
app:popUpToInclusive
是否要把當(dāng)前頁(yè)面彈出返回棧
這兩個(gè)參數(shù)與下面介紹的 popBackStack(@IdRes int destinationId, boolean inclusive) 方法類似。
6、代碼中使用 Navigation
6.1、MainActivity 中使用 Navigation
// 獲取 NavController
// R.id.nav_host_fragment 即 activity_main.xml 中配置的 NavHostFragment 的 id
val navController: NavController = Navigation.findNavController(this, R.id.nav_host_fragment)
// 獲取當(dāng)前正在顯示的 Destinations
navController.currentDestination
// 獲取當(dāng)前正在顯示的 Destinations 的 id
navController.currentDestination?.id
// 判斷正在顯示的 Destinations 是不是 Fragment1
// R.id.fragment1 即 res/navigation/nav_graph.xml 中對(duì)應(yīng) fragment 的 id
if (navController.currentDestination?.id == R.id.fragment1) {
......
}
6.2、Fragment 中使用 Navigation
// 獲取 NavController
// view 即 Fragment 的 getView() 方法。
// view 擁有 findNavController() 方法,是因?yàn)镹avigation組件依賴?yán)飳?duì) View 進(jìn)行了擴(kuò)展:
// fun View.findNavController(): NavController = Navigation.findNavController(this)
val navController: NavController = view?.findNavController()
// 不帶參數(shù)從 Fragment1 遷移到 Fragment2
// R.id.action_fragment1_to_fragment2 即 res/navigation/nav_graph.xml 中生成的 action 的 id
// 如果 action 配置了動(dòng)畫(huà)屬性,則會(huì)使用動(dòng)畫(huà)遷移
view?.findNavController().navigate(R.id.action_fragment1_to_fragment2)
// 帶參數(shù)從 Fragment1 遷移到 Fragment2
val args = Bundle()
args.putString("title", "123")
view?.findNavController().navigate(R.id.action_fragment1_to_fragment2, args)
// 返回前一個(gè) Fragment
// 這種方式返回:
// 如果棧中有多個(gè)Fragment,就直接調(diào)用 popBackStack方法進(jìn)行Fragment回退
// 如果當(dāng)前Fragment已經(jīng)是棧中最后一個(gè)Fragment,最終會(huì)調(diào)用Activity的finish()結(jié)束當(dāng)前Activity
view?.findNavController()?.navigateUp()
// 返回前一個(gè) Fragment
// 這種方式返回,默認(rèn)調(diào)用的是 popBackStack(getCurrentDestination().getId(), true)
// 即把當(dāng)前 Fragment 彈出棧,并返回到前一個(gè) Fragment
view?.findNavController()?.popBackStack()
// 根據(jù)指定的 destinationId 進(jìn)行返回
// 第一個(gè)參數(shù) destinationId 是 res/navigation/nav_graph.xml 中對(duì)應(yīng) fragment 的 id
// 第二個(gè)參數(shù) inclusive 表示是否把指定 destinationId 對(duì)應(yīng)的 Fragment 也彈出棧
view?.findNavController()?.popBackStack(@IdRes int destinationId, boolean inclusive)
// 比如當(dāng)前是 Fragment2 ,要返回到 Fragment1
// 如果把 destinationId 設(shè)置為 R.id.fragment2,inclusive 設(shè)為 true,此時(shí) Fragment2 會(huì)彈出棧,并返回到 Fragment1
view?.findNavController()?.popBackStack(R.id.fragment2, true)
// 與下面的效果一樣
view?.findNavController()?.navigateUp()
// 如果把 inclusive 設(shè)為 false,此時(shí)不能成功的返回到 Fragment1,因?yàn)镕ragment2未被彈出棧,一直在棧里,每次返回都是 Fragment2
view?.findNavController()?.popBackStack(R.id.fragment2, false)
// 如果想成功的返回到 Fragment1,可以改成下面這樣,把 destinationId 設(shè)置為 R.id.fragment1
view?.findNavController()?.popBackStack(R.id.fragment1, false)
// 還是上面的例子
// 如果把 destinationId 設(shè)置為 R.id.fragment1,inclusive 設(shè)為 true,此時(shí) Fragment2 會(huì)彈出棧,并返回到 Fragment1
view?.findNavController()?.popBackStack(R.id.fragment1, true)
// 但是,再次從 Fragment1 遷移到 Fragment2 時(shí)就會(huì)報(bào)異常:
view?.findNavController()?.navigate(R.id.action_fragment1_to_fragment2)
// 上面的代碼會(huì)報(bào)異常:java.lang.IllegalArgumentException: navigation destination com.example.navigationdemo:id/action_fragment1_to_fragment2 is unknown to this NavController
// 出現(xiàn)上面異常的原因是因?yàn)?,F(xiàn)ragment2 被彈出棧了,NavController 找不到 action_fragment1_to_fragment2 對(duì)應(yīng)的目的地
6.3、Navigation 與 BottomNavigationView 一起使用
6.3.1、在Gradle中增加相關(guān)依賴
implementation 'com.android.support:design:28.0.0'
6.3.1、創(chuàng)建菜單UI
和導(dǎo)航圖創(chuàng)建類似,選擇 Resource Type > Menu。
創(chuàng)建好之后添加兩個(gè)測(cè)試菜單:

注意:紅色標(biāo)識(shí)的 id 需要和下面 navigation/nav_graph.xml 中的一致,只有這樣,點(diǎn)擊 item 才會(huì)遷移到對(duì)應(yīng)的 Fragment:

6.3.2、 修改 activity_main.xml,添加 BottomNavigationView
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph"/>
<android.support.design.widget.BottomNavigationView
android:id="@+id/bottom_menu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
<!-- 菜單項(xiàng)多于3個(gè)時(shí),也顯示Label -->
<!-- Design 27 使用反射來(lái)修改 mShiftingMode 屬性實(shí)現(xiàn) -->
<!-- Design 28 中使用下面代碼實(shí)現(xiàn) -->
app:labelVisibilityMode="labeled"
<!-- 如果要禁用水波紋效果,請(qǐng)修改app:itemBackground屬性 -->
app:menu="@menu/menu"/>
</LinearLayout>
</android.support.constraint.ConstraintLayout>
6.3.3、 修改MainActivity
把BottomNavigationView與Navigation進(jìn)行綁定,綁定后即可測(cè)試使用了。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 獲取NavController
val navController = Navigation.findNavController(this, R.id.nav_host_fragment)
// 把BottomNavigationView與NavController綁定。
// 綁定后,當(dāng)菜單項(xiàng)被選中時(shí),會(huì)調(diào)用NavigationUI.onNavDestinationSelected(menuItem, navController)方法
// 這里有個(gè)注意事項(xiàng),上面創(chuàng)建菜單時(shí)有說(shuō)明,下面手動(dòng)綁定的注釋也有說(shuō)明
// BottomNavigationView擁有setupWithNavController()方法,是因?yàn)镹avigation組件依賴?yán)飳?duì)BottomNavigationView進(jìn)行了擴(kuò)展:
// fun BottomNavigationView.setupWithNavController(navController: NavController) {
// NavigationUI.setupWithNavController(this, navController)
// }
bottom_menu.setupWithNavController(navController)
// 如果不用上面的方式,可以用下面的方式自己手動(dòng)把菜單選中事件與Navigation的導(dǎo)航事件綁定
bottom_menu.setOnNavigationItemSelectedListener { menuItem ->
// 導(dǎo)航到與menuItem菜單項(xiàng)關(guān)聯(lián)的NavDestination,即與menu.xml中item的id相同的destinationId
// destinationId即navigation/nav_graph.xml中fragment的id
NavigationUI.onNavDestinationSelected(menuItem, navController)
}
}
}

