implementation deps.navigation.fragment_ktx
implementation deps.navigation.ui_ktx
implementation deps.navigation.runtime_ktx

这里使用配置好的,如果向了解详细依赖包可以参考version.gradle

创建Navigation布局文件

使用Navigation首先需要创建一个Navigation布局文件,在res/目录下新建navigation目录 ,然后在res目录右键,选择New->Navigation resource file,填入名称nav_simple

这样就创建好一个Navigation布局文件,打开可以看到

<?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_simple"
            tools:ignore="UnusedNavigation">
</navigation>

在布局文件的底部可以看到Design和Text两个选项, Design是图形化的编辑器,后续大部分设计都是在这里就可以搞定了。

左侧区域是列出所有页面,每个页面定义为Destination

中间为所有页面之间关联的图形化结果

右侧可以修改每个页面的属性和其他操作

快捷区域可以添加页面,修改页面属性和其他操作

底部可以切换图形化编辑和代码编辑

我们通过图形化编辑器来实现一个简单页面,我们新建一个Activity,命名为MainActivity

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

在清单文件中注册该活动

    <application
            android:label="@string/app_name"
            android:theme="@style/AppTheme"
        <activity
                android:name=".MainActivity"
                android:theme="@style/BaseAppTheme">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        .....

为该活动创建布局文件

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:layout_width="0dp"
            android:layout_height="0dp"
            android:name="androidx.navigation.fragment.NavHostFragment"
            app:defaultNavHost="true"
            app:navGraph="@navigation/nav_simple"
            android:id="@+id/nav_container"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

在布局文件中我们需要给NavHostFragment指定导航布局,在res/navigation创建一个简单的布局nav_simple.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"
        android:id="@+id/nav_simple">
</navigation>

切换到图形编辑页面,在快捷工具区域点击最左侧的添加按钮,新建一个Fragment

这时候默认新建的Fragment为app:startDestination,也就是默认启动页面.这样启动后就可以看到我们创建的页面了

<?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/nav_sample"
        app:startDestination="@id/firstFragment">
    <fragment
            android:id="@+id/firstFragment"
            android:name="com.example.navigation.FirstFragment"
            android:label="FirstFragment" />
</navigation>

以前我们切换Fragment页面都是类似如下代码

    supportFragmentManager.beginTransaction()
        .add(R.id.bottomNavigationView, fragemnt)
        .commit()

但是使用Navigation简单很多. 我们按着上面的方式在添加一个目标页面SecondFrament

<?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/nav_"
        app:startDestination="@id/firstFragment">
    <fragment
            android:id="@+id/firstFragment"
            android:name="com.example.navigation.FirstFragment"
            android:label="FirstFragment" >
        // 自动生成
        <action
                android:id="@+id/action_firstFragment_to_secondFragment"
                app:destination="@id/secondFragment" />
    </fragment>
    <fragment
            android:id="@+id/secondFragment"
            android:name="com.example.navigation.SecondFragment"
            android:label="SecondFragment" />
</navigation>

