简介
Navigation组件用于管理应用内的页面跳转,它默认能支持Activity、Fragment、DialogFragment之间的跳转,但是一般被用来处理Fragment之间的跳转问题,google推出它的目的也是让单 Activity 应用成为首选架构。 使用Navigation组件可以屏蔽处理 FragmentTransaction 的复杂性,自动处理跳转和返回栈问题,此外还支持deepLink,并提供了帮助程序,用于将导航关联到合适的 UI 小部件,例如抽屉式导航栏和底部导航。
简单使用
- 导入依赖
val nav_version = "2.3.5"
implementation("androidx.navigation:navigation-fragment-ktx:$nav_version")
implementation("androidx.navigation:navigation-ui-ktx:$nav_version")
- 在res目录下新建navigation文件夹,在文件夹下新建导航的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/mobile_navigation"
app:startDestination="@+id/navigation_home">
<fragment
android:id="@+id/navigation_home"
android:name="com.dean.playerdemo.ui.home.HomeFragment"
android:label="@string/title_home"/>
<activity
android:id="@+id/play_activity"
android:name="com.dean.playerdemo.PlayActivity"
android:label="PlayActivity"/>
<dialog
android:id="@+id/exit_dialog"
android:name="com.dean.playerdemo.ExitDialogFragment"
android:label="ExitDialogFragment"/>
</navigation>
- 在Activity的布局文件中添加页面容器,并用navGraph指定导航文件
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/mobile_navigation" />
5、 现在直接运行就可以了,现在默认会加载startDestination指定的HomeFragment。
标签详解
fragment、activity、dialog 三个标签下可以使用其他标签用来指定跳转所需参数,页面内跳转事件、deeplink等信息。 这三个标签的具体使用和管理应用内的页面都需要通过NavController,所以先要获取NavController。
val navController = findNavController()
NavController navController = Navigation.findNavController(getView());
val navController = findNavController(R.id.nav_host_fragment)
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
跳转事件 <action>
使用NavController.navigate跳转时,可以直接传入一个已经定义好的具体的页面:
navController.navigate(R.id.play_activity)
但是最好还是使用action来跳转,这样便于管理和添加动画等效果,action可以分为两类
- 全局action,在当前graph中任意页面都可以使用
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/navigation_home">
......
<action
android:id="@+id/global_to_no_internet_tip"
app:destination="@id/network_tip_fragment" />
......
</navigation>
- 局部action,只能在定义该action的页面内使用:
定义action:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/navigation_home">
......
<fragment
android:id="@+id/navigation_home"
android:name="com.dean.playerdemo.ui.home.HomeFragment"
android:label="@string/title_home">
<action
android:id="@+id/homefragment_action_to_play"
app:destination="@id/play_activity" />
</fragment>
<activity
android:id="@+id/play_activity"
android:name="com.dean.playerdemo.PlayActivity"
android:label="PlayActivity"/>
......
</navigation>
跳转(只能在HomeFragment内使用):
findNavController().navigate(R.id.homefragment_action_to_play)
action 标签有很多其他的属性,想要了解这些属性,需要先创造一个虚拟场景,假设有ABC三个页面,现在已经在栈中的页面是A和B,现在定义一个B跳转到C的Action:
<action
android:id="@+id/page_b_action_to_page_c"
app:destination="@id/Page_C"
app:enterAnim="@anim/fragment_close_enter"
app:exitAnim="@anim/fragment_close_exit"
app:popUpTo="@id/Page_A"
app:popUpToInclusive="true"
app:popEnterAnim="@anim/fragment_close_exit"
app:popExitAnim="@anim/fragment_fade_enter"
app:launchSingleTop="true" />
- popUpTo 和 popUpToInclusive
popUpTo指定出栈时回到哪个页面,这边写的是Page_A,那么Page_C出栈时会直接回到Page_A,Page_B跳转到Page_C后的栈内元素是AC。 popUpToInclusive用来判断到达指定元素时是否把指定元素也出栈,默认是false,如果设置为True,栈内元素就只剩下C,A也会出栈,这一般可以用来清空返回栈。 - enterAnim 和 popExitAnim
enterAnim是action目的地进入的动画,即Page_C的入场动画 popExitAnim是action目的地离开的动画,即Page_C的出场动画 - exitAnim 和 popEnterAnim
exitAnim是action所在元素离开的动画,即Page_B的出场动画 popEnterAnim是action所在元素进入的动画,即Page_B的入场动画 - launchSingleTop
效果类似于Activity的SingleTop,栈顶复用模式
指定参数 <argument>
argument用来指定跳转到该页面时可以携带的参数
<argument
android:name="channelId"
android:defaultValue="ch000009507"
app:argType="string" />
- name:指定参数的名称
- defaultValue:参数的默认值
- argType:参数类型,可以是integer、float、long、boolean、string、资源和自定义的类。
传参以及使用:
跳转时带入参数:
findNavController().navigate(R.id.homefragment_action_to_play, Bundle().apply {
putString("channelId", "ch0000020110816")
})
跳转后使用:
getArguments().getString("channelId")
深度链接 <deepLink>
深层链接指直接转到应用内特定目的地的链接,一般是从应用外部跳转时使用,深层链接可分为两种:
- 显式深层链接:一般用于通知或应用微件中
val pendingIntent = navController.createDeepLink()
.setGraph(R.navigation.navigation_settings)
.setDestination(R.id.page_d)
.setArguments(args)
.setComponentName(SettingsActivity::class.java)
.createPendingIntent()
如果没有navController实例,还可以通过NavDeepLinkBuilder(this)来创建PendingIntent:
val pendingIntent = NavDeepLinkBuilder(this)
.setGraph(R.navigation.navigation_settings)
.setDestination(R.id.page_d)
.setArguments(args)
.setComponentName(SettingsActivity::class.java)
.createPendingIntent()
- 隐式深层链接:用于其他如分享的链接、其他应用跳转等场景:
首先为页面设置deeplink条件:
<fragment
android:id="@+id/fragment_page_d"
android:name="com.dean.playerdemo.ui.settings.PageDFragment"
tools:layout="PageDFragment">
<deepLink app:uri="http://smart/app/settings/page/d"/>
</fragment>
其次在Activity中申明能够处理的deeplink
<activity android:name=".NavTestActvity">
<nav-graph android:value="@navigation/navigation_settings"/>
</activity>
这样在跳转时只要intent附带的data是 Page_D能够处理的,就能直接跳转到D:
startActivity(Intent().apply {
data = Uri.parse("http://smart/app/settings/page/d")
})
其原理大致是 当 nav-graph 设置到对应的activity中时,会自动解析 里面所包含的deeplink并且附加到该activity的 行为当中,其效果相当于为Activity直接申明 intent-filter。这样使用隐式跳转时就能定位到所需的activity,至于能够定位到Page_D,则还是通过NavController,上面代码中没有出现的原因是系统帮我们默认处理了,但是只有Activity启动模式是standard时才会自动处理,如果不是standard启动模式,则还需手动处理一下,在Activity的onNewIntent中添加:
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
findNavController(R.id.nav_host_fragment).handleDeepLink(intent)
}
NavController其他常用api
NavController:导航控制器,它负责操作Navigation框架下的Fragment的跳转与退出、动画、监听当前Fragment信息,下面看看它的一些常用api:
- setGraph:一般项目中为了便于管理,不同业务的页面使用的Navigation是分开的,比如设置功能会有个navigation_settings.xml文件,里面放置设置里面会有的页面;新闻功能会有个navigation_news.xml文件,里面放置新闻功能涉及的页面。而切换Navigation就是使用setGraph
controller.setGraph(R.navigation.navigation_settings)
controller.setGraph(R.navigation.navigation_news)
- popBackStack:当前Fragment出栈,返回true代表返回成功,假设当前栈中有ABCD四个页面
如果调用popBackStack(),D出栈,此时栈中剩下ABC,C显示出来
controller.popBackStack()
如果调用popBackStack(R.id.page_b, false),CD出栈,B显示出来
controller.popBackStack(R.id.page_b, false)
如果调用popBackStack(R.id.page_b, true),BCD出栈,A显示出来
controller.popBackStack(R.id.page_b, true)
- navigateUp:功能和popBackStack类似,也是当前页面出栈,不过效果不同,navigateUp多了层保护,popBackStack() 如果当前的返回栈是空的就会报错,因为栈是空的了,navigateUp() 则不会,还是停留在当前界面
controller.navigateUp()
- getCurrentDestination:获取当前目的地的信息
controller.getCurrentDestination();
Log.d(TAG, "onCreate: NavigatorName = ${navDestination.getNavigatorName()}");
Log.d(TAG, "onCreate: id = ${navDestination.getId()}");
Log.d(TAG, "onCreate: Parent = ${navDestination.getParent()}");
- addOnDestinationChangedListener:设置监听,用来监听NavController导航页面变更信息
navController.addOnDestinationChangedListener { controller, destination, arguments ->
...
}
- getNavInflater:可以根据资源文件来获取到NavGraph 对象,然后就可以动态添加页面或者删除
val settingsGraph = navController.navInflater.inflate(R.navigation.navigation_settings)
settingsGraph.addDestination(newDestination)
settingsGraph.remove(oldDestination)
原理浅析
Android Jectpack 中大部分架构组件都能自带生命周期处理,这是这系列组件的优势,能让开发者放心使用而不需要额外手动处理页面生命周期带来的问题。纵观这些架构组件源码,不难发现都是通过一些特殊的fragment完成这件事,比如LifeCycle是通过ReportFragment 来完成的,Navigation 也不例外,它的入口代码在NavHostFragment中,首先查看它的oncreate方法:
public void onCreate(@Nullable Bundle savedInstanceState) {
......
mNavController = new NavHostController(context);
......
onCreateNavController(mNavController);
......
}
摘取出来的这两行代码可以说是主要干了一件事,就是声明该NavHostController能够控制的页面类型。
public NavController(@NonNull Context context) {
.......
mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
}
protected void onCreateNavController(@NonNull NavController navController) {
navController.getNavigatorProvider().addNavigator(
new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}
而声明该NavHostController能够控制的页面类型就是将对应类型的Navigator添加到NavHostController的NavigatorProvider中。可以看到添加了四种Navigator:NavGraphNavigator、ActivityNavigator、DialogFragmentNavigator、FragmentNavigator。 这和之前说的navigation默认处理三种页面导航不太一样,其实NavGraphNavigator就是之前的资源文件中的根标签navigation,它的主要作用只有一个,就是加载startDestination指定的页面,它是找出startDestination指定的页面的具体类型,最后还是通过相应类型的Navigator来实现加载。 这四种Navigator都是抽象类Navigator的子类  以FragmentNavigator为例,查看FragmentNavigator类的声明:
@Navigator.Name("fragment")
public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> {
.......
}
在实现Navigator时需要通过注解Name来标识当前Navigator能够处理的页面名称,这个Name很重要,它是一个纽带。首先当FragmentNavigator被添加到NavHostController的NavigatorProvider中时,其实是加到一个HashMap中的,而这个hashmap使用的key就是Name注解中的值 “fragment”:
public class NavigatorProvider {
......
private final HashMap<String, Navigator<? extends NavDestination>> mNavigators =
new HashMap<>();
......
}
其次是之前xml中定义的fragment标签在解析完成后会生成NavDestination对象,而对象中的属性mNavigatorName就保存了该节点名称 “fragment”:
public class NavDestination {
......
private final String mNavigatorName;
private NavGraph mParent;
private int mId;
private String mIdName;
private CharSequence mLabel;
private ArrayList<NavDeepLink> mDeepLinks;
private SparseArrayCompat<NavAction> mActions;
private HashMap<String, NavArgument> mArguments;
......
}
最后在调用NavHostController的跳转方法时就是使用mNavigatorName在NavigatorProvider的HashMap中查找,找到后就调用相应的Navigator的navigate方法来完成跳转。
public class NavController {
.......
private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
.......
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
node.getNavigatorName());
Bundle finalArgs = node.addInDefaultArgs(args);
NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);
.......
}
.......
}
自此navigation的简单原理就是这样,下面图便于理解: 
实战中fragment 默认replace问题修改
使用Navigation来跳转Fragment时会发现每次都是替换之前的Fragment,每次跳转时原来的Fragment的view都会被销毁,当前的Fragment是新创建出来的:
D/smartFragments: PageAFragment onCreateView
D/smartFragments: PageBFragment onCreateView
D/smartFragments: PageAFragment onDestroyView
D/smartFragments: PageCFragment onCreateView
D/smartFragments: PageBFragment onDestroyView
之前知道Fragment的跳转是通过FragmentNavigator 来完成的,来查看FragmentNavigator 的navigate方法就能找到该问题的原因所在:
@Navigator.Name("fragment")
public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> {
@Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
......
final FragmentTransaction ft = mFragmentManager.beginTransaction();
......
ft.replace(mContainerId, frag);
......
ft.commit();
}
}
Navigation虽然帮助开发者屏蔽处理 FragmentTransaction 的复杂性,但是默认用的是replace来替换之前的Fragment,这与大部分实际项目需求不太符合,下面是自定义Fragment的Navigator,来替换系统默认的FragmentNavigator。
- 新建一个用于Fragment跳转的Navigator,它继承自FragmentNavigator:
@Navigator.Name("smartFragment")
class SmartFragmentNavigator(private val mContext: Context,
private val mFragmentManager: FragmentManager,
private val mContainerId: Int)
: FragmentNavigator(mContext, mFragmentManager, mContainerId) {
}
- 重写navigate方法,其中大部分是从FragmentNavigator拷贝来的,只修改replace涉及的必要部分:
override fun navigate(
destination: Destination, args: Bundle?,
navOptions: NavOptions?, navigatorExtras: Navigator.Extras?
): NavDestination? {
...
val currentFragment =
mFragmentManager.primaryNavigationFragment
if (currentFragment != null) {
ft.hide(currentFragment)
}
var needShowFragment =
mFragmentManager.findFragmentByTag(destination.id.toString())
if (needShowFragment != null) {
ft.show(needShowFragment)
} else {
needShowFragment = instantiateFragment(
mContext, mFragmentManager,
className, args
)
needShowFragment.arguments = args
ft.add(mContainerId, needShowFragment)
}
ft.setPrimaryNavigationFragment(needShowFragment)
...
}
- 点击Android studio》Build》Make Project,然后就可以在xml中使用了,将之前的所有fragment标签都替换成smartFragment
<?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/mobile_navigation"
app:startDestination="@+id/fragment_page_a">
<smartFragment
android:id="@+id/fragment_page_a"
android:name="com.dean.playerdemo.ui.settings.PageAFragment"
tools:layout="PageAFragment">
<action
android:id="@+id/action_fragment_page_a_to_fragment_page_b"
app:destination="@id/fragment_page_b" />
</smartFragment>
<smartFragment
android:id="@+id/fragment_page_b"
android:name="com.dean.playerdemo.ui.settings.PageBFragment"
tools:layout="PageBFragment">
<action
android:id="@+id/action_fragment_page_b_to_fragment_page_c"
app:destination="@id/fragment_page_c" />
</smartFragment>
<smartFragment
android:id="@+id/fragment_page_c"
android:name="com.dean.playerdemo.ui.settings.PageCFragment"
tools:layout="PageCFragment">
<action
android:id="@+id/action_fragment_page_c_to_fragment_page_d"
app:destination="@id/fragment_page_d" />
</smartFragment>
<smartFragment
android:id="@+id/fragment_page_d"
android:name="com.dean.playerdemo.ui.settings.PageDFragment"
tools:layout="PageDFragment">
<deepLink app:uri="http://smart/app/settings/page/d"/>
</smartFragment>
</navigation>
- 将新定义的Navigator添加至NavController中:
val navController = findNavController(R.id.nav_host_fragment)
navController.navigatorProvider.addNavigator("smartFragment",
SmartFragmentNavigator(this,
supportFragmentManager.findFragmentById(R.id.nav_host_fragment)!!.childFragmentManager,
R.id.nav_host_fragment)
)
navController.setGraph(R.navigation.navigation_settings)
- 运行结果
D/smartFragments: PageAFragment onCreateView
D/smartFragments: PageBFragment onCreateView
D/smartFragments: PageCFragment onCreateView
D/smartFragments: PageDFragment onCreateView
最后附上自定义Navigator全部源码:
package com.dean.playerdemo.utils
import android.content.Context
import android.os.Bundle
import android.util.Log
import androidx.annotation.IdRes
import androidx.fragment.app.FragmentManager
import androidx.navigation.NavDestination
import androidx.navigation.NavOptions
import androidx.navigation.Navigator
import androidx.navigation.fragment.FragmentNavigator
import java.util.*
@Navigator.Name("smartFragment")
class SmartFragmentNavigator(private val mContext: Context,
private val mFragmentManager: FragmentManager,
private val mContainerId: Int)
: FragmentNavigator(mContext, mFragmentManager, mContainerId) {
val TAG = SmartFragmentNavigator::class.java.simpleName
override fun navigate(
destination: Destination, args: Bundle?,
navOptions: NavOptions?, navigatorExtras: Navigator.Extras?
): NavDestination? {
if (mFragmentManager.isStateSaved) {
Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
+ " saved its state")
return null
}
var mBackStack: ArrayDeque<Int>? = null
try {
val field =
FragmentNavigator::class.java.getDeclaredField("mBackStack")
field.isAccessible = true
mBackStack = field[this] as ArrayDeque<Int>
} catch (e: Exception) {
e.printStackTrace()
}
var className = destination.className
if (className[0] == '.') {
className = mContext.packageName + className
}
val ft = mFragmentManager.beginTransaction()
var enterAnim = navOptions?.enterAnim ?: -1
var exitAnim = navOptions?.exitAnim ?: -1
var popEnterAnim = navOptions?.popEnterAnim ?: -1
var popExitAnim = navOptions?.popExitAnim ?: -1
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = if (enterAnim != -1) enterAnim else 0
exitAnim = if (exitAnim != -1) exitAnim else 0
popEnterAnim = if (popEnterAnim != -1) popEnterAnim else 0
popExitAnim = if (popExitAnim != -1) popExitAnim else 0
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
}
val currentFragment =
mFragmentManager.primaryNavigationFragment
if (currentFragment != null) {
ft.hide(currentFragment)
}
var needShowFragment =
mFragmentManager.findFragmentByTag(destination.id.toString())
if (needShowFragment != null) {
ft.show(needShowFragment)
} else {
needShowFragment = instantiateFragment(
mContext, mFragmentManager,
className, args
)
needShowFragment.arguments = args
ft.add(mContainerId, needShowFragment)
}
ft.setPrimaryNavigationFragment(needShowFragment)
@IdRes val destId = destination.id
val initialNavigation = mBackStack!!.isEmpty()
val isSingleTopReplacement = (navOptions != null && !initialNavigation
&& navOptions.shouldLaunchSingleTop()
&& mBackStack.peekLast() == destId)
val isAdded: Boolean
isAdded = if (initialNavigation) {
true
} else if (isSingleTopReplacement) {
if (mBackStack.size > 1) {
mFragmentManager.popBackStack(
generateBackStackName(mBackStack.size, mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE
)
ft.addToBackStack(generateBackStackName(mBackStack.size, destId))
}
false
} else {
ft.addToBackStack(generateBackStackName(mBackStack.size + 1, destId))
true
}
if (navigatorExtras is Extras) {
for ((key, value) in navigatorExtras.sharedElements) {
ft.addSharedElement(key!!, value!!)
}
}
ft.setReorderingAllowed(true)
ft.commit()
return if (isAdded) {
mBackStack.add(destId)
destination
} else {
null
}
}
private fun generateBackStackName(backStackIndex: Int, destId: Int): String? {
return "$backStackIndex-$destId"
}
}
|