Navigation使用以及源码简单分析

Navigation最新版本查看
使用文档

使用流程

根据自己是kotlin还是java来决定添加哪个

dependencies {
  def nav_version = "2.3.0"

  // Java language implementation
  implementation "androidx.navigation:navigation-fragment:$nav_version"
  implementation "androidx.navigation:navigation-ui:$nav_version"

  // Kotlin
  implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
  implementation "androidx.navigation:navigation-ui-ktx:$nav_version"

  // Feature module Support
  implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"

  // Testing Navigation
  androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"
}

可能还需要一些插件,开头的地址有写

Safe Args【非必须的】

要将 Safe Args 添加到您的项目,请在顶级 build.gradle 文件中包含以下 classpath

   buildscript {
       repositories {
           google()
       }
       dependencies {
           def nav_version = "2.3.0-alpha01"
           classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
       }
   }
   

您还必须应用以下两个可用插件之一。

要生成适用于 Java 或 Java 和 Kotlin 混合模块的 Java 语言代码,请将以下行添加到应用或模块build.gradle 文件中:

apply plugin: "androidx.navigation.safeargs"

此外,要生成适用于 Kotlin 独有的模块的 Kotlin 代码,请添加以下行:

apply plugin: "androidx.navigation.safeargs.kotlin"

根据迁移到 AndroidX) 文档,您的 gradle.properties 文件 中必须具有 android.useAndroidX=true

Navigation 组件旨在用于具有一个主 Activity 和多个 Fragment 目的地的应用。主 Activity 与导航图相关联,且包含一个负责根据需要交换目的地的 NavHostFragment。在具有多个 Activity 目的地的应用中,每个 Activity 均拥有其自己的导航图。

开始

  1. 要向项目添加导航图,请执行以下操作:

在“Project”窗口中,右键点击 res 目录,然后依次选择 New > Android Resource File。此时系统会显示 New Resource File 对话框。
在 File name 字段中输入名称,例如“nav_graph”。
从 Resource type 下拉列表中选择 Navigation,然后点击 OK。
当您添加首个导航图时,Android Studio 会在 res 目录内创建一个 navigation 资源目录。该目录包含您的导航图资源文件(例如 nav_graph.xml)。

image.png

image.png

刚建好啥都没有,自己可以点击加号添加,下边列出了所有的fragment和activity,然后左侧那个红框连接可以跳转到文档告诉你咋添加.


image.png
  1. 给activity的布局里添加NavHostFragment
    这个是系统写好的Fragment,可以处理导航图里的fragment,
    打开activity的布局文件,左侧palette面板,选择Containers,右侧选择NavHostFragment,然后拖动到布局里


    image.png

    然后出来个弹框,我们可以选择红框里我们刚刚建的那个nav_test文件,也可以点上边的加号新建一个,完事点OK


    image.png

    然后看下activity的xml文件多了个fragment
    <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/nav_test" />

请注意以下几点:

android:name 属性包含 NavHost 实现的类名称。
app:navGraph 属性将 NavHostFragment 与导航图相关联。导航图会在此 NavHostFragment 中指定用户可以导航到的所有目的地。
app:defaultNavHost="true" 属性确保您的 NavHostFragment 会拦截系统返回按钮。请注意,只能有一个默认 NavHost。如果同一布局(例如,双窗格布局)中有多个主机,请务必仅指定一个默认 NavHost

  1. 添加fragment
    通过点击navigation文件上边的那个加号,可以添加新的destination fragment进来,如果已经写好的可以直接在列表里选,如果还没有,可以直接点击create new,也可以暂时用个placeholder来代替【编译没问题,你要往这里跳转就不行了】

  2. 设置start Fragment,就是默认加载的那个
    在navigation视图里点击那个fragment,完事右键 ,选择save as fragment或者点击上边的房子图标也行
    然后navigation标签下就多了个属性startDestination

<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_test"
    app:startDestination="@id/fragmentNavPreference">
  1. 添加跳转流程
    选中某个fragment的时候,可以看到右侧中间有个圆圈,点击然后把它拖动到另外 一个fragment上松手,就可以看到两者直接有条线了,如下图


    image.png

    代码里也会多出一个标签action
    id :就是这action的id,之后跳转用到的
    destination:就是这个action要跳到的fragment的id

    <fragment
        android:id="@+id/fragmentNavRoot"
        android:name="com.mitac.app2020.sep.nav.FragmentNavRoot"
        android:label="FragmentNavRoot"
        tools:layout="@layout/fragment_nav_root">
        <action
            android:id="@+id/action_navRoot_to_navPreference"
            app:destination="@id/fragmentNavPreference" />
        <action
            android:id="@+id/action_fragmentNavRoot_to_fragmentCategory"
            app:destination="@id/fragmentCategory" />
    </fragment>
  1. 代码里如何跳转fragment?
    都是通过NavController来控制的,下边是获取Controller的方法


    image.png

