Navigation
Navigation的主要元素
- Navigation Graph是一種新型的XML資源文件,其中包含應(yīng)用程序所有的頁面,以及頁面間的關(guān)系
- NavHostFragment 是一個特殊的Fragment,可以認為它是其他Fragment的“容器”,Navigation Graph中的Fragment正是通過NavHostFragment進行展示的
- NavController 用于在代碼中完成Navigation Graph中具體的頁面切換工作
當(dāng)需要切換Fragment時,使用NavController對象,告訴想要去Navigation Graph中的哪個Fragment,NavController會將你想去的Fragment展示在NavHostFragment中
Navigation功能
主要有以下功能
- 可視化頁面導(dǎo)航圖
- 通過destination和action完成頁面間的導(dǎo)航
- 方便添加頁面切換動畫
- 頁面間類型安全的參數(shù)傳遞
- 通過NavigationUI類,對菜單、底部導(dǎo)航、抽屜菜單導(dǎo)航進行統(tǒng)一的管理
- 支持深層鏈接DeepLink
添加依賴
implementation 'android.arch.navigation:navigation-fragment:1.0.0-alpha01'
創(chuàng)建Navigation Graph
選中res目錄,右鍵 -> New -> Android Resource File

在彈出的界面中,填入文件名稱,類型和目錄名稱

如下,創(chuàng)建完成后,會出現(xiàn)下面的目錄和文件

打開nav_graph.xml文件,點擊Split,這樣可以同時看見代碼和效果,當(dāng)然現(xiàn)在什么都沒有

添加NavHostFragment
在Activity對應(yīng)的布局文件中,添加NavHostFragment,例如下面的示例
activity_navigation.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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=".navigation.NavigationActivity">
<fragment
android:id="@+id/main_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" />
</androidx.constraintlayout.widget.ConstraintLayout>
app:defaultNavHost屬性為true,則該Fragment會自動處理系統(tǒng)返回鍵。即當(dāng)用戶按下手機的返回按鈕時,系統(tǒng)能自動將當(dāng)前所展示的Fragment退出
app:navGraph屬性用于設(shè)置該Fragment對應(yīng)的導(dǎo)航圖
Fragment間的切換
創(chuàng)建兩個Fragment
FirstFragment
class FirstFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_first, container, false)
}
}
SecondFragment
class SecondFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_second, container, false)
}
}
將Fragment添加到nav_graph.xml
如下,打開nav_graph.xml,點擊右側(cè)的”+“號圖標(biāo),將 fragment_first 和 fragment_second 添加進去

在右側(cè)選中firstFragment,連線到secondFragment

這時nav_graph.xml會自動生成如下代碼,也可以直接編輯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/firstFragment">
<fragment
android:id="@+id/firstFragment"
android:name="com.example.jetpack.navigation.fragment.FirstFragment"
android:label="fragment_first"
tools:layout="@layout/fragment_first" >
<action
android:id="@+id/action_firstFragment_to_secondFragment"
app:destination="@id/secondFragment" />
</fragment>
<fragment
android:id="@+id/secondFragment"
android:name="com.example.jetpack.navigation.fragment.SecondFragment"
android:label="fragment_second"
tools:layout="@layout/fragment_second" />
</navigation>
navigation標(biāo)簽下的startDestination屬性,指定起始destination為firstFragment,即NavHostFragment容器首先展示的Fragment
第一個fragment中action標(biāo)簽的app:destination屬性表示它的目的地是secondFragment,NavControIIer使用這個action標(biāo)簽的id就能完成導(dǎo)航
使用NavControIIer完成導(dǎo)航
修改FirstFragment
class FirstFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_first, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
toSecondFragment.setOnClickListener {
// 跳轉(zhuǎn)到SecondFragment
Navigation.findNavController(it).navigate(R.id.action_firstFragment_to_secondFragment)
}
}
}
添加頁面切換動畫效果
同樣選擇Android Resource File

輸入動畫文件名稱,類型,文件更標(biāo)簽,文件目錄,點擊OK

點擊OK后會生成如下目錄和文件

