遗留的问题
上篇文章 手撕Jetpack组件之Navigation分析了导航框架的整个流程,还遗留了一个问题:在Fragment
切换时,由于用的是replace
方法,所以再次回到某个Fragment
界面时又会执行onCreateView
方法导致界面重绘。 Fragment
的加载都是在FragmentNavigator#navigate
方法内执行,所以重写这个方法相对来说改动量是最小的。再来回顾navigate
方法做的事情。
@Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
...
// 注释1
final Fragment frag = instantiateFragment(mContext, mFragmentManager,
className, args);
frag.setArguments(args);
final FragmentTransaction ft = mFragmentManager.beginTransaction();
...
// 注释2
ft.replace(mContainerId, frag);
// 注释3
ft.setPrimaryNavigationFragment(frag);
final @IdRes int destId = destination.getId();
// 注释4
final boolean initialNavigation = mBackStack.isEmpty();
...
ft.commit();
...
}
注释1和注释2的代码逻辑非常简单,上来就创建再替换。既然这样的话,我们就可以按下面的步骤来实现:
- 先将当前正在显示的
Fragment
隐藏,那如何能拿到正在显示的Fragment
呢?请看到注释3,每一次新的Fragment
都会调用setPrimaryNavigationFragment
这个方法,既然有setXXX
,那肯定会有getPrimaryNavigationFragment
,只不过不是在FragmentTransaction
对象上,而是在mFragmentManager
对象上。 - 根据
tag
查找这个目标Fragment
是否有加载过,有就直接show
,没有就反射创建 -
mBackStack
是一个ArrayDeque
类型的对象,用来记录Fragment
的回退栈。它在父类中是私有的,只能通过反射来拿这个对象。
@Navigator.Name("customFragment")
public class CustomFragmentNavigator extends FragmentNavigator {
private static final String TAG = "CustomFragmentNavigator";
@NonNull
private final Context mContext;
@NonNull
private final FragmentManager mFragmentManager;
private final int mContainerId;
public CustomFragmentNavigator(@NonNull Context context, @NonNull FragmentManager manager, int containerId) {
super(context, manager, containerId);
mContext = context;
mFragmentManager = manager;
mContainerId = containerId;
}
@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();
// 先把当前正在显示的Fragment隐藏
Fragment current = mFragmentManager.getPrimaryNavigationFragment();
if (current != null) {
Log.i(TAG, "当前需要隐藏的fragment: " + current.getClass().getCanonicalName());
ft.hide(current);
}
int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
}
// 查找需要显示的Fragment是否加载过
Fragment frag = mFragmentManager.findFragmentByTag(className);
if (frag == null) {
// 说明从未加载过,那就需要通过反射创建对象
frag = instantiateFragment(mContext, mFragmentManager, className, args);
frag.setArguments(args);
ft.add(mContainerId, frag, className);
Log.d(TAG, "反射创建fragment: " + className);
} else {
ft.show(frag);
}
// ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);
final @IdRes int destId = destination.getId();
ArrayDeque<Integer> mBackStack = null;
try {
Field mBackStackField = FragmentNavigator.class.getDeclaredField("mBackStack");
mBackStackField.setAccessible(true);
//noinspection unchecked
mBackStack = (ArrayDeque<Integer>) mBackStackField.get(this);
} catch (Exception e) {
Log.e(TAG, "反射获取mBackStack对象异常: " + e.getMessage());
}
final boolean initialNavigation = mBackStack.isEmpty();
// TODO Build first class singleTop behavior for fragments
final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
&& navOptions.shouldLaunchSingleTop()
&& mBackStack.peekLast() == destId;
boolean isAdded;
if (initialNavigation) {
isAdded = true;
} else if (isSingleTopReplacement) {
// Single Top means we only want one instance on the back stack
if (mBackStack.size() > 1) {
// If the Fragment to be replaced is on the FragmentManager's
// back stack, a simple replace() isn't enough so we
// remove it from the back stack and put our replacement
// on the back stack in its place
mFragmentManager.popBackStack(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
}
isAdded = false;
} else {
ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
isAdded = true;
}
if (navigatorExtras instanceof Extras) {
Extras extras = (Extras) navigatorExtras;
for (Map.Entry<View, String> sharedElement : extras.getSharedElements().entrySet()) {
ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
}
}
ft.setReorderingAllowed(true);
ft.commit();
// The commit succeeded, update our view of the world
if (isAdded) {
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
private String generateBackStackName(int backStackIndex, int destId) {
return backStackIndex + "-" + destId;
}
}
类上面有一个注解,注解的值被用来作为key保存到NavigatorProvider
中。
添加Navigator
我们自定义的Navigator
准备好了,那放到哪里去了?上篇文章提到过,所有的Navigator
都被保存在NavigatorProvider
,那就先要获取这个provider
对象。可以通过NavController
对象获取,那如何获取这个controller
对象呢?可以通过NavHostFragment
来获取。所以最终我们先要获取到这个占位Fragment
,怎么获取它呢,那就比较简单了,直接用FragmentManager
就可以得到。
// 此处代码放在Activity的onCreate方法内
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment_activity_main);
assert fragment != null;
NavController navController = NavHostFragment.findNavController(fragment);
NavigatorProvider navigatorProvider = navController.getNavigatorProvider();
CustomFragmentNavigator customNavigator = new CustomFragmentNavigator(this, getSupportFragmentManager(), R.id.nav_host_fragment_activity_main);
navigatorProvider.addNavigator(customNavigator);
将界面与Navigator
绑定
上篇文章提到过,整个navigation目录下的xml文件都是由NavGraph
来管理,里面的fragment
标签都会被封装成一个个NavDestination
。这部分加载是由NavGraphNavigator
负责。所以我们要把写在xml文件的逻辑用代码实现
NavGraph navGraph = new NavGraph(new NavGraphNavigator(navigatorProvider));
FragmentNavigator.Destination hfd = customNavigator.createDestination();
hfd.setId(R.id.id_home_fragment);
hfd.setClassName(HomeFragment.class.getCanonicalName());
hfd.setLabel(getResources().getString(R.string.title_home));
navGraph.addDestination(hfd);
FragmentNavigator.Destination dfd = customNavigator.createDestination();
dfd.setId(R.id.id_dashboard_fragment);
dfd.setClassName(DashboardFragment.class.getCanonicalName());
dfd.setLabel(getResources().getString(R.string.title_dashboard));
navGraph.addDestination(dfd);
FragmentNavigator.Destination nfd = customNavigator.createDestination();
nfd.setId(R.id.id_notifications_fragment);
nfd.setClassName(NotificationsFragment.class.getCanonicalName());
nfd.setLabel(getResources().getString(R.string.title_notifications));
navGraph.addDestination(nfd);
// 设置首页
navGraph.setStartDestination(R.id.id_home_fragment);
// 加载我们代码生成的导航图
navController.setGraph(navGraph);
binding.navView.setOnNavigationItemSelectedListener(item -> {
// 监听tab的点击事件,同时变更与之对应的NavDestination
navController.navigate(item.getItemId());
return true;
});
布局文件中就不需要再去加载navigation目录下的文件了。
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- app:navGraph="@navigation/mobile_navigation" />-->
由于使用了自定义的Navigator
,引入了一个新的问题,那就是回退栈的问题。从第一个tab依次点第二个、第三个,此时再按返回键,先返回到第二个tab再到第一个tab,怎么进来就怎么回去。而Navigation
框架是直接回到第一个tab,再退出界面。但我觉得这也是一个坑,从产品角度来看,应该是直接退出界面,而不是回到第一个tab。那么重写Activity
的onBackPressed
方法可以达到与原框架一样的效果。
@Override
public void onBackPressed() {
int curId = navController.getCurrentDestination().getId();
int startDestination = navController.getGraph().getStartDestination();
if (curId != startDestination) {
binding.navView.setSelectedItemId(startDestination);
} else {
finish();
}
}