跳转的时候就要用到action里的id了

    <fragment
        android:id="@+id/fragmentNavRoot"
        android:name="com.mitac.app2020.sep.nav.FragmentNavRoot"
        android:label="FragmentNavRoot"
        tools:layout="@layout/fragment_nav_root">
        <action
            android:id="@+id/action_navRoot_to_navPreference"
            app:destination="@id/fragmentNavPreference" />
        <action
            android:id="@+id/action_fragmentNavRoot_to_fragmentCategory"
            app:destination="@id/fragmentCategory" />
    </fragment>

代码

        iv1.setOnClickListener {
            findNavController().navigate(R.id.action_navRoot_to_navPreference)
        }

        iv6.setOnClickListener {

            findNavController().navigate(R.id.action_fragmentNavRoot_to_fragmentCategory)
        }
//也可以这样写
iv3.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_fragmentNavRoot_to_fragmentCategory))

当然了navigate后边参数有很多的,可以添加bundle啥的

  1. Safe Args
    如果你记不住id的话,又添加了文章开头的Safe Args 插件的话,那么你build一下工程,然后就多了一些类了
    比如我们的fragment类名是FragmentNavRoot,那么就有一个FragmentNavRootDirection的类,这个类下边有几个方法,navigation图里有几个action就有几个方法,方法名字貌似就是action的id
    如下
         val directions=FragmentNavRootDirections.actionFragmentNavRootToFragmentCategory()
        findNavController().navigate(directions)

        findNavController().navigate(FragmentNavRootDirections.actionNavRootToNavPreference())
  1. 返回
    咋回到上一页了?默认后退键就可以回去了,activity里已经处理过了。如果我们需要点击某个按钮啥的返回,那么也简单
 findNavController().navigateUp()

如果Fragment A 跳到B ,B再跳到C,然后C想直接回到A咋办?
如下,第一个参数就是A的fragment id,第二个参数是为是否A也弹出,这里肯定false了,我们要跳到A的,
如果第一个参数写B的id,那么第二个参数写成true倒是刚好.

findNavController().popBackStack(R.id.fragmentA,false)

9.参数介绍
navigate 跳转到 时候,其实除了第一个action id以外,还有3个参数可以设置的,bundle就不说了,这个都会,看下其他两个

    public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
            @Nullable Navigator.Extras navigatorExtras)

