欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

【Android】Navigation用户导航用法及源码解析

程序员文章站 2022-07-09 19:23:27
本文讲解 Navigation 用法,以及 Navigation 源码解析。官方文档:https://developer.android.google.cn/guide/navigation一句话介绍 Navigation :Navigation是指支持用户导航、进入和退出应用中不同内容片段的交互。Android Jetpack 的Navigation组件可帮助您实现导航,无论是简单的按钮点击,还是应用栏和抽屉式导航栏等更为复杂的模式,该组件均可应对。一、基本用法使......

本文讲解 Navigation 用法,以及 Navigation 源码解析。

官方文档:https://developer.android.google.cn/guide/navigation

一句话介绍 Navigation :Navigation 是指支持用户导航、进入和退出应用中不同内容片段的交互。


Android Jetpack 的 Navigation 组件可帮助您实现导航,无论是简单的按钮点击,还是应用栏和抽屉式导航栏等更为复杂的模式,该组件均可应对。


注:本文使用 Kotlin 编写。


一、基本用法

使用 Navigation 在两个 Fragment 之间相互导航


1. 在 res 里新建一个导航图 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"
    android:id="@+id/nav_graph">

</navigation>

这个过程 Android Studio 会自动帮我们导入必要的库

implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.0'

2. 在 nav_graph 里将布局预览方式切换到 Split ,点击 new Destination 添加两个 Fragment

Android Studio 会自动帮我们生成如下代码:

<?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/fragment1">

    <fragment
        android:id="@+id/fragment1"
        android:name="com.tyhoo.jetpack.navigation.Fragment1"
        android:label="fragment_1"
        tools:layout="@layout/fragment_1" />

    <fragment
        android:id="@+id/fragment2"
        android:name="com.tyhoo.jetpack.navigation.Fragment2"
        android:label="fragment_2"
        tools:layout="@layout/fragment_2" />

</navigation>

可视化界面:

【Android】Navigation用户导航用法及源码解析

3. 在可视化界面,从 fragment1 拖出一条线到 fragment2,这就代表从 Fragment1 导航到 Fragment2 。

【Android】Navigation用户导航用法及源码解析

同时会生成一个带 <action> 标签的代码:

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    ...>

    <fragment
        ...>

        <action
            android:id="@+id/action_fragment1_to_fragment2"
            app:destination="@id/fragment2" />

    </fragment>

    ...

</navigation>

导航图创建好之后需要放到 Activity 里去承载,


4. 在 Activity 中放置一个导航图,放置一个承载导航图的容器

这个容器是 NavHostFragment 或它的子类

<?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=".BasicActivity">

    <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:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/nav_graph" />

</androidx.constraintlayout.widget.ConstraintLayout>

5. 运行程序

【Android】Navigation用户导航用法及源码解析

在 Activity 中放置了一个 NavHostFragment ,NavHostFragment 关联到了导航图的 XML 文件,XML 文件的 startDestination 是 Fragment1 。


6. 修改 Fragment1 的布局文件,并添加一个按钮

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:layout_margin="16dp"
    android:orientation="vertical"
    tools:context=".Fragment1">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="Fragment1"
        android:textSize="24sp" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="16dp"
        android:text="跳转到 Fragment2" />

</LinearLayout>

7. 修改 Fragment1 代码,用 Navigation 的方式从 Fragment1 导航到 Fragment2

class Fragment1 : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_1, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        btn_1.setOnClickListener {
            val controller = it.findNavController()
            controller.navigate(R.id.action_fragment1_to_fragment2)
        }
    }
}

8. 运行程序

在 Fragment1 点击 Button,页面会跳转到 Fragment2 。

此时我们在 Fragment2 点击 Back 键,页面会回到 Fragment1 。Back 的回退栈 Google 已经帮我们实现,不需要我们自己去处理了。


Navigation 的思想:全局的 App 只有一个 Activity ,Activity 作为整体的一个 Controller 去控制界面的切换,所对应的界面就是各个 Fragment 。


这其实也是 JakeWharton 之前提出的一个思想,Google 慢慢的给它实现了。


Q:如果不通过 <action> 标签,就不能导航到 Fragment2 吗?

A:是可以的。

class Fragment1 : Fragment() {

    ...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...

        btn_1.setOnClickListener {
            ...
            controller.navigate(R.id.fragment2)
        }
    }
}

