本篇是有关 Navigation 的第二篇,若有对 Navigation 不了解的朋友请先阅读来学一波 Navigation。php
在上一篇中,咱们利用 Navigation 与 BottomNavigationView 作出了一个有三个 Tab 的页面,分别是 Feed、Timer、Mine,这三个 Fragment 都是只在当前页面显示各自的名称。java
如今咱们来给 TimerFragment 加点内容,咱们在 TimerFragment 的 onCreateView 方法中启动一个倒计时。node
private void startTimer() {
new CountDownTimer(10 * 1000, 1000) {
@Override
public void onTick(long millisUntilFinished) {
tvLabel.setText(String.valueOf((millisUntilFinished / 1000) + 1));
}
@Override
public void onFinish() {
tvLabel.setText("Finished");
}
}.start();
}
复制代码
仔细看上面的效果能够看到,每次切换到 TimerFragment 时,倒计时总会从新开始,不是咱们想要的仅开始一次。这是什么问题致使的呢?答案是 TimerFragment 执行了屡次的 onCreateView,为何是会执行屡次,Fragment 为何会加载屡次?咱们没有什么特殊的操做呀。是否是由于 Navigation?android
如今让咱们深刻到 Navigation 的源码看一看这究竟是怎么一回事,以及咱们该如何解决这一问题。git
首先,咱们须要明确咱们的方向,就是 Navigation 究竟是怎么作 Fragment 切换的,为何会致使 Fragment 的 onCreateView 被屡次执行。github
从哪里做为入口呢?了解过 Navigation 的朋友对下面这行代码应该不会陌生,就是经过一个 View 获取到 NavController,而后经过执行 NavController 的 navigate 这个方法,咱们就从这个方法开始。设计模式
Navigation.findNavController(view)
.navigate(id);
复制代码
这个 navigate 有多个重载方法,咱们开始的 navigate 方法最终也是执行到下面这个重载方法。app
navigate(NavDestination node, Bundle args, NavOptions navOptions, Navigator.Extras navigatorExtras)
复制代码
方法的具体内容以下图:框架
其中在第 9 行,咱们能够看到经过 mNavigatorProvider 获取到了一个泛型类型为 NavDestination 的 Navigator 对象,而且在第 12 行时,经过调用刚获取到的 navigator 的 navigate 方法,获得了 NavDestination 这个对象。ide
这两行是关键代码,一个是获取到执行 navigator 的对象,一个是实际执行 navigate 的方法。看到这,咱们就只须要找到 Navigator 的 navigate 方法便可。不过,Navigator 这个只是一个抽象类,咱们还须要继续寻找它的实现类。
快捷键:Implementation(s) Mac: option(⌥) + command(⌘)+B
Navigator 抽象类的关键代码:
public abstract class Navigator<D extends NavDestination> {
@Retention(RUNTIME)
@Target({TYPE})
@SuppressWarnings("UnknownNullness")
public @interface Name {
String value();
}
@NonNull
public abstract D createDestination();
@Nullable
public abstract NavDestination navigate(@NonNull D destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Extras navigatorExtras);
public abstract boolean popBackStack();
@Nullable
public Bundle onSaveState() {
return null;
}
public void onRestoreState(@NonNull Bundle savedState) {
}
public interface Extras {
}
}
复制代码
经过快捷键咱们能找到多个实现类,有 ActivityNavigator、DialogFragmentNavigator 还有 FragmentNavigator 等,这里咱们只关注 FragmentNavigator 这个类中的 navigate 这个方法。
别看这么多代码,别惧怕,其实关键部分的代码就是第 32 行 ft.replace(mContainerId, frag)
这里使用的是 FragmentTransaction 的 replace 方法,这个方法不用说了吧。 replace 是移除了相同 id 的 fragment 而后再进行 add 的。
因此,看到这,咱们也就知道了,为何 TimerFragment 的 onCreateView 方法会被执行屡次了,缘由就是在这。
找到缘由了,那咱们有什么方法去规避,或者说去绕过这个 replace 吗?答案是有的。
还记得刚才咱们找的下面这行代码吧(忘记的,请看第一张代码图的第 9 行),刚才我说,经过 mNavigatorProvider 找到一个泛型类型为 NavDestination 的 Navigator 对象,那它其实是怎么找到的呢?是经过 node.getNavigatorName() 而后找的,这个 node 是什么东西?以及 mNavigatorProvider.getNavigator 内部究竟发生了什么?
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
node.getNavigatorName());
复制代码
实际上这里的 node 就是一个 NavDestination 对象,而一个 NavDestination 对象就是对应着 navigation graph 中的节点信息。我用来演示的 Demo 的 navigation graph 文件以下:
<?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/tab_navigation" app:startDestination="@id/feedFragment">
<fragment android:id="@+id/feedFragment" android:name="me.monster.blogtest.tab.FeedFragment" android:label="fragment_feed" tools:layout="@layout/fragment_feed" />
<fragment android:id="@+id/timerFragment" android:name="me.monster.blogtest.tab.TimerFragment" android:label="fragment_timer" tools:layout="@layout/fragment_timer" />
<fragment android:id="@+id/mineFragment" android:name="me.monster.blogtest.tab.MineFragment" android:label="fragment_mine" tools:layout="@layout/fragment_mine" />
</navigation>
复制代码
node.getNavigatorName 返回的就是 fragment 节点的节点名称 fragment
,而 getNavigator 其实内部就是维护了一个类型为 HashMap 的 mNavigators,这个 HashMap 存的 key 就是节点名称,value 就是抽象类 Navigator 的实现类。而与 fragment 对应的 FragmentNavigator 也存储在其中。
既然是存在一个 map,并从中取出相对于的 Navigator 实现类,那咱们能不能建立一个类并实现 Navigator,而后将 key、value 添加到那个 HashMap 中。答案是可行的。在NavigatorProvider 这个类中有两个公共方法:
其中,一个参数的 addNavigator 也是调用了 两个参数的 addNavigator 方法,那个 name 也就是 navigation graph 中 fragment 节点的节点名称,同时也是 Navigator 这个抽象类中注解 Name
定义的值。并且在 NavController 这个类(最初咱们找到的 navigate 所在的类)中有一个 getNavigatorProvider() 方法。
看到这,关系应该就比较清楚了。因此,咱们须要本身建立一个类,实现 Navigator 并为 Name 注解添加一个值,而后在使用 Navigation 这个模块的 Activity 获取到 NavController 并调用其 getNavigatorProvider 方法后再调用 addNavigator 便可。
Github 上已经有一个演示自定义实现 Navigator 的项目了。这个项目是以 Kotlin 语言编写的。
项目地址: github.com/STAR-ZERO/n…
提及来这个项目仍是 Drakeet 在他的知识星球中分享的。感谢 Drakeet 的分享。
我根据按照他的代码写了一份 Java 版本的,而且在其中改了两行代码(注释部分)。注释的内容其实就是使用 FragmentTranslation 对 Fragment 进行控制。原做者写的是 detach 与 attach 方法,我改为了使用 hide 和 show 方法。
@Navigator.Name("keep_state_fragment")
public class KeepStateNavigator extends FragmentNavigator {
private Context context;
private FragmentManager manager;
private int containerId;
public KeepStateNavigator(@NonNull Context context, @NonNull FragmentManager manager, int containerId) {
super(context, manager, containerId);
this.context = context;
this.manager = manager;
this.containerId = containerId;
}
@Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
String tag = String.valueOf(destination.getId());
FragmentTransaction transaction = manager.beginTransaction();
boolean initialNavigate = false;
Fragment currentFragment = manager.getPrimaryNavigationFragment();
if (currentFragment != null) {
// transaction.detach(currentFragment);
transaction.hide(currentFragment);
} else {
initialNavigate = true;
}
Fragment fragment = manager.findFragmentByTag(tag);
if (fragment == null) {
String className = destination.getClassName();
fragment = manager.getFragmentFactory().instantiate(context.getClassLoader(), className);
transaction.add(containerId, fragment, tag);
} else {
// transaction.attach(fragment);
transaction.show(fragment);
}
transaction.setPrimaryNavigationFragment(fragment);
transaction.setReorderingAllowed(true);
transaction.commitNow();
return initialNavigate ? destination : null;
}
}
复制代码
注意,使用自定义 Navigator 的时候 navigation graph 须要把 fragment 节点名称改成 keep_state_fragment,而且在承载的 Activity 中进行设置而且还须要把 Activity 布局文件中 fragment 的 navGraph 属性移除。
NavController navController = Navigation.findNavController(this, R.id.fragment3);
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.fragment3);
KeepStateNavigator navigator = new KeepStateNavigator(this, navHostFragment.getChildFragmentManager(), R.id.fragment3);
navController.getNavigatorProvider().addNavigator(navigator);
navController.setGraph(R.navigation.tab_navigation);
复制代码
最后来看一下使用自定义 Navigator 时的 TabActivity。
这样好像看起来结束了?其实并无,咱们只是刚刚开始。
首先,我先更正一下,在第一篇关于 Navigation 的博客中从 SettingFragment 返回到 RootFragment 那一段代码有些问题。
没看过那篇文章的不要着急,其实就是 A 调到 B,而后在 B 中触发一个点击事件,再从 B 返回到 A。返回的代码以下。
原代码为:
btnToRoot.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Navigation.findNavController(btnToRoot)
.popBackStack();
}
});
复制代码
这里在点击事件中,最后执行的是 popBackStack
,其实不该该调用这个方法应该用 navigateUp
这个方法。
在第一篇博客中,Navigation Graph 中全部的节点名称都是 Fragment,若是我用上面这种 keep_state_fragment 的方式,会发生什么呢?
能够看到,在把 Navigation Graph 节点名替换为 keep_state_fragment 后,在 SettingFragment 点击返回并无进行返回。这是为何呢?我没干啥呀,怎么很差使了。
不行,我要看看 Navigation 源码里面到底怎么作的。因而我开始了 debug 之旅。后来,我发如今 Navigation.findNavController(btnToRoot).navigateUp();
内部判断了当前的返回栈个数是否为 1,结果让我很震惊,返回的居然真的是 1。因此,navigateUp 就理所固然的返回 false,也就没能从 SettingFragment 回到 RootFragment 了。
下面的两段代码分别是:NavController#navigateUp 和 NavController#getDestinationCountOnBackStack
private int getDestinationCountOnBackStack() {
int count = 0;
for (NavBackStackEntry entry : mBackStack) {
if (!(entry.getDestination() instanceof NavGraph)) {
count++;
}
}
return count;
}
复制代码
我查了一下 mBackStack 这个数据类型,发现它是一个栈,紧接着找到 mBackStack 入栈的方法。
mBackStack#add 相关的方法一共有 4 个,第一个方法是在 NavController#NavController 方法中进行调用的,其他 3 个 add 相关方法均是在 NavController#navigate 内调用,而调用 add 方法以外有一个判空。判空的对象就是来自 Navigator#navigate 这个方法。
NavController 的 navigate 方法,有删减。
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);
if (newDest != null) {
// The mGraph should always be on the back stack after you navigate()
if (mBackStack.isEmpty()) {
mBackStack.add(new NavBackStackEntry(mGraph, finalArgs));
}
// Now ensure all intermediate NavGraphs are put on the back stack
// to ensure that global actions work.
ArrayDeque<NavBackStackEntry> hierarchy = new ArrayDeque<>();
NavDestination destination = newDest;
while (destination != null && findDestination(destination.getId()) == null) {
NavGraph parent = destination.getParent();
if (parent != null) {
hierarchy.addFirst(new NavBackStackEntry(parent, finalArgs));
}
destination = parent;
}
mBackStack.addAll(hierarchy);
// And finally, add the new destination with its default args
NavBackStackEntry newBackStackEntry = new NavBackStackEntry(newDest,
newDest.addInDefaultArgs(finalArgs));
mBackStack.add(newBackStackEntry);
}
//......
}
复制代码
根据咱们以前的经验,能够得出这里的 Navigator 就是咱们自定义的 KeepStateNavigator 这个对象,那 navgate 这个方法的返回值也就是咱们本身控制的,也就是咱们本身给本身挖了个坑。2333~
来吧,来看一下刚才写的代码。
public NavDestination navigate(Destination destination, Bundle args, NavOptions navOptions, Navigator.Extras navigatorExtras) {
String tag = String.valueOf(destination.getId());
FragmentTransaction transaction = manager.beginTransaction();
boolean initialNavigate = false;
Fragment currentFragment = manager.getPrimaryNavigationFragment();
if (currentFragment != null) {
transaction.hide(currentFragment);
} else {
initialNavigate = true;
}
Fragment fragment = manager.findFragmentByTag(tag);
if (fragment == null) {
String className = destination.getClassName();
fragment = manager.getFragmentFactory().instantiate(context.getClassLoader(), className);
transaction.add(containerId, fragment, tag);
} else {
transaction.show(fragment);
}
transaction.setPrimaryNavigationFragment(fragment);
transaction.setReorderingAllowed(true);
transaction.commitNow();
return initialNavigate ? destination : null;
}
复制代码
在最后一行中,咱们经过对 initialNavigate
进行判断而后返回 null 或是 destination 对象。而把 initialNavigate 赋值为 true 则是只有在 currentFragment 为空时才会进行,何时 currentFragment 才会为空?只有当打开一个 Activity 并为其填充第一个 Fragment 时才会为 true,在咱们当前这个场景里,就是当应用启动,打开 RootFragment 时 initialNavigate 为 true,从 RootFragment 跳转到 SettingFragment 时 initialNavigate 为 false。
这显然是有问题的,那么咱们须要改成,当这个 fragmen 为空时,在 transaction.add(containerId, fragment, tag);
以后把 initialNavigate 赋值为 true。这样一来,NavController#getDestinationCountOnBackStack 就能获取到实际的 fragment 大小了,也就不会直接 return fase 了。
运行一下看看结果?别着急啊,再检查检查。刚才我说在 Navigation.findNavController(btnToRoot).navigateUp();
内部判断了当前的返回栈个数是否为 1,如今咱们把为 1 的状况解决了,那么当返回栈的个数不为 1 时它怎么作的?在判断返回栈个数不是 1 的后通过内部调用,最终来到了 NavController#popBackStackInternal
这个方法内。
NavController 的 popBackStackInternal 方法,有删减
boolean popBackStackInternal(@IdRes int destinationId, boolean inclusive) {
if (mBackStack.isEmpty()) {
// Nothing to pop if the back stack is empty
return false;
}
ArrayList<Navigator> popOperations = new ArrayList<>();
Iterator<NavBackStackEntry> 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);
}
//......
boolean popped = false;
for (Navigator navigator : popOperations) {
if (navigator.popBackStack()) {
NavBackStackEntry entry = mBackStack.removeLast();
popped = true;
// ......
return popped;
}
复制代码
在这个方法内,我又看到了那个熟悉的面孔 navigator,在这里 navigator 执行了一个叫 popBackStack 的方法,这个方法看起来好像就是作返回事件的。但是,咱们的 KeepStateNavigator 并无这个方法啊,那是由于咱们选择了继承自 FragmentNavigator,在 FragmentNavigator 有一套 popBackStack 逻辑,不过咱们用不了。因此咱们须要在 FragmentNavigator 进行重写这个方法。
因为咱们须要返回到上一个页面,因此咱们也得有个管理栈,而后在 KeepStateNavigator#navigate 方法中的 transaction.add(containerId, fragment, tag);
以后把当前 Fragment 添加到返回栈中,在 popBackStack 中根据一些条件再进行 remove 便可。
这样一来就能够了。
不知道为何,录制的 gif 画面一直在闪……
好了,这样就能够了,终于能够愉快的使用 Navigation 了。直到有一天,老大找到我,跟我说了一个需求。
从 A 页面进入 B 页面,再从 B 页面进入 C 页面,在 C 页面产生一个事件,而后用户返回时,须要跳过 B,也就是从 C 直接回到 A。
问我这个能不能在 Navigation 上实现,我想了一下,说能够。下面就分享一下实现这种效果的思路,我我的以为能够有两个解决方法,下面我依次来讲一下。
假设页面打开顺序为:A、B、C。
第一种:优先关闭
当从 C 返回到 A 时,其实并不必定是返回的时候进行操做,多是在某个事件产生以后,这时就把 B 给关闭,此时回退栈里面也就只剩下 A 和 B。这时候只须要正常走页面返回逻辑便可。
第二种:优先返回
当从 C 返回到 A 时,也能够直接跳过 B,具体方法为:当从 C 点击返回时,触发返回栈的操做,当完成返回操做后发现当前页面须要跳过期,则继续返回,此时也就回到了 A。
落实到 Navigation 中,就是在自定义 Navigator 中添加一些方法,而后在须要执行此类操做的地方获取到 Navigator 对象,并进行相关操做。
获取 Navigator 的相关代码以下:
NavController navController = Navigation.findNavController(btnToRoot);
NavigatorProvider navigatorProvider = navController.getNavigatorProvider();
Navigator<?> navigator = navigatorProvider.getNavigator("keep_state_fragment");
if (navigator instanceof KeepStateNavigator) {
((KeepStateNavigator) navigator).closeMiddle(R.id.settingsFragment);
}
复制代码
我第一次学习 Navigation 的时候就瞄了一眼,以为这不就是个 Fragment 的管理框架吗?有什么的呀,比其余Fragment 的管理框架好吗?看起来通常般啊。哇哦,好复杂啊,算了,不看了,知道大体怎么用就行。看着你们讨论的愈来愈多,没忍住,又去仔细看了一下 Navigation 的使用,以及稍微阅读了源码。不就是个 Fragment 管理框架吗?怎么搞这么复杂,什么 NavigatorProvider、Navigator、Destination 这些都是啥啊。
随着我看的内容愈来愈多,实践的也变多了,原生的 Navigation 越来不不能知足需求了,才发现,原来 Google 早就想到了,只是没有给咱们提供具体的解决方法,只是把一些东西开放出来供开发者在不一样场景下进行自定义使用。
想一想本身项目里的代码,好像若是要扩展的话,就得改动较多原来的代码,不能像 Navigation 这样,在须要改动的时候,尽可能不触动原有代码,而经过接口、Provider、泛型等更可能是编码技巧或是设计模式上的技巧来完成业务需求。
也不该该把过多的心思花在各式各样的第三方库上,而是把更多的精力花在基础技能上,虽然可能一时半会儿看不出什么结果,但这多是笑到最后的方法。
本文首发于我的博客,文中所有源代码已上传至 GitHub,代码分支为 closeBefore。喜欢本文的麻烦点个🌟。
本文封面图:Photo by João Silas on Unsplash