編輯Fragment退出動畫的文件 exit_to_left.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/accelerate_interpolator">
<translate
android:duration="200"
android:fromXDelta="0%p"
android:toXDelta="-100%p" />
</set>
編輯Fragment進入動畫的文件 enter_from_right.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/accelerate_interpolator">
<translate
android:duration="200"
android:fromXDelta="100%p"
android:toXDelta="0%p" />
</set>
在 nav_graph.xml 文件中設(shè)置動畫
<?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/firstFragment">
<fragment
android:id="@+id/firstFragment"
android:name="com.example.jetpack.navigation.fragment.FirstFragment"
android:label="fragment_first"
tools:layout="@layout/fragment_first">
<action
android:id="@+id/action_firstFragment_to_secondFragment"
app:destination="@id/secondFragment"
app:enterAnim="@anim/enter_from_right"
app:exitAnim="@anim/exit_to_left" />
</fragment>
......
</navigation>
app:exitAnim屬性設(shè)置Fragment退出動畫
app:enterAnim屬性設(shè)置Fragment進入動畫
也可以點擊Design視圖,在中間選中firstFragment,雙擊右側(cè)Actions下的secondFragment,在彈出的面板中,設(shè)置動畫。事實上,action也可以在右側(cè)的屬性面板中設(shè)置

使用safe args插件傳遞參數(shù)
應(yīng)用插件
在項目根目錄下的build.gradle添加
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
......
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5"
}
}
在app/build.gradle添加
plugins {
......
id 'androidx.navigation.safeargs'
}
創(chuàng)建可傳遞對象
創(chuàng)建一個子能夠以對象User,或者使用基本類型
@Parcelize
data class User(var name: String, var age: Int) : Parcelable
在nav_graph.xml 的 Design界面添加參數(shù)

也可以在nav_graph.xml文件中直接添加argument標(biāo)簽
<?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/firstFragment">
......
<fragment
android:id="@+id/secondFragment"
android:name="com.example.jetpack.navigation.fragment.SecondFragment"
android:label="fragment_second"
tools:layout="@layout/fragment_second" >
<argument
android:name="user"
app:argType="com.example.jetpack.navigation.model.User" />
</fragment>
</navigation>
傳遞接收參數(shù)
傳遞參數(shù)
修改FirstFragment
class FirstFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_first, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
toSecondFragment.setOnClickListener {
// 帶上參數(shù),跳轉(zhuǎn)到SecondFragment
val action =
FirstFragmentDirections.actionFirstFragmentToSecondFragment(User("Tony", 34))
Navigation.findNavController(it).navigate(action)
}
}
}
接收參數(shù)
修改 SecondFragment
class SecondFragment : Fragment() {
private val args: SecondFragmentArgs by navArgs()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_second, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
textview.text = args.user.toString()
}
}
NavigationUI
通過NavController便可以完成頁面的切換工作,App bar中的各種按鈕和菜單,同樣承擔(dān)著頁面切換的工作。例如,當(dāng)ActionBar左邊的返回按鈕被單擊時,我們需要響應(yīng)該事件,返回到上一個頁面,為了方便管理,引入了NavigationUI組件,使App bar中的按鈕和菜單能夠與導(dǎo)航圖中的頁面關(guān)聯(lián)起來
例如:FirstFragment的ActionBar右邊有一個按鈕,通過該按鈕,可以跳轉(zhuǎn)到SecondFragment,而在SecondFragment的ActionBar左側(cè)有一個返回按鈕,通過該按鈕,可以返回MainFragment
跳轉(zhuǎn)到SecondFragment
選中res,右鍵 -> New -> Android Resource File

輸入文件名,選擇文件類型,文件目錄

點擊OK,會生成如下目錄文件

在first_fragment_menu.xml中編輯如下
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/secondFragment"
android:title="到SecondFragment"
app:showAsAction="never" />
</menu>
注意:上面item標(biāo)簽的id屬性必須和nav_graph.xml中SecondFragment對應(yīng)的id一致,否則無法跳轉(zhuǎn)
修改NavigationActivity
class NavigationActivity : AppCompatActivity() {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_navigation)
navController = Navigation.findNavController(root)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.first_fragment_menu, menu)
return super.onCreateOptionsMenu(menu)
}
/**
* 控制App Bar右側(cè)的菜單項直接跳轉(zhuǎn)
* @param item MenuItem
* @return Boolean
*/
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return NavigationUI.onNavDestinationSelected(
item,
navController
) or super.onOptionsItemSelected(item)
}
}
但我沒有找到這種方法跳轉(zhuǎn)到SecondFragment的時候,怎么傳遞數(shù)據(jù)
返回FirstFragment
修改NavigationActivity
class NavigationActivity : AppCompatActivity() {
private val appBarConfiguration: AppBarConfiguration by lazy {
AppBarConfiguration(navController.graph)
}
private val navController: NavController by lazy {
Navigation.findNavController(this, R.id.main_fragment)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_navigation)
// AppBarConfiguration用于App bar的配置,NavController用于頁面的導(dǎo)航和切換
// 將App bar和NavController綁定起來,如果沒有這句,SecondFragment的App Bar左邊不會有返回按鈕
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.first_fragment_menu, menu)
return super.onCreateOptionsMenu(menu)
}
/**
* 控制App Bar右側(cè)的菜單項直接跳轉(zhuǎn)
* @param item MenuItem
* @return Boolean
*/
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return NavigationUI.onNavDestinationSelected(
item,
navController
) or super.onOptionsItemSelected(item)
}
/**
* 當(dāng)我們在SecondFragment中單擊App Bar左邊的返回按鈕時
* NavigationUI可以幫助我們從SecondFragment回到FirstFragment
* @return Boolean
*/
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(
navController,
appBarConfiguration
) or super.onSupportNavigateUp()
}
}
注意:App bar是在 Activity中進行管理的。當(dāng)你從FirstFragment跳轉(zhuǎn)到SecondFragment時,需要在SecondFragment中覆蓋onCreateOptionsMenu()方法,并在該方法中清除MainFragment所對應(yīng)的menu,否則SecondFragment的App Bar 右上角也有個“到SecondFragment”的菜單
SecondFragment
class SecondFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_second, container, false)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
menu.clear()
super.onCreateOptionsMenu(menu, inflater)
}
}
深層鏈接DeepLink
常見的兩種應(yīng)用場景如下
- PendingIntent的方式:當(dāng)應(yīng)用程序接收到某個通知推送,你希望用戶在單擊該通知時,能夠直接跳轉(zhuǎn)到展示該通知內(nèi)容的頁面,那么可以通過PendingIntent來完成此操作
- URL的方式:當(dāng)用戶通過手機瀏覽器瀏覽網(wǎng)站上的某個頁面時,可以在網(wǎng)頁上放置一個類似于“在應(yīng)用內(nèi)打開”的按鈕。如果用戶的手機安裝有我們的應(yīng)用程序,那么通過DeepLink就能打開相應(yīng)的頁面;如果沒有安裝,那么網(wǎng)站可以導(dǎo)航到應(yīng)用程序的下載頁面,從而引導(dǎo)用戶安裝應(yīng)用程序
PendingIntent的方式
在FirstFragment發(fā)送一條向SecondFragment的深度鏈接的通知,點擊通知就跳轉(zhuǎn)到SecondFragment
修改FirstFragment如下
class FirstFragment : Fragment() {
private val channelId = "normal"
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_first, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
toSecondFragment.setOnClickListener {
// 帶上參數(shù),跳轉(zhuǎn)到SecondFragment
val action =
FirstFragmentDirections.actionFirstFragmentToSecondFragment(User("Tony", 23))
Navigation.findNavController(it).navigate(action)
}
// 深度鏈接的測試
sendNotification.setOnClickListener {
val notificationManager =
activity?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// NotificationChannel類和createNotificationChannel()方法都是Android 8.0系統(tǒng)中新增的API
createNotificationChannel(notificationManager)
val notification = getNotification()
//讓通知顯示出來
notificationManager.notify(23, notification)
}
}
/**
* 創(chuàng)建通知渠道
* @param notificationManager NotificationManager
*/
private fun createNotificationChannel(notificationManager: NotificationManager) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 創(chuàng)建通知渠道
val notificationChannel =
NotificationChannel(
channelId,
"Normal",
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(notificationChannel)
}
}
private fun getNotification(): Notification? {
//第一個參數(shù)是context,第二個參數(shù)是渠道ID,需要和我們在創(chuàng)建通知渠道時指定的渠道ID相匹配
return NotificationCompat.Builder(requireActivity().applicationContext, channelId).apply {
// 點擊跳轉(zhuǎn)后,取消通知的顯示
setAutoCancel(true)
// 設(shè)置 PendingIntent,點擊通知會執(zhí)行 PendingIntent 里面的 Intent 的意圖
setContentIntent(getPendingIntent())
// 配置通知的標(biāo)題、正文內(nèi)容、圖標(biāo)
setContentTitle("深層鏈接DeepLink")
setContentText("this is content text this is content text")
// 小圖標(biāo)會顯示在系統(tǒng)狀態(tài)欄上,只能使用純alpha圖層的圖片(.png)進行設(shè)置,否則只是一塊灰色區(qū)域
setSmallIcon(R.drawable.ic_launcher_background)
// 當(dāng)下拉系統(tǒng)狀態(tài)欄時,就可以看到設(shè)置的大圖標(biāo)
setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.ic_launcher_foreground))
}.build()
}
/**
* 獲取深度鏈接的PendingIntent
*/
private fun getPendingIntent(): PendingIntent {
val bundle = Bundle().apply {
putParcelable("user", User("Jack", 45))
}
// 跳轉(zhuǎn)到secondFragment的意圖
return Navigation.findNavController(requireActivity(), R.id.sendNotification)
.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.secondFragment)
.setArguments(bundle)
.createPendingIntent()
}
}
URL的方式
在導(dǎo)航圖中為頁面添加<deepLink/>標(biāo)簽。在app:uri屬性中填入的是你的網(wǎng)站的相應(yīng)Web頁面地址,后面的參數(shù)會通過Bundle對象傳遞到頁面中
<?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/firstFragment">
......
<fragment
android:id="@+id/secondFragment"
android:name="com.example.jetpack.navigation.fragment.SecondFragment"
android:label="fragment_second"
tools:layout="@layout/fragment_second">
<argument
android:name="user"
app:argType="com.example.jetpack.navigation.model.User" />
<deepLink app:uri="http://192.168.104.2/{params}" />
</fragment>
</navigation>
為Activity設(shè)置<nav-graph/>標(biāo)簽。當(dāng)用戶在Web頁面中訪問你的網(wǎng)站時,應(yīng)用程序便能得到監(jiān)聽
......
<activity
android:name=".navigation.NavigationActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<nav-graph android:value="@navigation/nav_graph" />
</activity>
......
使用adb測試
adb shell am start -a android.intent.action.VIEW -d "http://192.168.104.2/a.json"
注意
- 如果deepLink標(biāo)簽中帶了{params},那么對應(yīng)的測試命令中就要帶a.json或者類似的路徑
- 如果在AndroidManifest.xml中配置了nav-graph 標(biāo)簽,再使用Android Studio 新建其他組件,會報如下錯誤
Failed to query the value of property 'namespace'.
org.xml.sax.SAXParseException; systemId: file:/E:/AndroidStudioProject/Jetpack/app/src/main/AndroidManifest.xml; lineNumber: 1; columnNumber: 2; 文檔中根元素前面的標(biāo)記必須格式正確。
文檔中根元素前面的標(biāo)記必須格式正確。
如下,我在AndroidManifest.xml中配置了nav-graph 標(biāo)簽

再新建Service


如下圖,新建后會出現(xiàn)此問題

而注釋掉nav-graph 標(biāo)簽,再次新建,則不會有此問題,猜測應(yīng)該是Android Studio 工具的問題,我的版本是