但是不是官方推荐的。如果缺少了 <action> 标签,在可视化界面就没有效果了。所以还是推荐使用 <action> 去链接。


9. 从 Fragment2 点击 Button 回到 Fragment1

布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:layout_margin="16dp"
    android:orientation="vertical"
    tools:context=".Fragment2">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="Fragment2"
        android:textSize="24sp" />

    <Button
        android:id="@+id/btn_2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="16dp"
        android:text="跳转到 Fragment1" />

</LinearLayout>

Fragment2:

class Fragment2 : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_2, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        btn_2.setOnClickListener {
            it.findNavController().popBackStack()
        }
    }
}

10. 运行程序

Fragment1 点击 Button 跳转到 Fragment2 ,在 Fragment2 点击 Button 又回到 Fragment1 ,然后在点击 Back ,程序退出。



二、过渡动画

在导航图 nav_graph 里添加从 Fragment1 跳转到 Fragment2 的过渡动画。

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    ...>

    <fragment
        ...>

        <action
            android:id="@+id/action_fragment1_to_fragment2"
            app:destination="@id/fragment2"
            app:enterAnim="@android:anim/fade_in"
            app:exitAnim="@android:anim/fade_out"
            app:popEnterAnim="@android:anim/slide_in_left"
            app:popExitAnim="@android:anim/slide_out_right" />

    </fragment>

    ...

</navigation>

过渡动画一定要添加到 <action> 里!



三、数据传递

1. 第一种实现方式:

1.1 在 Fragment1 创建一个 bundle ,然后 put 值,然后把 bundle 传给 navigate

class Fragment1 : Fragment() {

    ...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...

        btn_1.setOnClickListener {

            val bundle = Bundle()
            bundle.putString("name", "Navigation")

            val controller = it.findNavController()
            controller.navigate(R.id.action_fragment1_to_fragment2, bundle)
        }
    }
}

1.2 在 Fragment2 上添加一个 TextView ,用来显示从 Fragment1 传递过来的数据

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    ...>

    ...

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="16dp" />

</LinearLayout>

1.3 在 Fragment2 的 onViewCreated 里对 TextView 设置数据

class Fragment2 : Fragment() {

    ...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...

        tv_name.text = arguments?.getString("name")
    }
}

1.4 运行程序,Fragment1 跳转到 Fragment2 的同时,Fragment2 上也显示了 Fragment1 传过来的数据。


但是这种方式的缺点和之前我们用 Fragment 的 newInstance 的缺点是一样的,这些参数都必须维护在代码里。


2. 第二种实现方式:

将这些参数在导航图中定义出来,便于梳理和维护

2.1 在导航图上添加 <argument> 标签

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    ...>

    ...

    <fragment
        ...>

        <argument
            android:name="name"
            android:defaultValue="Android"
            app:argType="string" />

        <argument
            android:name="age"
            android:defaultValue="10"
            app:argType="integer" />

    </fragment>

</navigation>

2.2 删除 Fragment1 添加的代码,保留 Fragment2 添加的代码,并运行程序,发现和第一种实现方式效果一样。


3. 第三种实现方式:

使用 Safe Args 传递安全的数据。

官方文档:https://developer.android.google.cn/guide/navigation/navigation-pass-data


在* build.gradle 文件中包含以下 classpath

buildscript {
    ...

    dependencies {
        ...

        def nav_version = "2.3.0-alpha01"
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
    }
}

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

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

Rebuild 之后,会在工程里看到 插件帮我们生成的类

【Android】Navigation用户导航用法及源码解析

改造 Fragament1:

class Fragment1 : Fragment() {

    ...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...

        btn_1.setOnClickListener {
            val action = Fragment1Directions.actionFragment1ToFragment2("Navigation")
            val controller = it.findNavController()
            controller.navigate(action)
        }
    }
}

这个插件的作用是帮我们省去了很多有风险的代码。


改造 Fragment2:

class Fragment2 : Fragment() {

    ...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...

        val args = arguments?.let { Fragment2Args.fromBundle(it) }
        tv_name.text = args?.name
    }
}


运行程序,效果一样,但是更安全。



四、DeepLink

官方文档:https://developer.android.google.cn/guide/navigation/navigation-deep-link


1. 在导航图添加 deep link

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    ...>

    ...

    <fragment
        ...>

        ...

        <deepLink
            android:id="@+id/deepLink"
            android:autoVerify="true"
            app:uri="http://aaa.bbb.ccc/fragment2" />

    </fragment>

</navigation>

2. 修改 Manifest

找到导航图所在的 Activity 的标签,把导航图的加入到里面

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.tyhoo.jetpack.navigation">

    <application
        ...>

        <activity android:name=".BasicActivity">
            ...

            <nav-graph android:value="@navigation/nav_graph" />

        </activity>

    </application>

</manifest>

3. 运行程序

4. 编写一个 Html 文件

<!DOCTYPE html>
<!DOCTYPE html>
<html>
<head>
    <title>跳转测试</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
</head>
<body>
    <a href="http://aaa.bbb.ccc/fragment2" style="font-size: 60px">点击跳转测试</a>
</body>
</html>

5. 在手机的浏览器里执行 Html 文件

【Android】Navigation用户导航用法及源码解析

6. 点击跳转

【Android】Navigation用户导航用法及源码解析

会直接跳转到 Fragment2 ,点击 Back 键会回到 Fragment1 。


7. 在 Html 里添加参数

<!DOCTYPE html>
<!DOCTYPE html>
<html>
<head>
    <title>跳转测试</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
</head>
<body>
    <a href="http://aaa.bbb.ccc/fragment2/Android" style="font-size: 60px">点击跳转测试</a>
</body>
</html>

8. 在 App 中获取 Html 传来的数据

在导航图中使用占位符{},这个占位符要和 <argument> 的 name 相对应。

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    ...>

    ...

    <fragment
        ...>

        <argument
            android:name="name"
            android:defaultValue="Android"
            app:argType="string" />

        <deepLink
            android:id="@+id/deepLink"
            android:autoVerify="true"
            app:uri="http://aaa.bbb.ccc/fragment2/{name}" />

    </fragment>

</navigation>

9. 运行程序,效果和期待的一样。


在 Android 8.0 的时候添加了 Shortcut ,我们可以结合 Deep Link ,在桌面长按 App 图标,跳转到相应页面。

在通知栏,使用 Deep Link ,可以很好的解决日常难题。比如说我们点击通知栏的消息跳转到相应页面,然后点击 Back 键的时候回退到路径也要求是正确的,这中间要做好多处理,加入 Deep Link 之后,这些问题就不是问题了。



五、与其他导航组件的交互

例1:

1. 将 Navigation 和 ActionBar 关联

android {
    ...

    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8.toString()
    }
}
class BasicActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_basic)

        // 在当前的 Activity 找到 Fragment 的 Controller
        var controller: NavController = findNavController(R.id.fragment)
        // 创建一个 AppBar 的配置对象
        val configuration = AppBarConfiguration(controller.graph)
        // 将 controller 和 configuration 关联到 ActionBar 上
        setupActionBarWithNavController(controller, configuration)
    }
}

2. 运行程序

【Android】Navigation用户导航用法及源码解析

【Android】Navigation用户导航用法及源码解析

发现标题已经关联到对应的 Fragment 的 label 字段,Fragment2 自动生成了一个 Back 键,但是 Back 键点击无效。

说明导航图已经和 ActionBar 进行了关联。


3. 给 Back 键设置事件

class BasicActivity : AppCompatActivity() {

    ...

    override fun onSupportNavigateUp(): Boolean {
        var controller: NavController = findNavController(R.id.fragment)
        return controller.navigateUp() || super.onSupportNavigateUp()
    }
}

4. 运行程序

Back 键和预期效果一样。


5. 监听当前导航目的地

class BasicActivity : AppCompatActivity() {

    val TAG = "BasicActivity"

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // 通过 controller 监听当前导航目的地
        controller.addOnDestinationChangedListener { controller, destination, arguments ->
            Log.d(TAG, "destination: $destination")
            Log.d(TAG, "arguments: $arguments")
        }
    }

    ...
}

6. 运行程序,查看 Log

App 启动,打印的 Log 如下:

D/BasicActivity: destination: Destination(com.tyhoo.jetpack.navigation:id/fragment1) label=fragment_1 class=com.tyhoo.jetpack.navigation.basic.Fragment1
D/BasicActivity: arguments: null

跳转到 Fragment2 ,打印的 Log 如下:

