本文讲解 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>
可视化界面:
3. 在可视化界面,从 fragment1 拖出一条线到 fragment2,这就代表从 Fragment1 导航到 Fragment2 。
同时会生成一个带 <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. 运行程序
在 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 之后,会在工程里看到 插件帮我们生成的类
改造 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 文件
6. 点击跳转
会直接跳转到 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. 运行程序
发现标题已经关联到对应的 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)
}
...
}
运行程序:
发现 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)
}
}
运行程序,就实现了底部导航切换。
六、源码解析
底部导航切换使用 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 原理:
如果本文对你有帮助,请点赞支持!!!
来源:oschina
链接:https://my.oschina.net/u/4284321/blog/4480018