这样我们在FristFragment页面调用如下代码就可以实现页面的切换了

  btn_jump.setOnClickListener {
      findNavController().navigate(R.id.action_firstFragment_to_secondFragment)

但是Navigation切换页面是通过replace方法,所以返回上一个页面Fragment会重新初始化重建,解决方法可以参考https://blog.csdn.net/WitheredSkull/article/details/88532687

需要传递参数只需要调用NavController重载的navigation()方法

navigate(@IdRes int resId, @Nullable Bundle args) 

将参胡通过Bundle传递即可,在目标页面通过arguments变量就可以获取到

  arguments?.let {
      tv_argument.text = it.getString("Arg_Name")

对于页面跳转,传递数据,还可以通过navigation-arg插件来实现,会更加简单

应用插件需要在工程根目录下添加build.gradle插件

dependencies {
        classpath deps.android_gradle_plugin
        classpath deps.kotlin.plugin
        classpath deps.navigation.safe_args_plugin // navigation-safe-args-gradle-plugin

app的mobule下build.gradle中应用插件

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
// 应用插件
apply plugin: "androidx.navigation.safeargs.kotlin"

这样点击编译后就会在build目录下自动生成相关类,如下图:

在需要跳转的地方按着如下调用

FirstFragmentDirections.actionFirstFragmentToSecondFragment()

在目标页面获取参数可以通过navArgs来获取

    private val receiveArg by navArgs<ReceiveArgsFragmentArgs>()
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        btn_show_nav_args.setOnClickListener {
            tv_argument.text = receiveArg.ArgName

navArgs是Fragment一个扩展内联函数,从下面源码可以看出,通过懒加载方法读取argumentsBundle中的数据

@MainThread
inline fun <reified Args : NavArgs> Fragment.navArgs() = NavArgsLazy(Args::class) {
    arguments ?: throw IllegalStateException("Fragment $this has null arguments")
class NavArgsLazy<Args : NavArgs>(
    private val navArgsClass: KClass<Args>,
    private val argumentProducer: () -> Bundle
) : Lazy<Args> {
    private var cached: Args? = null
    override val value: Args
        get() {
            var args = cached
            if (args == null) {
                val arguments = argumentProducer()
                val method: Method = methodMap[navArgsClass]
                    ?: navArgsClass.java.getMethod("fromBundle", *methodSignature).also { method ->
                        // Save a reference to the method
                        methodMap[navArgsClass] = method
                @Suppress("UNCHECKED_CAST")
                args = method.invoke(null, arguments) as Args
                cached = args
            return args
    override fun isInitialized() = cached != null

Deeplink跳转

Deeplink可以让我们在app外或者启动活动后,直接跳转到指定的Deeplink页面. 该功能类似ARouter中我们指定链接后,在浏览器拉起App跳转到指定页面.

我们新建一个DeeplinkFragment来展示Deeplink跳转效果.然后在nav_simple.xml中,通过图形设计面板右侧,点击Deep Links +,输入链接

    <fragment
            android:id="@+id/deeplinkFragment"
            android:name="com.example.navigation.DeeplinkFragment"
            android:label="DeeplinkFragment">
        <deepLink
                android:id="@+id/deepLink"
                app:uri="www.example.com" />
    </fragment>

我们在清单文件中使用该Fragment的Activity添加intent-filter

.....
<activity android:name=".MainActivity"
            android:theme="@style/BaseAppTheme">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
        <nav-graph android:value="@navigation/nav_simple" />
</activity>
......

这样在外部通过链接进入App,就可以直接打开页面. 这里是通过<nav-graph>标签,在编译打包后,会自动转化为<intent-filter>,但是默认的schemehttp,https. 如果自定义的可以自己注册intent-filter

<intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data android:scheme="juejin" android:host="www.deeplink.com" android:pathPattern="/.*"/>
</intent-filter>

安装好App后,我们用adb命令,通过链接打开指定的页面

adb shell am start -a android.intent.action.VIEW -d "http://www.deeplink.com"

通过PendingIntent拉起指定页面,这里通过NavDeepLinkBuilder可以构建PendingIntent

NavDeepLinkBuilder(context)
        .setGraph(R.navigation.nav_simple)
        .setDestination(R.id.deeplinkFragment)
        .setArguments(Bundle().apply {
            putString("Arg_From", "AppWidget")
        .createPendingIntent()

通过h5唤起App,这里一般就需要自己定义scheme(非http,https),否则浏览器会识别为链接打开,不会拉起App(Google Chrome除外)

Fragment切换可以通过给<action>配置如下属性实现

  • app:enterAnim
  • 进入页面动画

  • app:exitAnim
  • 退出页面动画

  • app:popEnterAnim
  • 退出页面再次显示动画

  • app:popExitAnim
  • 进入页面被弹出动画

    如下示例:

        <action
                android:id="@+id/action_secondFragment_to_animFragment"
                app:destination="@id/animFragment"
                app:enterAnim="@anim/right_in"
                app:exitAnim="@anim/fade_out"
                app:popEnterAnim="@anim/left_in"
                app:popExitAnim="@anim/right_out"
                app:popUpTo="@id/firstFragment" />
    

    NavController中有个重载的方法

    navigate(@NonNull NavDirections directions, @Nullable NavOptions navOptions)
    

    NavOptions与xml中属性是对应的,所以可以通过NavOptions.Builder设置动画

    findNavController().navigate(
        SecondFragmentDirections.actionSecondFragmentToAnimFragment(), NavOptions.Builder()
            .setEnterAnim(R.anim.nav_default_enter_anim)
            .setExitAnim(R.anim.nav_default_exit_anim)
            .setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
            .setPopExitAnim(R.anim.nav_default_pop_exit_anim)
            .build()
    

    说明:弹出返回popUp属性

    popUp指定的页面必须在返回堆栈内,否则无效固 popUpToInclusive true 则指定要返回的页面也会弹出堆栈

    问题:在设置了popUp后属性app:exitAnim的动画无效果 Fixed: A->B->C B->C 设置的action动画app:exitAnim不会作用与B A->B 设置的action的动画app:popExitAnim作用于B退出 因为B在进入C的时候是弹出堆栈所以设置弹出动画 而B->C设置的弹出动画则作用于C退出

    共享元素切换

    Navigation也支持共享元素的切换.通过调用NavController重载的navigate方法

    public void navigate(@NonNull NavDirections directions,
            @NonNull Navigator.Extras navigatorExtras) 
    

    参数1是掉转目标,参数2指定共享的View,如下

                val extra = FragmentNavigatorExtras(
                    iv_thumb to SHARE_ELEMENT_NAME
                findNavController().navigate(
                    FirstFragmentDirections.actionFirstFragmentToShareElementFragment(),
                    extra
    

    注意这里的SHARE_ELEMENT_NAME与下面给View设置的transitionName属性值是相同的

    同时需要在两个Fragment的xml布局中对共享的View设置如下属性,并且值相同

     android:transitionName=""
    

    对话框是使用DialogFragment,而DialogFragment本身就是集成Fragment,所以对话框的使用和导航Fragment使用方法是一样的,不同之处就是navigation的xml文件中使用的是<dialog>标签

        <dialog
                android:id="@+id/navDialogFragment"
                android:name="com.example.navigation.NavDialogFragment"
                android:label="NavDialogFragment"
                tools:layout="@layout/dialog_update_layout" />
    

    整合DrawerLayout和BottomNavigationView

    对于与DrawableLayout,BottomNavigationView一起使用主要依靠NavigationUI这个工具类, 通过重载setupWithNavController来实现对Toolbar, DrawableLayout等组件的组合控制

  • 与DrawableLayout组合
  • 协调DrwableLayout是通过调用下面方法实现

    public static void setupWithNavController(@NonNull final NavigationView navigationView,
            @NonNull final NavController navController) 
    

    协调BottomNavigationView是通过调用下面方法实现

    public static void setupWithNavController(
            @NonNull final BottomNavigationView bottomNavigationView,
            @NonNull final NavController navController) {
    

    核心代码示例:

    navController = findNavController(R.id.drawer_nav_fragment) appBarConfiguration = AppBarConfiguration(navController.graph, drawer_layout) // Set up ActionBar setSupportActionBar(toolbar) setupActionBarWithNavController(navController, appBarConfiguration) // Set up navigation menu navigation_view.setupWithNavController(navController) // if bottomNavigationView bottom_nav_view.setupWithNavController(navController)

    在布局文件中BottomNavigationView和NavigationView都要指定app:menu属性. 这里要注意一点,res/menu下创建的文件中<item>标签的Id必须与res/navigation下fragment的id相同才可以实现点击效果

    res/menu/bottom_nav_menu.xml

    <menu xmlns:android="http://schemas.android.com/apk/res/android">
                android:id="@+id/home"
                android:contentDescription="@string/cd_home"
                android:icon="@drawable/ic_home"
                android:title="@string/title_home" />
                android:id="@+id/list"
                android:contentDescription="@string/cd_list"
                android:icon="@drawable/ic_list"
                android:title="@string/title_list" />
                android:id="@+id/register"
                android:contentDescription="@string/cd_form"
                android:icon="@drawable/ic_feedback"
                android:title="@string/title_register" />
    </menu>
    

    res/navigation/nav_bottom.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"
            android:id="@+id/nav_bottom_simple"
            app:startDestination="@id/home">
        <fragment
                android:id="@+id/home"
                android:name="com.example.navigation.nav.Home"
                android:label="Home" />
        <fragment
                android:id="@+id/list"
                android:name="com.example.navigation.nav.List"
                android:label="List" />
        <fragment
                android:id="@+id/register"
                android:name="com.example.navigation.nav.Register"
                android:label="Register" />
    </navigation>
    

    我们可以通过NavigationUI中的代码找到答案

    public static boolean onNavDestinationSelected(@NonNull MenuItem item, @NonNull NavController navController) { ..... NavOptions options = builder.build(); try { //TODO provide proper API instead of using Exceptions as Control-Flow. navController.navigate(item.getItemId(), null, options); // MenuItem的Id也是导航fragment的Id return true; } catch (IllegalArgumentException e) { return false; .....

    至此Navigation组件的使用介绍完成,欢迎startstar

    题外:踩坑小记

    更新了AndroidStudio 3.6Canary 3 到 3.6Canary 4编译后报错,记录下:

    AndroidStudio升级踩坑日志:

    AndroidStudio 3.6 Canary 4 kotlin_version = 1.3.40 gradle_plugin = 3.6.0-alpha04 否则报错

    constrain-layout 2.0.0-alpha3以下可以,以上的版本编译报错