DEVLOG 9.145 手寫一個RecyclerView總結(jié)
手寫RecyclerView的內(nèi)容參考了?手寫RecyclerView
歸納一下手寫RecyclerView中的一些重點和可能會遇到的面試題,這個總結(jié)主要談?wù)凴ecyclerView的設(shè)計、View的復(fù)用、View邊界判斷的思路。手寫的過程中遇到的問題也總結(jié)在這里:手寫RecyclerView問題總結(jié)
Github代碼連接:https://github.com/kolibreath/Practices/tree/master/MyRecyclerView

手寫RecyclerView設(shè)計思路
MyRecyclerView繼承自ViewGroup
手寫RecyclerView繼承自ViewGroup,眾所周知,ViewGroup是默認(rèn)設(shè)置了CLICKABLE 和 LONG_CLICKABLE = false,這樣一來onTouchEvent中的super就回返回false,所以最簡單的解決思路是在xml文件中設(shè)置MyRecyclerView的clickable=true。
ViewGroup繪制的三大流程: onMeasure
通常而言,ViewGroup首先應(yīng)該對子View進(jìn)行測量:調(diào)用measureChildren之類的方法測量子View。但是我們簡化的MyRecyclerView在初始化的時候并不知道子View的個數(shù),這個內(nèi)容是通過Adapter傳入的。所以我們的onMeasure沒有重寫:
? ? ?????測量工作可以在onLayout中執(zhí)行。我們默認(rèn)子View的高度為一個固定值,這樣比較方便。因為是指定的固定值,所以在測量的過程中我們設(shè)置測量模式為EXACTLY:
????
??
??????c.?ViewGroup繪制的三大流程: onLayout
????????在寫代碼之前我們應(yīng)當(dāng)思考一下onLayout需要執(zhí)行的任務(wù)
當(dāng)?shù)谝淮渭虞dData的時候,ViewGroup中沒有任何內(nèi)容,我們應(yīng)當(dāng)使用LayoutInflater加載View(先不考慮復(fù)用)。
使用LayoutInflater加載的View應(yīng)該被添加到ViewGroup中。(addView)但是LayoutInflater并不會產(chǎn)生寬高,因為寬高的產(chǎn)生需要結(jié)合LayoutParams+父容器的MeasureSpec(mode+size)。
第一次加載,沒有出現(xiàn)任何滑動行為時,我們通過屏幕的高度+View的高度推算應(yīng)該現(xiàn)實的View個數(shù)。
當(dāng)后續(xù)加載的過程中,先前顯示在ViewGroup中的內(nèi)容應(yīng)當(dāng)被清除
因此,我們可以得出以下的代碼:

但是,當(dāng)我們滑動的時候向上滑動或者向下滑動,View可能會被移除掉,被移除掉的View我們通過Recycler管理??紤]Recycler中保存View的數(shù)據(jù)結(jié)構(gòu),使用Stack是最為合理的。假設(shè)我們使用ArrayList,需要考慮ArrayList的擴(kuò)容行為;我們使用LinkedList,滑動手指過快,導(dǎo)致View被移除和補(bǔ)充得很快的時候,刪除和添加行為需要鏈接節(jié)點,也不是最優(yōu)的解決方案,所以使用Stack對于棧頂?shù)牟僮魇亲詈线m的。
如果我們支持多種不同的ViewType,我們就需要一個二維數(shù)組(棧)。
所以在上面的代碼makeAndSetupView(i, l,itemViewTop,r,itemViewBottom)?中我們通過obtain來獲取緩存的View。在得到這個View對象之后在進(jìn)行l(wèi)ayout。
這里我想說明一點,MyAdapter是我們實現(xiàn)的一個接口類,里面定義了很多回調(diào)函數(shù)。其中
onBindViewHolder還有onCreateViewHolder都應(yīng)該返回ViewHolder對象,但是我們?yōu)榱朔奖闫陂g直接返回View。 并且onBindViewHolder應(yīng)該是返回Unit(void),但是在這里如果返回Unit,還需要重新綁定一遍,我覺得也沒有必要,于是采用了視頻中的寫法。 這里還需要注意setTag的兩個小細(xì)節(jié):
setTag的第一個參數(shù)int 會加載資源文件,所以不能給一個Int定值
setTag使用<key-value>的模式,因為View.setTag也是非常常見的操作。如果在其他地方對于View進(jìn)行了setTag,在這里又進(jìn)行setTag,會改變預(yù)設(shè)的行為。

obtain獲取緩存的View。我們需要重寫removeView,以便存放View到Recycler中。

View的另外的draw流程讓View自行完成,這里就不說了。
onLayout只是一個靜態(tài)的行為,滑動的時候可以理解為滑動一點,onLayout一點。我們需要重寫消息處理機(jī)制,這里主要是重寫onInterceptTouchEvent和onTouchEvent。
onInterceptTouchEvent:
這個方法需要和onTouchEvent進(jìn)行配合。我們現(xiàn)在在一個ViewGroup中,要想onTouchEvent被執(zhí)行,我們需要返回true,表示不將消息交給子控件消費。當(dāng)滑動的動作超過touchSlop時返回true,這樣就會調(diào)用我們重寫的onTouchEvent:
? ?2. onTouchEvent:?
超過TouchSlop 我直接滑動:
3. 重寫ScrollBy?
因為scrollBy本身是通過調(diào)整canvas的位置進(jìn)行View的滑動,所以我們不能使用這個方法。我們必須要重寫。
重寫思路:
如果MyRecyclerView向上滑動,下面需要新的ItemView進(jìn)來補(bǔ)充,同時上面的View需要出去;向下滑動同理
如果MyRecyclerView向上滑動,但是已經(jīng)是第一個View了,控制不能上劃。同理,如果是最后一個View,我們不能下滑,所以我們可以定義scrollBound做邊界控制:
????3. 上劃需要從下面補(bǔ)充新的View;下滑需要從上面補(bǔ)充新的View,這也需要在scrollBy中實現(xiàn)。具體的邏輯我注釋在代碼中了:
滑動完成之后需要重新布局: