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

相關文章
相關標籤/搜索