注意一下,这些参数是可以写在导航的action标签里的
9.1 NavOptions
一般使用里边的builder来创建对象,kotlin下有扩展的方法可用,后边有写

    public static final class Builder {
        boolean mSingleTop;
        @IdRes
        int mPopUpTo = -1;
        boolean mPopUpToInclusive;
        @AnimRes @AnimatorRes
        int mEnterAnim = -1;//四种动画就不讲了
        @AnimRes @AnimatorRes
        int mExitAnim = -1;
        @AnimRes @AnimatorRes
        int mPopEnterAnim = -1;
        @AnimRes @AnimatorRes
        int mPopExitAnim = -1;

        /**
         * Launch a navigation target as single-top if you are making a lateral navigation
         * between instances of the same target (e.g. detail pages about similar data items)
         * that should not preserve history.
         *
         * @param singleTop true to launch as single-top
         */
        @NonNull
        public Builder setLaunchSingleTop(boolean singleTop) {
            mSingleTop = singleTop;
            return this;
        }

        /**
         * Pop up to a given destination before navigating. This pops all non-matching destinations
         * from the back stack until this destination is found.
         *跳转之前先弹出一些Fragment,在destinationId之上的【inclusive决定是否也弹出这个destinationId】
         * @param destinationId The destination to pop up to, clearing all intervening destinations.
         * @param inclusive true to also pop the given destination from the back stack.
         * @return this Builder
         * @see NavOptions#getPopUpTo
         * @see NavOptions#isPopUpToInclusive
         */
        @NonNull
        public Builder setPopUpTo(@IdRes int destinationId, boolean inclusive) {
            mPopUpTo = destinationId;
            mPopUpToInclusive = inclusive;
            return this;
        }

下边是kotlin版本的

 navOptions { NavOptionsBuilder().popUpTo=R.id.fragmentNavRoot }

java的

val navOptions=NavOptions.Builder().apply {
            setPopUpTo(R.id.fragmentNavRoot,false)
        }.build()

设置了popUpTo 我们可以在跳转之前弹出一些Fragment.
9.2 Extras
这个就一个接口,没有任何方法的

    /**
     * Interface indicating that this class should be passed to its respective
     * {@link Navigator} to enable Navigator specific behavior.
     */
    public interface Extras {
    }

搜了下,有两个实现的地方


image.png

Fragment传递sharedElement

FragmentNavigator.Extras.Builder().addSharedElement(btn_go_search,"test").build()

Activity也差不多,可以传递共享元素

        val options = ActivityOptions.makeSceneTransitionAnimation(
            this,
            sharedView,
            "shared_element_container" // The transition name to be matched in Activity B.
        )

ActivityNavigator.Extras.Builder().setActivityOptions(options).build()

跳转监听

我们导航来回跳转fragment的时候,可能用的是同一个toolbar或者bottomBar之类的,这时候切换fragment的时候我们可能需要修改标题啥的,咋办?activity里添加一个监听即可,监听里可以拿到要跳转的目的地信息以及参数.

findNavController(R.id.fragment).addOnDestinationChangedListener(this)

findNavController(R.id.fragment).removeOnDestinationChangedListener(this)

    override fun onDestinationChanged(
        controller: NavController,
        destination: NavDestination,
        arguments: Bundle?
    ) {
        println("changed=======$destination=======${arguments?.size()}")
        changeTitle(destination.label?:"...")
    }

argument支持的参数类型

image.png
使用safe arg插件传递数据的demo

上边的基础数据类型没啥说的,我们测试下序列化数据很枚举的
注意:

  1. argType 后边是类的完整名字,带路径的
  2. 如果默认值是null的话,必须添加app:nullable="true"
        <action
            android:id="@+id/action_fragmentNavRoot_to_fragmentSearchResult"
            app:destination="@id/fragmentSearchResult"
            app:popUpToInclusive="false">
            <argument
                android:name="data1"
                android:defaultValue="@null"
                app:argType="com.xxx.app2020.sep.nav.ArgSerializableTest"
                app:nullable="true" />
            <argument
                android:name="data2"
                android:defaultValue="SUN"
                app:argType="com.xxx.app2020.sep.nav.ArgEnumTest" />
        </action>


    <fragment
        android:id="@+id/fragmentSearchResult"
        android:name="com.mitac.app2020.sep.nav.FragmentSearchResult"
        android:label="FragmentSearchResult"
        tools:layout="@layout/fragment_search_result">
        <argument
            android:name="data1"
            android:defaultValue="@null"
            app:nullable="true"
            app:argType="com.mitac.app2020.sep.nav.ArgSerializableTest" />
        <argument
            android:name="data2"
            android:defaultValue="SUN"
            app:argType="com.mitac.app2020.sep.nav.ArgEnumTest" />
    </fragment>

然后build一下工程,就能看到在build/generated/source/navigation-args/debug/{pakage name}/下自动生成的类了,如下图
Fragment下带action的会生成FragmentXXXDirections
Fragment下带argument的会生成FragmentXXXArgs


image.png

然后就可以用了,这些FragmentXXXDirections 下就会多了一些带argument参数的方法
传递数据

  val data1=ArgSerializableTest("test",22)
            val data2=ArgEnumTest.FLOWER
            findNavController().navigate(FragmentNavRootDirections.actionFragmentNavRootToFragmentSearchResult(data1 = data1,data2 = data2))

接收数据的Fragment代码

 val args:FragmentSearchResultArgs by navArgs()//kotlin下Fragment的扩展方法

//   "${args.data1?.msg}  ${args.data1?.age}  ${args.data2}"

随时补充

如下代码

<?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"
    app:startDestination="@id/fragmentHome">
    <action
        android:id="@+id/go_StepOne"
        app:destination="@id/fragmentStepOne" />
    <fragment
        android:id="@+id/fragmentHome"
        android:name="com.charliesong.demo0327.navigation.FragmentHome"
        android:label="FragmentHome"
        tools:layout="@layout/nav_fragment_home">

    </fragment>
    <fragment
        android:id="@+id/fragmentStepOne"
        android:name="com.charliesong.demo0327.navigation.FragmentStepOne"
        android:label="FragmentStepOne"
        tools:layout="@layout/nav_fragment_step_one">
        <argument
            android:name="title"
            android:defaultValue="just test" />
        <argument
            android:name="title2"
            android:defaultValue="just test2" />
        <action
            android:id="@+id/fragmentStepOne1"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right"
            app:destination="@id/fragmentStepTwo" />
    </fragment>

    <fragment
        android:id="@+id/fragmentStepOne1"
        android:name="com.charliesong.demo0327.navigation.FragmentStepOne"
        android:label="FragmentStepOne"
        tools:layout="@layout/nav_fragment_step_one">
        <action
            android:id="@+id/go_StepTwo"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right"
            app:destination="@id/fragmentStepTwo" />
    </fragment>

    <fragment
        android:id="@+id/fragmentStepTwo"
        android:name="com.charliesong.demo0327.navigation.FragmentStepTwo"
        android:label="FragmentStepTwo"
        tools:layout="@layout/nav_fragment_step_two">
        <action
            android:id="@+id/go_StepOne"
            app:destination="@id/fragmentStepOne" />
        <action
            android:id="@+id/go_home"
            app:popUpTo="@id/fragmentHome"/>
    </fragment>


</navigation>

说明:action标签可以写在fragment外边,也可以是里边,区别在于。
写在外边,这个id哪里都可以用。写在fragment里边的,只有这个fragment可以用,其他fragment用这个action的id就会报错。
问题:
因为我基类里写了如下的代码,也就是使用了toolbar的后退键

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when(item.itemId){
            android.R.id.home->{
                onBackPressed()
//忘了加 return true了,所以引发了下边的执行了2次后退操作
            }
        }
        return super.onOptionsItemSelected(item)
    }

而现在加了navigation以后,我们的fragment也要拦截后退键的

override fun onSupportNavigateUp(): Boolean = findNavController(my_nav_host_fragment).navigateUp()

然后就发现,点击手机上的物理后退键是没有任何问题了。fragment是正常的一次后退一个。
可点击toolbar上的后退箭头,就发现一次退了至少2个fragment。然后去看下了代码
如下AppCompatActivity里,果然也处理 android.R.id.home,这等于处理了2次。

    public final boolean onMenuItemSelected(int featureId, android.view.MenuItem item) {
        if (super.onMenuItemSelected(featureId, item)) {
      //super的代码在下边,由于忘了写return true了,所以后退执行了2次。
            return true;
        }

        final ActionBar ab = getSupportActionBar();
        if (item.getItemId() == android.R.id.home && ab != null &&
                (ab.getDisplayOptions() & ActionBar.DISPLAY_HOME_AS_UP) != 0) {
            return onSupportNavigateUp();
        }
        return false;
    }

再看下Activity里的代码

    public boolean onMenuItemSelected(int featureId, MenuItem item) {
        CharSequence titleCondensed = item.getTitleCondensed();

        switch (featureId) {
            case Window.FEATURE_OPTIONS_PANEL:
                // Put event logging here so it gets called even if subclass
                // doesn't call through to superclass's implmeentation of each
                // of these methods below
                if(titleCondensed != null) {
                    EventLog.writeEvent(50000, 0, titleCondensed.toString());
                }
                if (onOptionsItemSelected(item)) {
                    return true;
                }

首先Navigation这个工具类

公开的静态方法就是下边这几个


image.png

然后我们看下获取
可以看到,是对我们传入的view,读取getTag来得到的,如果没有,就找view的parent。
需要注意的是,如果没找到,它就直接抛出异常了。所以传的view要确保有


image.png

然后看下设置,就是把controller设置为tag

    public static void setViewNavController(@NonNull View view,
            @Nullable NavController controller) {
        view.setTag(R.id.nav_controller_view_tag, controller);
    }

看看哪里设置的,就是系统的NavHostFragment


image.png

我们在布局里一般这么写的,所以如上图所注释的,我们的controller就是设置给了这个fragment的View拉。

   <fragment
       android:layout_width="match_parent"
       android:layout_height="0dp"
       android:layout_weight="1"
       android:id="@+id/my_nav_host_fragment"
       android:name="androidx.navigation.fragment.NavHostFragment"
       app:navGraph="@navigation/mobile_navigation"
       app:defaultNavHost="true"
       />

另外获取controller也可以通过如下方法
NavHostFragment里有

public static NavController findNavController(@NonNull Fragment fragment)

或者

    public NavController getNavController() {
        if (mNavController == null) {
            throw new IllegalStateException("NavController is not available before onCreate()");
        }
        return mNavController;
    }

需要注意的是controller是在NavHostFragment的onCreate方法里创建的,所以在这个生命周期之前获取不到的。

  @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final Context context = getContext();

        mNavController = new NavController(context);
        mNavController.getNavigatorProvider().addNavigator(createFragmentNavigator());
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容