概述

Navigation是采用一个Activity和多个Fragment形式设计的Ui架构模式,但是众所周知,Fragment的管理一直是个麻烦事,需要通过FragmentManager和FragmentTransaction来管理Fragment之间的切换。所以Google提供了一套Navigation用来管理Fragment相互间的跳转等逻辑。我们先看下Navigation的优势:

  • 处理 Fragment 事务。
  • 默认情况下,正确处理往返操作。
  • 为动画和转换提供标准化资源。
  • 实现和处理深层链接。
  • 包括导航界面模式(例如抽屉式导航栏和底部导航),用户只需完成极少的额外工作。
  • Safe Args - 可在目标之间导航和传递数据时提供类型安全的 Gradle 插件。
  • ViewModel支持 - 您可以将ViewModel的范围限定为导航图,以在图表的目标之间共享与界面相关的数据。

正式介绍前,我们需要了解Navigation是由哪几部分组成的,现在我们看一下:

  • NavHostFragment:一种特殊的Fragment,用于承载导航内容的容器。
  • Navigation Graph:一个包含所有导航和页面关系相关的 XML资源。
  • NavController:管理应用导航的对象,实现Fragment之间的跳转等操作。

Navigation使用

Navigation Graph

首先我们在build.gradle 文件中新增navigation的依赖:

// Kotlin
    implementation("androidx.navigation:navigation-fragment-ktx:$lifecycle_version") 
    implementation("androidx.navigation:navigation-ui-ktx:$lifecycle_version")

然后我们需要新建一个Navigation,点击Project->Res->New Android Resource File,Resource Type选择Navigation,如下图:

android顶部导航自定义 android 导航组件_android顶部导航自定义

打开新建的nav_graph.xml文件,在Design界面可以看到目前还没有内容,可以依次点击[New Destination]图标,然后点击[Create new destination],即可快速创建新的Fragment,这里作次示例,因为本身之前我已经新建好多个Fragment了,就不单独示范了。

android顶部导航自定义 android 导航组件_xml_02

我们可以点击视图的小圆点,建议页面的绑定关系。我这里已经建立好多个页面的绑定关系,同时你可以切换至code模块,本人代码如下:

<?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/jetpack_nav"
    app:startDestination="@id/navigationFragment">

    <fragment
        android:id="@+id/navigationFragment"
        android:name="com.silence.apmprojetct.NavigationFragment"
        android:label="fragment_navigation"
        tools:layout="@layout/fragment_navigation">
        <action
            android:id="@+id/action_navigationFragment_to_blankFragment"
            app:destination="@id/blankFragment" />
        <action
            android:id="@+id/action_navigationFragment_to_threeFragment"
            app:destination="@+id/threeFragment" />
    </fragment>
    <fragment
        android:id="@+id/blankFragment"
        android:name="com.silence.apmprojetct.SecondFragment"
        android:label="fragment_blank"
        tools:layout="@layout/fragment_second">
        <action
            android:id="@+id/action_blankFragment_to_threeFragment"
            app:destination="@id/threeFragment" />
    </fragment>
    <fragment
        android:id="@+id/threeFragment"
        android:name="com.silence.apmprojetct.ThreeFragment"
        android:label="fragment_three"
        tools:layout="@layout/fragment_three">
        <action
            android:id="@+id/action_threeFragment_to_navigationFragment"
            app:destination="@+id/navigationFragment"
            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" />
    </fragment>
</navigation>

我们看下几个标签的含义:

  • navigation:根标签,通过startDestination配置指定默认的启动页面。
  • fragment: fragment标签代表一个fragment。
  • action:action标签定义了页面跳转的行为,destination标签定义跳转的目标页,里面还有启动模式的相关设置等等。
NavHostFragment

而讲到这里,我们只概括前面三个模块其中之一Navigation Graph。我们知道Fragment需要一个Activity才能正常运行,但是我们如何在我们的Activity指定我们上面startDestination呢。这就需要用到了NavHostFragment。我们看下我们Activity的布局代码:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context=".NavigationActivity">
    <fragment
        android:id="@+id/fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/jetpack_nav" />

</FrameLayout>

我们可以看到布局里定义了一个Fragment,并且它使用的就是NavHostFragment,可以看到navGraph属性绑定了我们刚刚写的Navigation文件。而defaultNavHost就是做到了自动实现了页面间的返回操作。如果没有这个属性,点击返回的时候就是直接关闭当前Activity了。当然,其内部Fragment的点击事件我们可以自己控制。

然后我们可以运行程序,可以看到默认展示的是NavigationFragment页面,这是因为NavigationActivity的布局文件中配置了NavHostFragment,并且给NavHostFragment指定了默认展示的页面为NavigationFragment。

NavController

NavController主要用来管理fragment之间的跳转,每个NavHost均有自己的相应NavController。然后可以使用它的navigate或者navigateUp方法来进行页面之间的路由操作。比如我这边的NavigationFragment需要跳转SecondFragment。我们可以这么写:

override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_navigation, container, false)
        view.findViewById<TextView>(R.id.next).setOnClickListener {
            findNavController().navigate(R.id.action_navigationFragment_to_blankFragment)
        }
        return view
    }

也就是通过navigate指向前面我们在action定义的id。即可完成跳转。

参数传递

Bundle数据传递

我们可以创建一个Bundle对象,然后使用navigate()将它传递给目的地,代码如下:

val bundle = Bundle()
        bundle.putString(TITLE, getString(R.string.three_fragment_navigation_bundle_label))
        findNavController().navigate(R.id.action_blankFragment_to_threeFragment, bundle)
使用 Safe Args 传递安全的数据

这是官方为了navigation数据传递安全提供的一个数据传递方式,首先我们需要在根目录的build.gradle文件中添加如下依赖:

dependencies {
        def nav_version = "2.3.3"
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
    }

然后我们在模块的build.gradle文件中新增如下:

//kotlin
apply plugin: "androidx.navigation.safeargs.kotlin"
//java
apply plugin: 'androidx.navigation.safeargs'

然后我们ReBuild项目,可以看到在build目录中生成了如下文件:

android顶部导航自定义 android 导航组件_android顶部导航自定义_03

然后我们修改我们的代码,比如我需要在ThreeFragment页面新增一个参数,那么我们需要在navigation文件中新增如下代码:

<fragment
        android:id="@+id/threeFragment"
        android:name="com.silence.apmprojetct.ThreeFragment"
        android:label="fragment_three"
        tools:layout="@layout/fragment_three">
        <action
            android:id="@+id/action_threeFragment_to_navigationFragment"
            app:destination="@+id/navigationFragment" />
        <argument
            android:name="key"
            app:argType="string"
            android:defaultValue="safeArgs" />
    </fragment>

然后我们在需要跳转的时候修改下代码,比如我这里SecondFragment会跳转至ThreeFragment。那么我们只需要这么写:

view.findViewById<TextView>(R.id.next).setOnClickListener {
//            val bundle = Bundle()
//            bundle.putString(TITLE, getString(R.string.three_fragment_navigation_bundle_label))
//            findNavController().navigate(R.id.action_blankFragment_to_threeFragment, bundle)
            var secondFragment =
                SecondFragmentDirections.actionBlankFragmentToThreeFragment("safe传递的数据")
            findNavController().navigate(secondFragment)
        }

我们在看下如何在ThreeFragment里接收数据:

view.findViewById<TextView>(R.id.title).text = arguments?.let {
            ThreeFragmentArgs.fromBundle(it).key
        }

其他场景

堆栈处理

前面我们说到了正常的页面启动,以及如何挟带参数跳转。现在有个场景,比如我A-》B-》C之后,然后直接回到A。并且清栈,也就是在A页面返回就直接关闭当前的Activity了。我们看一下,目前我们的代码执行后效果会如何:

android顶部导航自定义 android 导航组件_android_04

可以看出来我们每次打开新的fragment就相当于新开了一个堆栈。我们可记得我们配置页面的时候有一个action属性。我们把action的代码改成如下:

<fragment
        android:id="@+id/threeFragment"
        android:name="com.silence.apmprojetct.ThreeFragment"
        android:label="fragment_three"
        tools:layout="@layout/fragment_three">
        <action
            android:id="@+id/action_threeFragment_to_navigationFragment"
            app:destination="@+id/navigationFragment"
            app:popUpTo="@+id/navigationFragment"
            app:popUpToInclusive="true" />
    </fragment>

我们运行一下,在看一下效果:

android顶部导航自定义 android 导航组件_xml_05

哎,可以了。

其实action还有一个属性launchSingleTop的设置,这个看具体场景使用即可。

指定startDestination

我们想一下,如何指定startDestination的Fragment呢。上面介绍过,activity要绑定NavHostFragment。而NavHostFragment要指定startDestination。在我们在navigaiton指定了fragment后,可以修改么。或者说,我可以动态改变他的startDestination嘛。其实是可以的。我们知道startDestination是由navGraph指定的。那么我们只需要修改activity的代码,就可以做到了。具体示例如下:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_navigation)
        val startDestinationID = R.id.blankFragment
        val navController = Navigation.findNavController(this, R.id.fragment)
        val navInflater = navController.navInflater
        val navGraph = navInflater.inflate(R.navigation.jetpack_nav)
        navGraph.startDestination = startDestinationID
        navController.graph = navGraph
    }

那我们执行一下,看下效果:

android顶部导航自定义 android 导航组件_xml_06

我这边指定了SecondFragment成功了。那具体指定哪一个,由你自己的业务场景而定。

总结

本文我们主要介绍了Jetpack中的导航组件Navigation的组件,从普通的跳转,已经挟带参数跳转,甚至一些特殊场景的跳转逻辑。关于一些其他场景,我这里不过多介绍,因为目前正常的跳转场景基本已经了解了。如果你想知道更多的使用场景,请查看官方文档