D/BasicActivity: destination: Destination(com.tyhoo.jetpack.navigation:id/fragment2) label=fragment_2 class=com.tyhoo.jetpack.navigation.basic.Fragment2
D/BasicActivity: arguments: Bundle[{name=Navigation}] 


所以,通过 controller 的回调可以关联到任何的外部事件。

例如:

在 Activity 的布局里添加一个 Toolbar ,并关联:

class BasicActivity : AppCompatActivity() {

    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        // 关联 Toolbar
        toolbar.setupWithNavController(controller, configuration)
    }

    ...
}

运行程序:

【Android】Navigation用户导航用法及源码解析

【Android】Navigation用户导航用法及源码解析

发现 Navigation 已经和 Toolbar 进行了关联,并且 Toolbar 自带的动画效果要比 ActionBar 好看。


例2:

先拿 Fragment 和 Navigation 做一个对比

Fragment:

  • Fragment 事务处理麻烦,容易出错
  • 可读性差
  • 可复用性差(业务与视图耦合)

Navigation:

  • 代码简洁
  • 可读性高
  • 充分解耦(业务与视图隔离)


将 Navigation 和 Toolbar、fragment、BottomNavigationView 关联:

<?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=".advance.AdvanceActivity">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?attr/colorPrimary"
        android:minHeight="?attr/actionBarSize"
        android:theme="?attr/actionBarTheme"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toTopOf="@+id/bottom_nav_view"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/toolbar"
        app:navGraph="@navigation/nav_graph_advance" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_nav_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:background="?android:attr/windowBackground"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:menu="@menu/bottom_view" />

</androidx.constraintlayout.widget.ConstraintLayout>
<?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_advance"
    app:startDestination="@id/first">

    <fragment
        android:id="@+id/first"
        android:name="com.tyhoo.jetpack.navigation.advance.FirstFragment"
        android:label="Fragment first"
        tools:layout="@layout/fragment_first" />

    <fragment
        android:id="@+id/second"
        android:name="com.tyhoo.jetpack.navigation.advance.SecondFragment"
        android:label="Fragment second"
        tools:layout="@layout/fragment_second" />

    <fragment
        android:id="@+id/third"
        android:name="com.tyhoo.jetpack.navigation.advance.ThirdFragment"
        android:label="Fragment third"
        tools:layout="@layout/fragment_third" />

</navigation>
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/first"
        android:icon="@mipmap/ic_launcher"
        android:title="First" />

    <item
        android:id="@+id/second"
        android:icon="@mipmap/ic_launcher"
        android:title="Second" />

    <item
        android:id="@+id/third"
        android:icon="@mipmap/ic_launcher"
        android:title="Third" />

</menu>

注意:如果你 menu 中 item 的 id 和导航图自定义的 fragment 的 id 不一致的话是无法跳转的,因为 id 不对应。

class AdvanceActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_advance)

        val navView: BottomNavigationView = findViewById(R.id.bottom_nav_view)
        val navController = findNavController(R.id.nav_host_fragment)
        val appBarConfiguration = AppBarConfiguration(
            setOf(R.id.first, R.id.second, R.id.third)
        )

        toolbar.setupWithNavController(navController, appBarConfiguration)
        NavigationUI.setupWithNavController(navView, navController)
    }
}

运行程序,就实现了底部导航切换。

【Android】Navigation用户导航用法及源码解析



六、源码解析

底部导航切换使用 navigate 方式实现,但是有个缺点,在 navigate 中每个 Fragment 都是重新实例化的,但是有时不需要它实例化,以前的实现方式是对 Fragment hide show 来进行切换。阅读源码发现默认使用 replace ,所以会重新实例化。

看 NavigationUI 源码:

public static boolean onNavDestinationSelected(@NonNull MenuItem item, @NonNull NavController navController) {
    ...

    try {
        navController.navigate(item.getItemId(), null, options);
        return true;
    } catch(IllegalArgumentException e) {
        return false;
    }
}
public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions) {
    navigate(resId, args, navOptions, null);
}

