最新消息:XAMPP默认安装之后是很不安全的,我们只需要点击左方菜单的 "安全"选项,按照向导操作即可完成安全设置。

Fragment中ViewPager嵌套Fragment,共享元素错位解决方案

XAMPP下载 文, 员 11浏览 0评论

Fragment中ViewPager嵌套Fragment,共享元素错位解决方案
前言
前事告一段落,在新的项目中,觉得采用ViewPager+Fragment的方案作为主界面,创建的过程很快,也没有遇到什么问题。但在实现界面跳转的时候,才发现单Activity多Fragment结构的坑点还是有很多。这次采用了Google最新发布的Android Jetpack组件中的Navigation来控制Fragment跳转,使用途中有优点,也有缺点,不过在整体算来,还是极大打简化了我们需要编写的代码。例如:NavHostFragment.findNavController(this).navigate(…)默认是利用FragmentManager.FragmentTransaction.replace()来进行导航,我们我无法通过干预其过程来使用hide()和show(),不过从某种程度上看来也不算是缺点,相反,我觉得这正好统一了Fragment的使用吧,比较通过hide()和show()来展示Fragment有可能会出发Fragment重叠问题,故我们还需要手动去解决这个问题。

App概览
谈完Navigation,就让我们先来看一下App简化后的层级:

image

可以看到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。
继续回到问题,既然我们已经知道了问题出在了哪里,那么现在就需要着手解决问题了。首先我想到的方案是是这样的:

方案一
既然重新返回导致重新创建的view使其回到了初始状态那么我们只需要在跳转之前保存view的相关状态与viewModel中即可,初期需要保存的状态并不多,暂时只需要RecyclerView滚动位置即可,并且在onDestroyView()中调用即可。具体代码如下:

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状态的确跟跳转之前相同的,此时,我以为问题到这儿就算是结束了,可是随后新的问题,确又令人煞费苦心。

问题二:共享元素退出动画失效
因为RecyclerView中的Item都设定了ItemClick点击事件,点击之后跳转到相应详情页,为了使跳转不那么生硬,这里采用了共享元素+其他动画的方式来实现过渡,但就是在应用共享元素动画的时候又出现了新的问题:具体表现为,共享元素在跳转发生后的进入动画完全正常,但是点击Back返回的时候,生硬的切回了主界面。我的共享元素的返回动画呢???文档不是说设定了共享元素进入动画后,可以不设定返回动画,系统会按照和进入相反的动画进行过渡。我以为是我没有给共享元素设定返回动画的原因,于是又加上了设定返回动画的代码。这次我满怀期待的重新构建了一遍项目,期望它能如我所愿,可惜世事总不如意,纳尼?我的返回动画呢,为什么还不出来。在经历了各种尝试无果之后,没办法只能先给Fragemnt1这个整体加了一个退出动画来暂时顶替。虽然视觉上是不那么生硬了,但是由于进入和退出动画没有联系,在感知上,总有一种不合理的感觉。就这么过去了一天,可还是一点头绪也没有。在第二天的时候突然想到了一个问题,因为在之前使用Fragment+ViewPager+FragmentStatePagerAdapter的时候遇到过返回后ViewPager不显示的问题,那个时候查阅资料,最后找的的解决办法是在ViewPager所在的Fragment的onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?)方法中判断一次rootView是否为空,如果为空,则inflate一个新的view否则就将rootView从它的父视图中移除(如果有的话),然后return rootView,即:
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自然也无法生成相应的过渡动画,这个时候我的脑海中就浮现出了另一个方案:

方案二
这次我们既然了解到了共享元素返回动画失效的原因,是由于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)

转载请注明:XAMPP中文组官网 » Fragment中ViewPager嵌套Fragment,共享元素错位解决方案