Q: 什么是Navigation?
导航是指允许用户在应用中的不同内容段之间导航、进入和退出的交互。Android Jetpack 的 Navigation 组件可帮助您实现导航,从简单的按钮点击到更复杂的模式,例如应用栏和导航抽屉。
Q:Navigation组件带来的好处?
可视化的页面导航图,便于我们理清页面间的关系。
通过destination和action完成页面间的导航
方便添加页面切换动画
页面间类型安全的参数传递
通过NavigationUI类,对菜单、底部导航、抽屉菜单导航进行统一的管理
支持深层链接DeepLink
Q:popBackStack()和navigateUp()的区别?
public boolean navigateUp() {
if (getDestinationCountOnBackStack() == 1) {
// If there's only one entry, then we've deep linked into a specific destination
// on another task so we need to find the parent and start our task from there
NavDestination currentDestination = getCurrentDestination();
int destId = currentDestination.getId();
NavGraph parent = currentDestination.getParent();
while (parent != null) {
if (parent.getStartDestination() != destId) {
//省略部分代码
return true;
}
destId = parent.getId();
parent = parent.getParent();
}
// We're already at the startDestination of the graph so there's no 'Up' to go to
return false;
} else {
return popBackStack();
}
}
//获取实际目的地个数
private int getDestinationCountOnBackStack() {
int count = 0;
for (NavBackStackEntry entry : mBackStack) {
if (!(entry.getDestination() instanceof NavGraph)) {
count++;
}
}
return count;
}
从源码可以看出,
不同的是 navigateUp 在返回前会先检查 当前返回栈是否存在多余一个的 实际的目的地,也就是 NavDestination 而非虚拟的 NavGraph。
如果大于1,则直接执行 popBackStack;若等于 1,则判断 是否当前返回栈就是 其 NavGraph 的起始目的地,如果是 则说明该返回栈已经空了,什么都不做;反之,说明是通过 deeplink 跳转的过来的,此时会退出当前的 Activity,并且以之前 intent 跳转参数重新启动 Activity。
当栈中只有一个导航首页(start destination)的时候,navigateUp()不会弹出导航首页,它什么都不做,直接返回false. popBackStack则会把导航首页也出栈,但是由于没有回退到任何其他页面,此时popBackStack会返回false, 如果此时又继续调用navigate()函数,会发生exception。
pop 过程比较简单,最终会走到 popBackStackInternal 方法。
# NavController.java
public boolean popBackStack(@IdRes int destinationId, boolean inclusive) {
boolean popped = popBackStackInternal(destinationId, inclusive);
return popped && dispatchOnDestinationChanged();
}
boolean popBackStackInternal(@IdRes int destinationId, boolean inclusive) {
if (mBackStack.isEmpty()) {
return false;
}
ArrayList> popOperations = new ArrayList<>();
//倒序遍历返回栈
Iterator iterator = mBackStack.descendingIterator();
boolean foundDestination = false;
while (iterator.hasNext()) {
NavDestination destination = iterator.next().getDestination();
Navigator> navigator = mNavigatorProvider.getNavigator(
destination.getNavigatorName());
//查找本次操作需要退出的页面
if (inclusive || destination.getId() != destinationId) {
popOperations.add(navigator);
}
if (destination.getId() == destinationId) {
//返回栈中匹配到目的地,结束流程
foundDestination = true;
break;
}
}
if (!foundDestination) {
//没找到回退目标 什么都不做
return false;
}
boolean popped = false;
for (Navigator> navigator : popOperations) {
//依次退出,不同 navigator 采取各自定义的退出动作
if (navigator.popBackStack()) {
//取出并移除返回栈
NavBackStackEntry entry = mBackStack.removeLast();
//清除返回栈viewmodel,更新 LifeState
...
popped = true;
}
...
}
//更新返回按钮状态
updateOnBackPressedCallbackEnabled();
return popped;
}
另外,实际的回退动作还是交由 navigator 完成,我们以 FragmentNavigator 为例:
# FragmentNavigator.java
public boolean popBackStack() {
if (mBackStack.isEmpty()) {
return false;
}
...
//通过 fm 完成事务的回滚
mFragmentManager.popBackStack(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
mBackStack.removeLast();
return true;
}
所以,Fragment 弹出栈最终还是通过 FM 回滚事务完成。
Q:popUpTo 和 popUpToInclusive的作用
app:popUpTo
这是出栈直到某个元素。Fragment1@01>Fragment2@02>Fragment3@03,在Fragment3启动Fragment4时设置出栈到Fragment1,那栈中的Fragment2,Fragment3会出栈销毁,只存Fragment1和Fragment4。
app:popUpToInclusive
这属性配合app:popUpTo使用,用来判断到达指定元素时是否把指定元素也出栈。同样上面的例子,true的话Fragment1也会出栈销毁,栈中只存留Fragment4。
Q:app:navGraph 属性指向一个navigation_graph的xml文件,以声明其 导航的结构,这个xml资源文件的导航图是如何解析的?
NavController 管理者Navigation组件中的所有导航行为,涉及xml解析,导航堆栈管理,导航跳转等方面。
1、在NavHostFragment的inflate()方法中,解析出我们上面提到的在xml配置的两个参数defaultNavHost, 和navGraph,并保存在成员变量中 mGraphId,mDefaultNavHost。
final TypedArray navHost = context.obtainStyledAttributes(attrs,
androidx.navigation.R.styleable.NavHost);
final int graphId = navHost.getResourceId(
androidx.navigation.R.styleable.NavHost_navGraph, 0);
if (graphId != 0) {
mGraphId = graphId;
}
navHost.recycle();
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
if (defaultHost) {
mDefaultNavHost = true;
}
a.recycle();
2、在onCreate()中,创建NavController,并把mGraphId设置给了它,
mNavController = new NavHostController(context);
//省略部分代码
if (mGraphId != 0) {
// Set from onInflate()
mNavController.setGraph(mGraphId);
} else {
// See if it was set by NavHostFragment.create()
final Bundle args = getArguments();
final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
final Bundle startDestinationArgs = args != null
? args.getBundle(KEY_START_DESTINATION_ARGS)
: null;
if (graphId != 0) {
mNavController.setGraph(graphId, startDestinationArgs);
}
}
3、NavController持有一个NavInflater对象,把导航xml文件传递给了NavInflater, NavInflater主要负责解析导航xml文件,解析完毕后,生成NavGraph,NavGraph是个目标管理容器,保存着xml中配置的导航目标NavDestination。
@NonNull
private NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser,
@NonNull AttributeSet attrs, int graphResId)
throws XmlPullParserException, IOException {
Navigator navigator = mNavigatorProvider.getNavigator(parser.getName());
final NavDestination dest = navigator.createDestination();
dest.onInflate(mContext, attrs);
....
final String name = parser.getName();
if (TAG_ARGUMENT.equals(name)) { // argument 节点
inflateArgumentForDestination(res, dest, attrs, graphResId);
} else if (TAG_DEEP_LINK.equals(name)) { // deeplink 节点
inflateDeepLink(res, dest, attrs);
} else if (TAG_ACTION.equals(name)) { // action 节点
inflateAction(res, dest, attrs, parser, graphResId);
} else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) { // include 节点
final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavInclude);
final int id = a.getResourceId(R.styleable.NavInclude_graph, 0);
((NavGraph) dest).addDestination(inflate(id));
a.recycle();
} else if (dest instanceof NavGraph) { // NavGraph 节点
((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId));
}
}
return dest;
}
解析后返回 NavGraph ,NavGraph是继承自 NavDestination的,里面主要是保存了所有解析出来的节点信息。
Q:Navigation是如何实现导航的?
在使用过程中我们调用的是 NavController 的 navigate 函数,抽丝剥茧,发现导航最终调用的是 Navigator 的 navigate 函数。
public abstract class Navigator {
//省略很多代码,包括部分抽象方法,这里仅阐述设计的思路!
//导航
public abstract void navigate(@NonNull D destination, @Nullable Bundle args,
@Nullable NavOptions navOptions);
//实例化NavDestination(就是Fragment)
public abstract D createDestination();
//后退导航
public abstract boolean popBackStack();
}
Navigator(导航者) 的职责很单纯:
- 1.能够实例化对应的 NavDestination
- 2.能够指定导航
- 3.能够后退导航
现在我们可以着手了解用于跳转的 navigate 方法到底做了什么。
# NavController.java
private void navigate(NavDestination node, Bundle args, NavOptions navOptions, Navigator.Extras navigatorExtras) {
// 处理 popUpTo 属性,也就是先 pop 再跳转
boolean popped = false;
boolean launchSingleTop = false;
if (navOptions != null) {
if (navOptions.getPopUpTo() != -1) {
popped = popBackStackInternal(navOptions.getPopUpTo(),
navOptions.isPopUpToInclusive());
}
}
// 根据不同类型的目的地,获取对应的Navigator
Navigator navigator = mNavigatorProvider.getNavigator(
node.getNavigatorName());
//添加导航图中设置的默认参数
Bundle finalArgs = node.addInDefaultArgs(args);
//发起导航
NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);
if (newDest != null) {
...
// 实际发生了导航,将跳转的页面添加到返回栈,如果其中包含 NavGraph 应添加到返回栈顶
if (mBackStack.isEmpty() || mBackStack.getFirst().getDestination() != mGraph) {
NavBackStackEntry entry = new NavBackStackEntry(mContext, mGraph, finalArgs,
mLifecycleOwner, mViewModel);
mBackStack.addFirst(entry);
}
//最终将实际目的地添加到返回栈顶
NavBackStackEntry newBackStackEntry = new NavBackStackEntry(mContext, newDest,
newDest.addInDefaultArgs(finalArgs), mLifecycleOwner, mViewModel);
mBackStack.add(newBackStackEntry);
} else if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
//singleTop 情况下 仅更新跳转数据
launchSingleTop = true;
NavBackStackEntry singleTopBackStackEntry = mBackStack.peekLast();
if (singleTopBackStackEntry != null) {
singleTopBackStackEntry.replaceArguments(finalArgs);
}
}
//更新返回按钮的可点击状态
updateOnBackPressedCallbackEnabled();
if (popped || newDest != null || launchSingleTop) {
//满足上述情况 发送 destinationChagned 回调
dispatchOnDestinationChanged();
}
}
- node 需要导航的目的地
- args 需要携带的参数
- navOptions 跳转的配置项,如转场动画、popupto、popUpToInclusive、launchSingleTop 属性等。
- navigatorExtras 其他额外参数,默认实现支持 Fragment 跳转时共享元素(shareElement)、Activity 导航添加 Flag 参数。
总结起来:
1、若配置了 popUpTo 属性,则先执行弹出动作(popBackStack 我们后面会讲到)。
2、根据 NavDestination 的 navigatorName 属性 从 navigatorProvider 中得到对应的 Navigator,通过上文我们已经知道几个内置名字: fragment、activity、dialog 等。
3、调用 navigator.navigate 方法执行具体的跳转动作。
4、如果3实际执行了跳转,那么会将 传入的 NavDestination 返回,此时将跳转过程中生成的 NavBackStackEntry 添加到返回栈中。
4、处理 singleTop 的情况。
5、更新返回按钮的可用性,默认情况下 如果返回栈中有实际的目的地,则 UI 层的返回按钮应该显示。
6、发送 dispatchOnDestinationChanged 回调通知业务层。
可以看到NavController的navigate并没有真正的实现导航,而是通过 mNavigatorProvider.getNavigator()得到对应的导航器,做了一个对应多态调用,最后由对应的导航器去实现导航。
Navigation 内置了 四种常用的 导航器:
NavGraphNavigator、FragmentNavigator、DialogFragmentNavigator、ActivityNavigator
这些默认导航器的注册同样在 NavHostFragment 的初始化过程就完成。
首先看一下ActivityNavigator的navigate()
@Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
if (destination.getIntent() == null) {
throw new IllegalStateException("Destination " + destination.getId()
+ " does not have an Intent set.");
}
Intent intent = new Intent(destination.getIntent());
// 参数 flagsd等配置
mContext.startActivity(intent);
//动画配置
// You can't pop the back stack from the caller of a new Activity,
// so we don't add this navigator to the controller's back stack
return null;
}
就是通过Intent来实现跳转的,中间做了一下参数设置,flags的添加,和动画等。让后调用 mContext.startActivity(intent);启动activity。
接着看看FragmentNavigator的navigate()
@SuppressWarnings("deprecation") /* Using instantiateFragment for forward compatibility */
@Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
if (mFragmentManager.isStateSaved()) {
Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
+ " saved its state");
return null;
}
String className = destination.getClassName();
if (className.charAt(0) == '.') {
className = mContext.getPackageName() + className;
}
final Fragment frag = instantiateFragment(mContext, mFragmentManager,
className, args);
frag.setArguments(args);
final FragmentTransaction ft = mFragmentManager.beginTransaction();
//动画代码
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);
}
可以看到instantiateFragment(mContext, mFragmentManager,
className, args); 通过反射实例化一个Fragment,然后调用replace方法显示出来了,
这里使用replace导致每次切换都会重新创建Framgnt。
Q:底部导航栏 + Fragment 切换,假设有两个Tab 分别为 首页 和 个人,首页 Tab 支持内部跳转到搜索页面,当用户处于搜索页面时 切换 Tab 到 个人后,再次切回首页,如果用 Navigation 实现 将会回到首页,而不是首页的二级搜索页。这是为什么呢?
这本质上是 Navigation 不支持 多个返回栈的状态保存。因为两个个 Fragment都由 androidx.navigation.fragment.NavHostFragment 直接管理,而目前单个 FragmentManager 仅支持一个返回栈。
也就是首页和个人都是嵌套图且在导航图中的为兄弟节点,那么在这两个嵌套图来回跳转时应当保持各自的返回栈。
Q: 如何在Fragment中拦截系统返回键?比如使用Navigation从Fragment1 跳转到Fragment2,此时按下返回键,希望可以弹出一个对话框确认是否退出,该如何实现?
handling-back-button-in-android-navigation-component
正常情况下,当 Activity 收到返回事件后,会直接 退出本页面。但由于 Navigation 是单 Activity 模式,默认应该先弹出返回栈,当返回栈没有目的地后才执行 Activity 的退出逻辑。
AndroidX 包中的 ComponentActivity 添加了对返回事件的拦截——OnBackPressedDispatcher。外部可以向 OnBackPressedDispatcher 添加回调,其内部维护一个 callback 列表,当收到返回事件时,会倒序依次回调返回列表,当某个回调消费了此返回事件,则停止遍历。若均没有消费,则会回调一个 fallback 方法。
# ComponentActivity.java
public class ComponentActivity extends androidx.core.app.ComponentActivity
private final OnBackPressedDispatcher mOnBackPressedDispatcher =
new OnBackPressedDispatcher(new Runnable() {
@Override
public void run() {
//fallback 执行 finish
ComponentActivity.super.onBackPressed();
}
});
public void onBackPressed() {
mOnBackPressedDispatcher.onBackPressed();
}
public final OnBackPressedDispatcher getOnBackPressedDispatcher() {
return mOnBackPressedDispatcher;
}
}
另外,callback 是可感知生命周期的,也就是当注册回调的组件处 于unactive,则会自动移除回调。
以上,所以基于 ComponentActivity 开发就默认支持返回拦截了,
那么Navigation中是怎么处理的?
Navigation 就在内部添加了一个默认的拦截器。
# NavHostFragment.onCreate()
//将Activity 使用的 Dispatcher 传入 Navigation
mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());
# NavController.java
void setOnBackPressedDispatcher(OnBackPressedDispatcher dispatcher) {
...
dispatcher.addCallback(mLifecycleOwner, mOnBackPressedCallback);
...
}
// enable属性控制该拦截器是否生效
private boolean mEnableOnBackPressedCallback = true;
private final OnBackPressedCallback mOnBackPressedCallback =
new OnBackPressedCallback(false) {
@Override
public void handleOnBackPressed() {
//拦截返回的处理就是弹出返回栈
popBackStack();
}
};
这里可以看到,在Navigtion组件中,默认的返回就是调用popBackStack弹出返回栈。
另外,每次跳转和返回最后都会执行 updateOnBackPressedCallbackEnabled,事实上也是在更新这个拦截器的可用性。
# NavController.java
private void updateOnBackPressedCallbackEnabled() {
mOnBackPressedCallback.setEnabled(mEnableOnBackPressedCallback
&& getDestinationCountOnBackStack() > 1);
}
如果我们需要拦截返回操作,可以通过 OnBackPressedDispatcher 提供的 addCallback 接口完成返回的拦截,如果不需要拦截了,将拦截器的 enable 属性设置为 false。
public class MyFragment extends Fragment {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// This callback will only be called when MyFragment is at least Started.
OnBackPressedCallback callback = new OnBackPressedCallback(true /* enabled by default */) {
@Override
public void handleOnBackPressed() {
// Handle the back button event
}
};
requireActivity().getOnBackPressedDispatcher().addCallback(this, callback);
// The callback can be enabled or disabled here or in handleOnBackPressed()
}
...
}
Q:如何为 start destination 设置入参?
默认情况下 Navigation 不支持为 start destination 设置入参,如果需要传参需使用代码完成 graph 的初始化。去掉 xml 中设置的graph,转用代码设置 graph,并指定入参。
mNavController.setGraph(graphId, startDestinationArgs);
Q:如何动态设置start destination(起始目的地)?
(set-startdestination-conditionally-using-android-navigation)[https://stackoverflow.com/questions/51929290/is-it-possible-to-set-startdestination-conditionally-using-android-navigation-ar?noredirect=1]
在
MainActivity.java
中:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
NavHostFragment navHost = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_main_nav_host);
NavController navController = navHost.getNavController();
NavInflater navInflater = navController.getNavInflater();
NavGraph graph = navInflater.inflate(R.navigation.navigation_main);
if (false) {
graph.setStartDestination(R.id.oneFragment);
} else {
graph.setStartDestination(R.id.twoFragment);
}
navController.setGraph(graph);
}
activity_main.xml
:
navigation_main.xml:
大巧不工的Fragment管理框架
Navigation 实现原理篇