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

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

前言

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


App概览

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

image

能够看到Activity只是Fragment的一个载体,全部界面的跳转均有Fragment完成,均由Navigation控制。在第一次跳转发生后,发现了一个问题:布局

具体分析

问题一:跳转返回主界面发现回到初始状态

即原本跳转以前,咱们的RecyclerView是滚动到自定义的位置的,可是在跳转以后,再进行了返回以后,RecyclerView回到了顶部,也就是默认位置。初步猜测,该问题是Fragment从新建立了布局致使的,通过在Fragment各个生命周期回调方法内部打印log发如今跳转发生的时候,的确致使了Fragment1view销毁,回调了onDestroyView方法,而在跳转返回的时候也确实回调了onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?)方法。既然这样,那么为何返回以后主界面即view为何没有保存返回前的状态也就不难理解,原来是每次返回以后咱们所见到的主界面实际上是一个新的view,而不是跳转以前的那个view实例,天然也就没有跳转的状态了。动画

  • 这里插入一点多余的话语:有些同窗可能会问为何Fragment只回调了onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?)onDestroyView生命周期之间的方法,缘由是咱们所使用的ViewPagerAdapterFragmentPagerAdapter而不是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所在的FragmentonCreateView(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以后,共享元素动画终于恢复了正常(骗你的,←_←),心想,终于能够松一口气了。可一番演示以后,定睛一看,这返回过渡动画参数不正确吧(你唬谁呢,→_→),额,真是好事多磨,怎么就又出现新的问题了呢?

问题三:共享元素退出动画参数错误

不啰嗦了,具体错误描述以下:
退出动画的起点位置始终为endViewimageView)的左上角(这里使用的共享元素动画为android.R.transition.move

这里也直接给出解决办法:即自定义transitionSet,把move中包涵的changeImageTransform去除就能够了,从字面上看来这就是为ImageView量身定制的Transition,可为何添加以后反而会出现共享元素过渡动画错误呢?但愿知道缘由的小伙伴告知我。

<?xml version="1.0" encoding="utf-8"?>
<transitionSet>
    <changeTransform />
    <changeClipBounds />
    <changeBounds />
    <!--<changeImageTransform />-->
</transitionSet>

结语

这一次的经历,让我初次解到了Transition Framework这个组件,同时对ViewPagerNavigation的使用也更驾轻就熟,也更能熟练的运用MVVM,同时翻阅Adnroid Developers和Android Jetpack,就愈发让人着迷。接下来的一个计划是实现一个懒加载的ViewPager。我我的认为(通常咱们只须要在ViewPager中的Fragment去支持懒加载,天然他应该由ViewPager控制,而不是Fragment

泠音 写于2018/11/02

相关文章
相关标签/搜索