本文讲解 Navigation 用法,以及 Navigation 源码解析。
官方文档:https://developer.android.google.cn/guide/navigation
一句话介绍 Navigation :Navigation 是指支持用户导航、进入和退出应用中不同内容片段的交互。
Android Jetpack 的 Navigation 组件可帮助您实现导航,无论是简单的按钮点击,还是应用栏和抽屉式导航栏等更为复杂的模式,该组件均可应对。
注:本文使用 Kotlin 编写。
使用 Navigation 在两个 Fragment 之间相互导航
1. 在 res 里新建一个导航图 nav_graph.xml
这个过程 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 会自动帮我们生成如下代码:
可视化界面:
3. 在可视化界面,从 fragment1 拖出一条线到 fragment2,这就代表从 Fragment1 导航到 Fragment2 。
同时会生成一个带
...
导航图创建好之后需要放到 Activity 里去承载,
4. 在 Activity 中放置一个导航图,放置一个承载导航图的容器
这个容器是 NavHostFragment 或它的子类
5. 运行程序
在 Activity 中放置了一个 NavHostFragment ,NavHostFragment 关联到了导航图的 XML 文件,XML 文件的 startDestination 是 Fragment1 。
6. 修改 Fragment1 的布局文件,并添加一个按钮
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:如果不通过
A:是可以的。
class Fragment1 : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
btn_1.setOnClickListener {
...
controller.navigate(R.id.fragment2)
}
}
}
但是不是官方推荐的。如果缺少了
9. 从 Fragment2 点击 Button 回到 Fragment1
布局:
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 的过渡动画。
...
过渡动画一定要添加到
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 传递过来的数据
...
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 在导航图上添加
...
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
}
}
运行程序,效果一样,但是更安全。
官方文档:https://developer.android.google.cn/guide/navigation/navigation-deep-link
1. 在导航图添加 deep link
...
...
2. 修改 Manifest
找到导航图所在的 Activity 的标签,把导航图的加入到里面
...
3. 运行程序
4. 编写一个 Html 文件
跳转测试
点击跳转测试
5. 在手机的浏览器里执行 Html 文件
6. 点击跳转
会直接跳转到 Fragment2 ,点击 Back 键会回到 Fragment1 。
7. 在 Html 里添加参数
跳转测试
点击跳转测试
8. 在 App 中获取 Html 传来的数据
在导航图中使用占位符{},这个占位符要和
...
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:
Navigation:
将 Navigation 和 Toolbar、fragment、BottomNavigationView 关联:
注意:如果你 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 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 就是导航图里的
以 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 原理:
如果本文对你有帮助,请点赞支持!!!