Fragment中的RecycleView的轉(zhuǎn)場動畫與保存焦點(diǎn)
這一周鄙渣都被一個需求困擾,這個需求就是給FAQ列表添加一個shareElement的轉(zhuǎn)場過度動畫,并且在切回來的時候保存焦點(diǎn)。其中FAQ列表是Fragment下的一個RecycleView,F(xiàn)AQ詳情頁是另一個Fragment。
實(shí)際上這個需求完成非常的簡單,貼一下代碼
fun showDetailFragment(position: Int, faqInfo: FaqInfo, view: View) {
val faqContentFragment = FaqContentFragment().apply {
faqTitle = faqInfo.title
faqContent = faqInfo.content
transitionName = "faqTitle$position"
}
//SDK21以上設(shè)置共享內(nèi)容的轉(zhuǎn)場動畫
//21以下轉(zhuǎn)場動畫需要在這設(shè)置,不然無進(jìn)入的動畫效果
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
fragmentManager?.apply {
beginTransaction()
.addSharedElement(view, ViewCompat.getTransitionName(view)
?: "faqTitle$position")
.replace(R.id.fl_faq_container, faqContentFragment)
.addToBackStack(null)
.commit()
}
} else {
fragmentManager?.apply {
beginTransaction()
.setTransition((FragmentTransaction.TRANSIT_FRAGMENT_FADE))
.addToBackStack(null)
.commit()
}
}
}
接下來說一說在其中踩過的坑
使用supportFragmentManager與fragmentManager的效用不一致
這個問題鄙渣也只是知其然不知其所以然,只知道效果確實(shí)不一樣。
鄙渣使用的均是androidx.fragment.app下面的FragmentManager類,不存在一個是被deprecated的v4包中的getFragmentManager(),一個是support包下的getSuppotrFragmentManager()的的情況。
前者是在activity中獲得的supportFragmentManager,后者是在fragment中獲得的fragmentManager。
并且通過hashcode()進(jìn)行比對,兩個fragmentManager擁有一樣的hashCode。但是前者就是無法正確添加SharedElement。
如果有大佬看到這篇文章,麻煩告訴一下鄙渣具體原因。或者之后鄙渣研究出來了再貼結(jié)果。
保存RecycleView跳轉(zhuǎn)前的焦點(diǎn)
這個需求大多數(shù)開發(fā)者都用不上,只有鄙渣在的這個行當(dāng)需要,也簡單地說一下思路。一開始鄙渣想的是利用hide和show對第一個Fragment進(jìn)行操作,第二個Fragment手動add和remove。并且覆寫onBackPressed(),根據(jù)Fragment的存在與否決定增刪改等操作。
這個思路并沒有完全解決焦點(diǎn)的問題,依舊需要通過存一個全局變量來保存焦點(diǎn),從而在view繪制后重新調(diào)用requestFocus()。
但這樣一來就完全可以使用replace與addToBackStack操作,不需要進(jìn)行上述那么繁瑣的操作。
但是這個時候又出現(xiàn)了另外一個問題,即使是在Fragment的onStart回調(diào)中,RecycleView也并沒有繪制,其中獲取到的ChildView數(shù)量為0。查閱了網(wǎng)上的資料,有一種比較麻煩的方法進(jìn)行實(shí)現(xiàn),就是使用viewTreeObserver。給RecycleView綁定一個onDrawListner,即可在此時找到子view。
只是此處也是伴隨著一個棘手的問題:何時解綁onDrawListner。如果不進(jìn)行解綁,每一次滑動RecycleView都會進(jìn)行一次繪制,那么焦點(diǎn)就被固定死了,無法移動;規(guī)范要求也不能在onDrawListner內(nèi)部進(jìn)行解綁操作;如果進(jìn)行delay后再解綁,那我也一樣可以delay后再找到焦點(diǎn),一點(diǎn)也不優(yōu)雅精致。最后我放棄了這種方法。
接下來是最終使用的方法,在viewHolder內(nèi)部進(jìn)行操作,貼一下代碼
class FaqAdapter(val data: List<FaqInfo>, private val faqItemClickListener: FaqItemClickListener) : RecyclerView.Adapter<FaqViewHolder>() {
var thisRecyclerView: RecyclerView? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FaqViewHolder {
val itemView: View = LayoutInflater.from(parent.context).inflate(R.layout.faq_item, parent, false)
return FaqViewHolder(itemView)
}
override fun getItemCount(): Int {
return data.size
}
override fun onBindViewHolder(holder: FaqViewHolder, position: Int) {
holder.setText(R.id.tv_faq_item_title, data[position].title)
val faqItemTitle = holder.getView<TextView>(R.id.tv_faq_item_title)
val faqItemContainer = holder.itemView.findViewById<ConstraintLayout>(R.id.cl_faq_item_container)
/*
* 設(shè)置轉(zhuǎn)場動畫所需
* */
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
faqItemTitle.transitionName = "faqTitle$position"
}
/*
* 點(diǎn)擊事件的回調(diào)
* */
faqItemContainer.setOnClickListener {
faqItemClickListener.onFaqItemClickListener(position, data[position], faqItemTitle)
}
/*
* 回來recycleView找到上次的焦點(diǎn)
* */
if (position == latestPosition) {
thisRecyclerView?.smoothScrollToPosition(latestPosition)
faqItemContainer.requestFocus()
}
/*
* 記錄焦點(diǎn)變換的位置
* */
faqItemContainer.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
latestPosition = position
}
}
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
thisRecyclerView = recyclerView
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
super.onDetachedFromRecyclerView(recyclerView)
thisRecyclerView = null
}
companion object {
var latestPosition = 0
}
}
其實(shí)代碼已經(jīng)很清晰明朗了。首先在attach的時候獲取此時的RecycleView(detach的時候同樣要記得施放),然后在綁定ViewHolder的時候進(jìn)行判斷,如果是記錄中的位置,那么就滑動到該位置并獲取焦點(diǎn)。并且附帶一個焦點(diǎn)變化監(jiān)聽器,如果某個位置的view獲得了焦點(diǎn),就記錄該控件的位置。這樣一來代碼就十分簡潔優(yōu)雅了。
動畫的進(jìn)入效果消失
這個其實(shí)是一個版本問題。由于往上要兼容到8.0,向下最低兼容到4.4,而sharedElement是21(5.0)后才有,所以之前需要統(tǒng)一設(shè)置成淡入淡出的效果。而此時卻有一個莫名其妙的問題:在Fragment中的設(shè)置對進(jìn)入動畫不生效。貼一下設(shè)置代碼。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
sharedElementEnterTransition = DetailTransition()
sharedElementReturnTransition = DetailTransition()
}
allowEnterTransitionOverlap = true
allowReturnTransitionOverlap = true
enterTransition = Fade()
returnTransition = Fade()
exitTransition = Fade()
}
實(shí)際上這個的解決方法并不是難題,低版本有自己對應(yīng)的設(shè)置方法。鄙渣疑惑的地方是,退出動畫生效,那么為什么進(jìn)入動畫會不生效?這個問題鄙渣也沒有研究出結(jié)果,希望有大佬能夠提點(diǎn)一下,順帶貼一下現(xiàn)在的解決設(shè)置。
fragmentManager?.apply {
beginTransaction()
.setTransition((FragmentTransaction.TRANSIT_FRAGMENT_FADE))
.addToBackStack(null)
.commit()
}
其中setTransition((FragmentTransaction.TRANSIT_FRAGMENT_FADE))就是設(shè)置動畫效果的語句。
最后貼一下示意圖
