上篇文章咱们简单的介绍了Navigation组件的使用,以及深刻分析了源码中的具体实现,基本原理咱们已经很清晰了。本篇文章主要介绍下我在项目中遇到的问题,以及目前关于Navigation实现的一些探讨。尚未看过上篇文章的能够查看一下:java
Jetpack组件之Navigation---看完你就知道Navigation是什么了?node
先来看一下Navigation
组件在官方文档上的介绍:android
今天,咱们宣布推出Navigation组件,做为构建您的应用内界面的框架,重点是让单 Activity 应用成为首选架构。利用Navigation组件对 Fragment 的原生支持,您能够得到架构组件的全部好处(例如生命周期和 ViewModel),同时让此组件为您处理 FragmentTransaction 的复杂性。此外,Navigation组件还可让您声明咱们为您处理的转场。它能够自动构建正确的“向上”和“返回”行为,包含对深层连接的完整支持,并提供了帮助程序,用于将导航关联到合适的 UI 小部件,例如抽屉式导航栏和底部导航。git
确实通过源码分析咱们就能够发现,Navigation
组件封装了Menu
菜单栏、Fragment
的切换、NavigationView
、Drawerlayout
等一系列涉及到的组件,为了更方便的让咱们使用单Activity多Fragment的架构。github
可是我在使用的时候发现,当一个Fragment
中的布局稍微复杂一些,切换Fragment
的时候会顿卡,并且若是再配合DrawrLayout
使用的话,还会闪一下屏,效果体验不是很好,本着这个问题,我又再次对Navigation
组件进行了分析。数组
经过现象分析,发现当切换NavigationView
中的menu菜单来切换Fragment
时,DrawerLayout
抽屉关闭有一个短暂的动画(具体的这里就不分析了,感兴趣的能够自行查看,可是这不是根本缘由),同时Fragment
切换,发生顿卡和闪屏的现象。因此....仍是看源码吧:架构
private void navigate(@NonNull NavDestination node, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
boolean popped = false;
....
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
node.getNavigatorName());
Bundle finalArgs = node.addInDefaultArgs(args);
NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);
....
}
复制代码
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
...
//根据classname反射获取Fragmnent
final Fragment frag = instantiateFragment(mContext, mFragmentManager,
className, args);
frag.setArguments(args);
//获取Fragment事务
final FragmentTransaction ft = mFragmentManager.beginTransaction();
//切换动画设置
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
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);
......
ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
........
}
复制代码
看到这里就很清楚了吧,Fragment
的切换是经过replace
方式来切换的,而且加入回退栈,也就是说每次切换Fragment
,都会销毁视图和从新建立视图。至于为何用这种方式我是真的想不到,也没搞清楚初衷是什么?按照咱们目前的开发来讲,Fragment
的切换一般都会使用hide()
、show()
,而replcae()
的方式不多用,替换会把容器中的全部内容全都替换掉,有一些app会使用这样的作法,保持只有一个fragment在显示,减小了界面的层级关系。app
不只仅是这样,上篇文章有小伙伴问切换了
Fragment
以后,点击返回按钮,发现以前的Fragment
重走了onCreateView
流程,这就意味着以前的状态没了。对于这个问题其实根据上面的分析,也能大概想到是由于什么,可是返回按钮的操做我以前还真没有看过源码,因此此次顺便了解一下:框架
咱们一样从首页的onBackPressed
入手:ide
override fun onBackPressed() {
if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
drawerLayout.closeDrawer(GravityCompat.START)
} else {
super.onBackPressed()
}
}
复制代码
public void onBackPressed() {
mOnBackPressedDispatcher.onBackPressed();
}
复制代码
最终调用了mOnBackPressedDispatcher
的onBackPressed()
方法。咱们查看这个类,经过Debug调试,咱们跟到了FragmentManagerImpl
类:
private final OnBackPressedCallback mOnBackPressedCallback =
new OnBackPressedCallback(false) {
@Override
public void handleOnBackPressed() {
FragmentManagerImpl.this.handleOnBackPressed();
}
};
复制代码
发现点击返回按钮以后就走到这个,执行handleOnBackPressed()
方法。
继续跟踪源码,中间的一些过程我这里就忽略掉了,大部分都是一些popBackStack
的操做,这里咱们直接跟踪到关键点:
//在BackStackRecords中进行入栈出栈操做。
private static void executeOps(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) {
for (int i = startIndex; i < endIndex; i++) {
final BackStackRecord record = records.get(i);
final boolean isPop = isRecordPop.get(i);
if (isPop) {
record.bumpBackStackNesting(-1);
// Only execute the add operations at the end of
// all transactions.
boolean moveToState = i == (endIndex - 1);
record.executePopOps(moveToState);
} else {
record.bumpBackStackNesting(1);
record.executeOps();
}
}
}
复制代码
咱们能够看到经过遍历栈数组,对record
作executePopOps()
操做,经过cmd来让FragmentManager
作相关操做。
void executePopOps(boolean moveToState) {
for (int opNum = mOps.size() - 1; opNum >= 0; opNum--) {
final Op op = mOps.get(opNum);
Fragment f = op.mFragment;
if (f != null) {
f.setNextTransition(FragmentManagerImpl.reverseTransit(mTransition),
mTransitionStyle);
}
switch (op.mCmd) {
case OP_ADD:
f.setNextAnim(op.mPopExitAnim);
mManager.removeFragment(f);
break;
case OP_REMOVE:
f.setNextAnim(op.mPopEnterAnim);
mManager.addFragment(f, false);
break;
case OP_HIDE:
f.setNextAnim(op.mPopEnterAnim);
mManager.showFragment(f);
break;
case OP_SHOW:
f.setNextAnim(op.mPopExitAnim);
mManager.hideFragment(f);
break;
case OP_DETACH:
f.setNextAnim(op.mPopEnterAnim);
mManager.attachFragment(f);
break;
case OP_ATTACH:
f.setNextAnim(op.mPopExitAnim);
mManager.detachFragment(f);
break;
case OP_SET_PRIMARY_NAV:
mManager.setPrimaryNavigationFragment(null);
break;
case OP_UNSET_PRIMARY_NAV:
mManager.setPrimaryNavigationFragment(f);
break;
case OP_SET_MAX_LIFECYCLE:
mManager.setMaxLifecycle(f, op.mOldMaxState);
break;
default:
throw new IllegalArgumentException("Unknown cmd: " + op.mCmd);
}
if (!mReorderingAllowed && op.mCmd != OP_REMOVE && f != null) {
mManager.moveFragmentToExpectedState(f);
}
}
if (!mReorderingAllowed && moveToState) {
mManager.moveToState(mManager.mCurState, true);
}
}
复制代码
同时从新设置PrimaryNavigationFragment
,add咱们的首页Fragment
,最后执行moveToState
方法:
public void addFragment(Fragment fragment, boolean moveToStateNow) {
if (DEBUG) Log.v(TAG, "add: " + fragment);
makeActive(fragment);
if (!fragment.mDetached) {
if (mAdded.contains(fragment)) {
throw new IllegalStateException("Fragment already added: " + fragment);
}
synchronized (mAdded) {
mAdded.add(fragment);
}
fragment.mAdded = true;
fragment.mRemoving = false;
if (fragment.mView == null) {
fragment.mHiddenChanged = false;
}
if (isMenuAvailable(fragment)) {
mNeedMenuInvalidate = true;
}
if (moveToStateNow) {
moveToState(fragment);
}
}
}
复制代码
当咱们继续跟踪的时候就会发现,在moveToState
方法中,Fragment
的state是Fragment.CREATED
,而且会执行performCreateView()
中的onCreateView()
方法:
f.mContainer = container;
f.performCreateView(f.performGetLayoutInflater(f.mSavedFragmentState), container, f.mSavedFragmentState);
复制代码
void performCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mChildFragmentManager.noteStateNotSaved();
mPerformedCreateView = true;
mViewLifecycleOwner = new FragmentViewLifecycleOwner();
mView = onCreateView(inflater, container, savedInstanceState);
....
}
复制代码
到这里就基本结束了,我只分析了一个大概,能够了解到点击返回按钮,一样也会从新建立视图,也就是onCreateView
会从新走一遍。
对于Navigation
组件的这种切换方式,我也很无奈,并且也并无暴露出来API供咱们使用其余切换方式,我也询问了不少大佬,他们也不是很清楚,也有的发现这也是Navigation
的一个很大的诟病。那么有没有解决办法呢?很遗憾我目前尚未想到比较好的办法。
基于
Navigation
用来承载Fragment
的容器是NavHostFragment
,因此咱们并不能使用ViewPager+Fragment
的经过setUserVisibleHint
实现懒加载的方式;一样咱们也没办法使用onHiddenChanged
的方式来实现复杂逻辑的加载;可是你能够在进入Fragment
的时候先显示一个Loading框,加载完数据以后再渲染布局,这样的话能够减小一些尴尬。
这里个人建议是:若是你的每一个Fragment
真的每次都须要从新绘制的话,你能够考虑使用Navigation
组件来实现,毕竟经过Navgation
组件真的很方便帮助咱们切换导航,并且虽然布局会从新绘制,可是Google的官方Demo--SunFlower仍是使用了这种方式,因此这里面我以为:官方推荐咱们使用Jetpack组件中的ViewModel、LiveData.....等,能够发现SunFlowerdemo中,即使是切换Fragmengt也不会有很明显的卡顿现象,由于每一个Fragment即使从新绘制,可是View所对应的ViewModel还在,数据并不须要从新加载或者请求,固然这仅仅是我本身的见解啊.
可是若是你没有这种场景的话,建议仍是用普通的方式咱们本身来控制切换吧,这样不管是基于Drawerlayout
仍是BottomNaivgationView
的话,咱们能够本身实现切换。这块我也不是很肯定哈,也但愿听取你们的意见和建议。
我还发现一个问题,就是Play商店,如今就是这样的状况,抽屉栏中的
Item
每一个基本都是从新绘制,并且第一个Item
个人应用和游戏切换的时候就会有很明显的卡顿和闪屏,猜想Google play 商店具体是否是使用的Navigation
组件不敢肯定,可是它很大概率是经过replace
方式来作的切换。感兴趣的话能够看一下,我这贴一个GIF图,不必定能看清楚,不过确实是这个效果。
最后,若是有不对的地方或者更好的解决办法,能够一块儿讨论一下哈!