public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
    // 拿到当前的节点对应的 NavDestination (这个 NavDestination 内部有一些属性,代表的就是导航图里的每个标签)
    NavDestination currentNode = mBackStack.isEmpty() ? mGraph: mBackStack.getLast().getDestination();
    if (currentNode == null) {
        throw new IllegalStateException("no current navigation node");
    }

    @IdRes int destId = resId;

    // 拿到当前的 action
    final NavAction navAction = currentNode.getAction(resId);

    // 去拼接 args
    Bundle combinedArgs = null;
    if (navAction != null) {
        if (navOptions == null) {
            navOptions = navAction.getNavOptions();
        }

        // 从 navAction 中会获取到 destId 
        destId = navAction.getDestinationId();
        Bundle navActionArgs = navAction.getDefaultArguments();
        if (navActionArgs != null) {
            combinedArgs = new Bundle();
            combinedArgs.putAll(navActionArgs);
        }
    }

    if (args != null) {
        if (combinedArgs == null) {
            combinedArgs = new Bundle();
        }
        combinedArgs.putAll(args);
    }

    // 弹栈操作,也就是返回了    
    if (destId == 0 && navOptions != null && navOptions.getPopUpTo() != -1) {
        popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
        return;
    }

    if (destId == 0) {
        throw new IllegalArgumentException("Destination id == 0 can only be used" + " in conjunction with a valid navOptions.popUpTo");
    }

    // 找到下一个节点
    NavDestination node = findDestination(destId);
    if (node == null) {
        ...
    }

    navigate(node, combinedArgs, navOptions, navigatorExtras);
}
private void navigate(@NonNull NavDestination node, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        boolean popped = false;
        boolean launchSingleTop = false;

        // 做了一些 Check
        if (navOptions != null) {
            if (navOptions.getPopUpTo() != -1) {
                popped = popBackStackInternal(navOptions.getPopUpTo(),
                        navOptions.isPopUpToInclusive());
            }
        }

        // mNavigatorProvider 对应的是一个 navigator 的容器,通过 node.getNavigatorName() 去获取相应的容器
        // 如果跳转的目的地是 Fragment ,就返回 Fragment navigator
        // 如果跳转的目的地是 Activity ,就返回 Activity navigator
        // ...
        // node.getNavigatorName() 实际上就是导航图的标签名
        Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(node.getNavigatorName());

        // 把 args 组装起来
        Bundle finalArgs = node.addInDefaultArgs(args);

        NavDestination newDest = navigator.navigate(node, finalArgs,
                navOptions, navigatorExtras);

        ...
    }

以 ActivityNavigator 为例,探索它是何时放到容器中的。

在 NavController 的构造方法中:

public NavController(@NonNull Context context) {
    ...
    mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
    mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
}

在初始化的时候会把 ActivityNavigator 传进来,在 add 的过程中

public final Navigator < ?extends NavDestination > addNavigator(@NonNull Navigator < ?extends NavDestination > navigator) {
    String name = getNameForNavigator(navigator.getClass());

    return addNavigator(name, navigator);
}

通过函数得到 name ,这个 name 就是导航图里的 <activity> 。

以 name 为 key,自己为 value ,放到一个 HashMap 中

public Navigator < ?extends NavDestination > addNavigator(@NonNull String name, @NonNull Navigator < ?extends NavDestination > navigator) {
    ...
    return mNavigators.put(name, navigator);
}

所以在 NavController 的 navigate 函数中通过之前的 get 就会得到 Activity 的 navigator 。


看 ActivityNavigator.navigate() 函数:

public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
    ...

    Intent intent = new Intent(destination.getIntent());
    
    ...

    if (navigatorExtras instanceof Extras) {
        ...

        if (activityOptions != null) {
            ActivityCompat.startActivity(mContext, intent, activityOptions.toBundle());
        } else {
            mContext.startActivity(intent);
        }
    } else {
        mContext.startActivity(intent);
    }
    
    ...
}

通过 destination 和其他可选参数来拼装了一个新的 Intent ,最后会添加一个启动方式 startActivity 和启动动画。


同理,FragmentNavigator 也是用类似方式:

public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
    ...

    ft.replace(mContainerId, frag);
    
    ...
}

通过 replace 进行了 Fragment 切换。


针对底部导航切换使用 navigate 方式实现,会导致 Fragment 重建,可以自定义自己的 Navigator ,把 replace 改成 show 或 hide 的方式来实现需求。


NavigationController 原理:

【Android】Navigation用户导航用法及源码解析


如果本文对你有帮助,请多支持!!!

本文地址:https://blog.csdn.net/cnwutianhao/article/details/107807024

相关标签: Jetpack android