本文是
RecyclerView源碼分析系列最後一篇文章
, 主要講一下我我的對於RecycleView
的使用的一些思考以及一些常見的問題怎麼解決。先來看一下使用RecycleView
時常見的問題以及一些需求。android
這個每每是由於你沒有設置LayoutManger
。 沒有LayoutManger
的話RecycleView
是沒法佈局的,便是沒法展現數據,下面是RecycleView
佈局的源碼:git
void dispatchLayout() { //沒有設置 Adapter 和 LayoutManager, 都不可能有內容
if (mAdapter == null) {
Log.e(TAG, "No adapter attached; skipping layout");
// leave the state in START
return;
}
if (mLayout == null) {
Log.e(TAG, "No layout manager attached; skipping layout");
// leave the state in START
return;
}
}
複製代碼
即Adapter
或Layout
任意一個爲null,就不會執行佈局操做。github
RecycleView
在滾動過程當中ViewHolder
是會不斷複用的,所以就會帶着上一次展現的UI信息(也包含滾動狀態), 因此在設置一個ViewHolder
的UI時,儘可能要作resetUi()
操做:bash
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
holder.itemView.resetUi()
...設置信息UI
}
複製代碼
resetUi()
這個方法就是用來把Ui還原爲最初的操做。固然若是你的每一次bindData
操做會對每個UI對象從新賦值的話就不須要有這個操做。就不會出現itemView
的UI混亂問題。app
咱們可能會有這樣的需求: 當RecycleView
中的特定Item
滾動到某個位置時作一些操做。好比某個Item
滾動到頂部時,展現搜索框。那怎麼實現呢?框架
首先要獲取的Item確定處於數據源的某個位置而且確定要展現在屏幕。所以咱們能夠直接獲取這個Item
的ViewHolder
:ide
val holder = recyclerView.findViewHolderForAdapterPosition(speicalItemPos) ?: return
val offsetWithScreenTop = holder.itemview.top
if(offsetWithScreenTop <= 0){ //這個ItemView已經滾動到屏幕頂部
//do something
}
複製代碼
smoothScrollToPosition()
你們應該都用過,若是滾動二、3個Item。那麼總體的用戶體驗仍是很是棒的。源碼分析
可是,若是你滾動20個Item,那這個體驗可能就會就不好了,由於用戶看到的多是下面這樣子:佈局
恩,滾動的時間有點長。所以對於這種case其實我推薦直接使用scrollToPosition(20)
,效果要比這個好。 但是若是你就是想在200ms
內從Item 1
滾到Item 20
怎麼辦呢?post
能夠參考StackOverflow上的一個答案。大體寫法是這樣的:
//自定義 LayoutManager, Hook smoothScrollToPosition 方法
recyclerView.layoutManager = object : LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) {
override fun smoothScrollToPosition(recyclerView: RecyclerView?, state: RecyclerView.State?, position: Int) {
if (recyclerView == null) return
val scroller = get200MsScroller(recyclerView.context, position * 500)
scroller.targetPosition = position
startSmoothScroll(scroller)
}
}
private fun get200MsScroller(context: Context, distance: Int): RecyclerView.SmoothScroller = object : LinearSmoothScroller(context) {
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float {
return (200.0f / distance) //表示滾動 distance 花費200ms
}
}
複製代碼
好比上面我把時間改成10000
,那麼就是用10s的時間完成這個滾動操做。
先描述一下這個需求: RecyclerView
中的每一個ItemView
的高度都是不固定的。我數據源中有20條數據,在沒有渲染的狀況下我想知道這個20條數據被RecycleView
渲染後的總共高度, 好比下面這個圖片:
怎麼作呢?個人思路是利用LayoutManager
來測量,由於RecycleView
在對子View
進行佈局時就是用LayoutManager
來測量子View
來計算還有多少剩餘空間可用,源碼以下:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler); //這個方法會向 recycler要一個View
...
measureChildWithMargins(view, 0, 0); //測量這個View的尺寸,方便佈局, 這個方法是public
...
}
複製代碼
因此咱們也能夠利用layoutManager.measureChildWithMargins
方法來測量,代碼以下:
private fun measureAllItemHeight():Int {
val measureTemplateView = SimpleStringView(this)
var totalItemHeight =
dataSource.forEach { //dataSource當前中的全部數據
measureTemplateView.bindData(it, 0) //設置好UI數據
recyclerView.layoutManager.measureChild(measureTemplateView, 0, 0) //調用源碼中的子View的測量方法
currentHeight += measureTemplateView.measuredHeight
}
return totalItemHeight
}
複製代碼
但要注意的是,這個方法要等佈局穩定的時候才能夠用,若是你在Activity.onCreate
中調用,那麼應該post
一下, 即:
recyclerView.post{
val totalHeight = measureAllItemHeight()
}
複製代碼
這個異常一般是因爲Adapter的數據源大小
改變沒有及時通知RecycleView
作UI刷新致使的,或者通知的方式有問題。 好比若是數據源變化了(好比數量變少了),而沒有調用notifyXXX
, 那麼此時滾動RecycleView
就會產生這個異常。
解決辦法很簡單 : Adapter的數據源
改變時應當即調用adapter.notifyXXX
來刷新RecycleView
。
分析一下這個異常爲何會產生:
在RecycleView刷新機制
一文介紹過,RecycleView
的滾動操做是不會走RecycleView
的正常佈局過程的,它直接根據滾動的距離來擺放新的子View
。 想象一下這種場景,原來數據源集合中 有8個Item,而後刪除了4個後沒有調用adapter.notifyXXX()
,這時直接滾動RecycleView
,好比滾動將要出現的是第6個Item,LinearLayoutManager
就會向Recycler
要第6個Item的View:
Recycler.tryGetViewHolderForPositionByDeadline()
:
final int offsetPosition = mAdapterHelper.findPositionOffset(position); //position是6
if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) { //但此時 mAdapter.getItemCount() = 5
throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
+ "position " + position + "(offset:" + offsetPosition + ")."
+ "state:" + mState.getItemCount() + exceptionLabel());
}
複製代碼
即這時就會拋出異常。若是調用了adapter.notifyXXX
的話,RecycleView
就會進行一次徹底的佈局操做,就不會有這個異常的產生。
其實還有不少異常和這個緣由差很少,好比:IllegalArgumentException: Scrapped or attached views may not be recycled. isScrap:false
(不少狀況也是因爲沒有及時同步UI和數據)
因此在使用RecycleView
時必定要注意保證數據和UI的同步,數據變化,及時刷新RecyclerView, 這樣就能避免不少crash。
如今不少app都會使用RecyclerView
來構建一個頁面,這個頁面中有各類卡片類型。爲了支持快速開發咱們一般會對RecycleView
的Adapter
作一層封裝來方便咱們寫各類類型的卡片,下面這種封裝是我認爲一種比較好的封裝:
/**
* 對 RecyclerView.Adapter 的封裝。方便業務書寫。 業務只須要處理 (UI Bean) -> (UI View) 的映射邏輯便可
*/
abstract class CommonRvAdapter<T>(private val dataSource: List<T>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val item = createItem(viewType)
return CommonViewHolder(parent.context, parent, item)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val commonViewHolder = holder as CommonViewHolder<T>
commonViewHolder.adapterItemView.bindData(dataSource[position], position)
}
override fun getItemCount() = dataSource.size
override fun getItemViewType(position: Int): Int {
return getItemType(dataSource[position])
}
/**
* @param viewType 須要建立的ItemView的viewType, 由 {@link getItemType(item: T)} 根據數據產生
* @return 返回這個 viewType 對應的 AdapterItemView
* */
abstract fun createItem(viewType: Int): AdapterItemView<T>
/**
* @param T 表明dataSource中的一個data
*
* @return 返回 顯示 T 類型的data的 ItemView的 類型
* */
abstract fun getItemType(item: T): Int
/**
* Wrapper 的ViewHolder。 業務沒必要理會RecyclerView的ViewHolder
* */
private class CommonViewHolder<T>(context: Context?, parent: ViewGroup, val adapterItemView: AdapterItemView<T>)
//這一點作了特殊處理,若是業務的AdapterItemView自己就是一個View,那麼直接當作ViewHolder的itemView。 不然inflate出一個view來當作ViewHolder的itemView
: RecyclerView.ViewHolder(if (adapterItemView is View) adapterItemView else LayoutInflater.from(context).inflate(adapterItemView.getLayoutResId(), parent, false)) {
init {
adapterItemView.initViews(itemView)
}
}
}
/**
* 能被 CommonRvAdapter 識別的一個 ItemView 。 業務寫一個RecyclerView中的ItemView,只須要實現這個接口便可。
* */
interface AdapterItemView<T> {
fun getLayoutResId(): Int
fun initViews(var1: View)
fun bindData(data: T, post: Int)
}
複製代碼
爲何我認爲這是一個不錯的封裝?
abstract fun createItem(viewType: Int): AdapterItemView<T>
abstract fun getItemType(item: T): Int
複製代碼
即業務寫一個Adapter
只須要對 UI 數據 -> UI View 作映射便可, 不須要關心RecycleView.ViewHolder
的邏輯。
AdapterItemView
, ItemView足夠靈活因爲封裝了RecycleView.ViewHolder
的邏輯,所以對於UI item view
業務方只須要返回一個實現了AdapterItemView
的對象便可。能夠是一個View
,也能夠不是一個View
, 這是由於CommonViewHolder
在構造的時候對它作了兼容:
val view : View = if (adapterItemView is View) adapterItemView else LayoutInflater.from(context).inflate(adapterItemView.getLayoutResId(), parent, false)
複製代碼
即若是實現了AdapterItemView
的對象自己就是一個View
,那麼直接把它當作ViewHolder
的itemview
,不然就inflate
出一個View
做爲ViewHolder
的itemview
。
其實這裏我比較推薦實現AdapterItemView
的同時直接實現一個View
,即不要把inflate
的工做交給底層框架。好比這樣:
private class SimpleStringView(context: Context) : FrameLayout(context), AdapterItemView<String> {
init {
LayoutInflater.from(context).inflate(getLayoutResId, this) //本身去負責inflate工做
}
override fun getLayoutResId() = R.layout.view_test
override fun initViews(var1: View) {}
override fun bindData(data: String, post: Int) { simpleTextView.text = data }
}
複製代碼
爲何呢?緣由有兩點 :
SimpleStringView
不只能夠在RecycleView
中當一個itemView
,也能夠在任何地方使用。但其實直接繼承自一個View
是有坑的,即上面那行inflate代碼LayoutInflater.from(context).inflate(getLayoutResId, this)
它實際上是把xml
文件inflate成一個View
。而後add到你ViewGroup
中。由於SimpleStringView
就是一個FrameLayout
,全部至關於add到這個FrameLayout
中。這其實就有問題了。好比你的佈局文件是下面這種:
<FrameLayout>
.....
</FrameLayout>
複製代碼
這就至關於你可能多加了一層無用的父View
全部若是是直接繼承自一個View的話,我推薦這樣寫:
<merge>
標籤來消除這層無用的父View, 即上面的<FrameLayout>
改成<merge>
固然,若是你不須要對這個View作複用的話你能夠不用直接繼承自View
,只實現AdapterItemView
接口, inflate的工做交給底層框架便可。這樣是不會產生上面這個問題的。
這篇文章就先說這麼多吧。歡迎關注個人Android進階計劃。看更多幹貨。
另外歡迎瀏覽個人
RecyclerView源碼分析系列
的其餘文章: