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重疊問題,故我們還需要手動去解決這個問題。


App概覽

談完Navigation,就讓我們先來看一下App簡化後的層級:

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

繼續回到問題,既然我們已經知道了問題出在了哪裏,那麼現在就需要着手解決問題了。首先我想到的方案是是這樣的:

方案一

既然重新返回導致重新創建的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所在的FragmentonCreateView(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之後,共享元素動畫終於恢復了正常(騙你的,←_←),心想,終於可以鬆一口氣了。可一番演示之後,定睛一看,這返回過渡動畫參數不正確吧(你唬誰呢,→_→),額,真是好事多磨,怎麼就又出現新的問題了呢?

問題三:共享元素退出動畫參數錯誤

不囉嗦了,具體錯誤描述如下:
退出動畫的起點位置始終爲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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章