前事告一段落,在新的项目中,以为采用ViewPager
+Fragment
的方案做为主界面,建立的过程很快,也没有遇到什么问题。但在实现界面跳转的时候,才发现单Activity
多Fragment
结构的坑点仍是有不少。此次采用了Google最新发布的Android Jetpack组件中的Navigation
来控制Fragment
跳转,使用途中有优势,也有缺点,不过在总体算来,仍是极大打简化了咱们须要编写的代码。例如:NavHostFragment.findNavController(this).navigate(...)
默认是利用FragmentManager.FragmentTransaction.replace()
来进行导航,咱们我没法经过干预其过程来使用hide()
和show()
,不过从某种程度上看来也不算是缺点,相反,我以为这正好统一了Fragment
的使用吧,比较经过hide()
和show()
来展现Fragment
有可能会出发Fragment
重叠问题,故咱们还须要手动去解决这个问题。android
谈完Navigation
,就让咱们先来看一下App简化后的层级:ide
能够看到Activity
只是Fragment
的一个载体,全部界面的跳转均有Fragment
完成,均由Navigation
控制。在第一次跳转发生后,发现了一个问题:布局
即原本跳转以前,咱们的RecyclerView
是滚动到自定义的位置的,可是在跳转以后,再进行了返回以后,RecyclerView
回到了顶部,也就是默认位置。初步猜测,该问题是Fragment
从新建立了布局致使的,通过在Fragment各个生命周期回调方法内部打印log发如今跳转发生的时候,的确致使了Fragment1
的view
销毁,回调了onDestroyView
方法,而在跳转返回的时候也确实回调了onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?)
方法。既然这样,那么为何返回以后主界面即view
为何没有保存返回前的状态也就不难理解,原来是每次返回以后咱们所见到的主界面实际上是一个新的view
,而不是跳转以前的那个view
实例,天然也就没有跳转的状态了。动画
Fragment
只回调了onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?)
到onDestroyView
生命周期之间的方法,缘由是咱们所使用的ViewPager
的Adapter
是FragmentPagerAdapter
而不是FragmentStatePagerAdapter
,经过查阅二者的源码能够知道FragmentPagerAdapter
在销毁起子项时即调用destroyItem()
时调用了FragmentManager.FragmentTransaction.detach()
而不是remove()
,故Fragment只是销毁了视图,其实例依然存在;而FragmentStatePagerAdapter
则在销毁子项时即destroyItem()
时调用了FragmentManager.FragmentTransaction.remove()
故而完全移除了Fragment
。继续回到问题,既然咱们已经知道了问题出在了哪里,那么如今就须要着手解决问题了。首先我想到的方案是是这样的:this
既然从新返回致使从新建立的view使其回到了初始状态那么咱们只须要在跳转以前保存view
的相关状态与viewModel
中便可,初期须要保存的状态并很少,暂时只须要RecyclerView
滚动位置便可,而且在onDestroyView()
中调用便可。具体代码以下:spa
private fun getPositionAndOffset() { val topView = recyclerView.gridLayoutManager.getChildAt(0) if (topView != null) { stateViewModel.lastOffset = topView.top stateViewModel.lastPosition = recyclerView.gridLayoutManager.getPosition(topView) } } private fun scrollToPositionWithOffset() { if (recyclerView.layoutManager != null && viewModel.lastPosition >= 0) { recyclerView.gridLayoutManager.scrollToPositionWithOffset(viewModel.lastPosition, viewModel.lastOffset) } } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) ··· scrollToPositionWithOffset() ··· } override fun onDestroyView() { super.onDestroyView() ··· getPositionAndOffset() ··· }
如此修改以后,返回以后发现view
状态的确跟跳转以前相同的,此时,我觉得问题到这儿就算是结束了,但是随后新的问题,确又使人煞费苦心。code
由于RecyclerView
中的Item
都设定了ItemClick
点击事件,点击以后跳转到相应详情页,为了使跳转不那么生硬,这里采用了共享元素+其余动画的方式来实现过渡,但就是在应用共享元素动画的时候又出现了新的问题:具体表现为,共享元素在跳转发生后的进入动画彻底正常,可是点击Back
返回的时候,生硬的切回了主界面。个人共享元素的返回动画呢???文档不是说设定了共享元素进入动画后,能够不设定返回动画,系统会按照和进入相反的动画进行过渡。我觉得是我没有给共享元素设定返回动画的缘由,因而又加上了设定返回动画的代码。此次我满怀期待的从新构建了一遍项目,指望它能如我所愿,惋惜世事总不如意,纳尼?个人返回动画呢,为何还不出来。在经历了各类尝试无果以后,没办法只能先给Fragemnt1
这个总体加了一个退出动画来暂时顶替。虽然视觉上是不那么生硬了,可是因为进入和退出动画没有联系,在感知上,总有一种不合理的感受。就这么过去了一天,可仍是一点头绪也没有。在次日的时候忽然想到了一个问题,由于在以前使用Fragment
+ViewPager
+FragmentStatePagerAdapter
的时候遇到过返回后ViewPager
不显示的问题,那个时候查阅资料,最后找的的解决办法是在ViewPager
所在的Fragment
的onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?)
方法中判断一次rootView
是否为空,若是为空,则inflate
一个新的view
不然就将rootView
从它的父视图中移除(若是有的话),而后return rootView
,即:orm
private var rootView: View? = null override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val layoutId = getLayoutId() ?: return null if (rootView == null) { rootView = inflater.inflate(layoutId, container, false) } return rootView }
这个时候想到了多是新的view没有设置transitionName
所致,Transition Framework
天然也没法生成相应的过渡动画,这个时候个人脑海中就浮现出了另外一个方案:xml
此次咱们既然了解到了共享元素返回动画失效的缘由,是因为view
是新建立的,而且系统找不到对应的transitionName
,那么咱们能够换一个角度去思考,结合我前一次解决ViewPager
不显示的案例,很容易想到,复用已经生成的view
。这样带来了一些意想不到的好处:首先,由于复用view
的缘由,不须要每次去从新初始化view
了,这不经意间提高了咱们主界面恢复的时间(Navigation
内部是经过一个FrameLayout
做为Container
,对须要导航的Fragment
经过replace来实现界面跳转的);其次,因为复用的关系,view
的状态都还在,也就不须要咱们手动去保存和恢复状态了;同时,省去了将数据从新填充到视图上的过程。这个时候咱们须要处理一下数据初始化的问题,通常是不须要从新填充数据的。从新填充以后可能还会引起新的问题(好比我,→_→)。具体状况是这样子滴:在initView阶段咱们只是绑定了数据和视图的关系,并无填充数据,因此重复initView以后,虽然视图和逻辑不会发生变化,可是因为这个时候,RecyclerView
其实数据还未加载彻底,致使Transition Framework
没法找到匹配的transitionName
,这就又回到了以前的问题。因此在下面的基类里面避规了重复初始化的问题:生命周期
abstract class KeepViewFragment<VM : ViewModel> : BaseFragment<VM>() { protected var rootView: View? = null private var needInitView = false override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { addBackPressedListener() val layoutId = getLayoutId() ?: return null if (rootView == null) { rootView = inflater.inflate(layoutId, container, false) needInitView = true } return rootView } override fun init() { if (needInitView) initView(view!!) } override fun onDestroyView() { super.onDestroyView() needInitView = false } }
在将MainFragment
的基类改成KeepViewFragment
以后,共享元素动画终于恢复了正常(骗你的,←_←),心想,终于能够松一口气了。可一番演示以后,定睛一看,这返回过渡动画参数不正确吧(你唬谁呢,→_→),额,真是好事多磨,怎么就又出现新的问题了呢?
不啰嗦了,具体错误描述以下:
退出动画的起点位置始终为endView
(imageView
)的左上角(这里使用的共享元素动画为android.R.transition.move
)
这里也直接给出解决办法:即自定义transitionSet
,把move中包涵的changeImageTransform
去除就能够了,从字面上看来这就是为ImageView
量身定制的Transition
,可为何添加以后反而会出现共享元素过渡动画错误呢?但愿知道缘由的小伙伴告知我。
<?xml version="1.0" encoding="utf-8"?> <transitionSet> <changeTransform /> <changeClipBounds /> <changeBounds /> <!--<changeImageTransform />--> </transitionSet>
这一次的经历,让我初次解到了Transition Framework
这个组件,同时对ViewPager
和Navigation
的使用也更驾轻就熟,也更能熟练的运用MVVM,同时翻阅Adnroid Developers和Android Jetpack,就愈发让人着迷。接下来的一个计划是实现一个懒加载的ViewPager
。我我的认为(通常咱们只须要在ViewPager
中的Fragment
去支持懒加载,天然他应该由ViewPager
控制,而不是Fragment
)
泠音 写于2018/11/02