Jetpack之Navigation

Navigation

Navigation的主要元素

  1. Navigation Graph是一種新型的XML資源文件,其中包含應(yīng)用程序所有的頁面,以及頁面間的關(guān)系
  2. NavHostFragment 是一個特殊的Fragment,可以認為它是其他Fragment的“容器”,Navigation Graph中的Fragment正是通過NavHostFragment進行展示的
  3. NavController 用于在代碼中完成Navigation Graph中具體的頁面切換工作

當(dāng)需要切換Fragment時,使用NavController對象,告訴想要去Navigation Graph中的哪個Fragment,NavController會將你想去的Fragment展示在NavHostFragment中

Navigation功能

主要有以下功能

  1. 可視化頁面導(dǎo)航圖
  2. 通過destination和action完成頁面間的導(dǎo)航
  3. 方便添加頁面切換動畫
  4. 頁面間類型安全的參數(shù)傳遞
  5. 通過NavigationUI類,對菜單、底部導(dǎo)航、抽屜菜單導(dǎo)航進行統(tǒng)一的管理
  6. 支持深層鏈接DeepLink

添加依賴

implementation 'android.arch.navigation:navigation-fragment:1.0.0-alpha01'

創(chuàng)建Navigation Graph

選中res目錄,右鍵 -> New -> Android Resource File


image-20211217161402849.png

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


image-20211217161527198.png

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

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


image-20211217162312552.png

添加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 添加進去


image-20211217163551812.png

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


image-20211217163914709.png

這時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


image-20211217165333467.png

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


image-20211217165458606.png

點擊OK后會生成如下目錄和文件
image-20211217165621816.png

編輯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è)置


image-20211217170923154.png

使用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ù)


image-20211217175436614.png

也可以在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


image-20211217202634402.png

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


image-20211217202751831.png

點擊OK,會生成如下目錄文件
image-20211217202858488.png

在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)用場景如下

  1. PendingIntent的方式:當(dāng)應(yīng)用程序接收到某個通知推送,你希望用戶在單擊該通知時,能夠直接跳轉(zhuǎn)到展示該通知內(nèi)容的頁面,那么可以通過PendingIntent來完成此操作
  2. 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"

注意

  1. 如果deepLink標(biāo)簽中帶了{params},那么對應(yīng)的測試命令中就要帶a.json或者類似的路徑
  2. 如果在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)簽


image-20211218131547534.png

再新建Service


image-20211218131633059.png

image-20211218131650769.png

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


image-20211218131726101.png

而注釋掉nav-graph 標(biāo)簽,再次新建,則不會有此問題,猜測應(yīng)該是Android Studio 工具的問題,我的版本是
image-20211218131858388.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容