前事告一段落,在新的項目中,以爲採用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