深入浅出 RecyclerView
寫在前面
起深入淺出這名字的時(shí)候我是慎重又慎重的,生怕被人罵標(biāo)題黨,寫的什么破玩意還敢說(shuō)深入淺出。所以還是請(qǐng)大家不要抱著太高的期望,因?yàn)闆](méi)有期望就沒(méi)有失望,就像陳潤(rùn)說(shuō)的,超預(yù)期嘛。全當(dāng)看小說(shuō)的心情來(lái)看這系列文章了。
這篇文章分三個(gè)部分,簡(jiǎn)單跟大家講一下 RecyclerView 的常用方法與奇葩用法;工作原理與ListView比較;源碼解析;
1. 常用方法
RecyclerView 與 ListView、GridView 類似,都是可以顯示同一種類型 View 的集合的控件。
首先看看最簡(jiǎn)單的用法,四步走:
1.1 接入 build.gradle 文件中加入
compile 'com.android.support:recyclerview-v7:24.0.0'1.2 創(chuàng)建對(duì)象
RecyclerView recyclerview = (RecyclerView) findViewById(R.id.recyclerview);1.3 設(shè)置顯示規(guī)則
recyclerview.setLayoutManager(new LinearLayoutManager( this, LinearLayoutManager.VERTICAL, false));RecyclerView 將所有的顯示規(guī)則交給一個(gè)叫 LayoutManager 的類去完成了。
LayoutManager 是一個(gè)抽象類,系統(tǒng)已經(jīng)為我們提供了三個(gè)默認(rèn)的實(shí)現(xiàn)類,分別是 LinearLayoutManager、 GridLayoutManager 、 StaggeredGridLayoutManager,從名字我們就能看出來(lái)了,分別是,線性顯示、網(wǎng)格顯示、瀑布流顯示。當(dāng)然你也可以通過(guò)繼承這些類來(lái)擴(kuò)展實(shí)現(xiàn)自己的 LayougManager。
1.4 設(shè)置適配器
recyclerview.setAdapter(adapter);適配器,同 ListView 一樣,用來(lái)設(shè)置每個(gè)item顯示內(nèi)容的。 通常,我們寫 ListView 適配器,都是首先繼承 BaseAdapter,實(shí)現(xiàn)四個(gè)抽象方法,創(chuàng)建一個(gè)靜態(tài)ViewHolder , getView() 方法中判斷 convertView 是否為空,創(chuàng)建還是獲取 viewholder 對(duì)象。
而 RecyclerView 也是類似的步驟,首先繼承RecyclerView.Adapter類,實(shí)現(xiàn)三個(gè)抽象方法,創(chuàng)建一個(gè)靜態(tài)的 ViewHolder。不過(guò) RecyclerView 的 ViewHolder 創(chuàng)建稍微有些限制,類名就是上面繼承的時(shí)候泛型中聲明的類名(好像反了,應(yīng)該是上面泛型中的類名應(yīng)該是這個(gè)holder的類名);并且 ViewHolder 必須繼承自RecyclerView.ViewHolder類。
public class DemoAdapter extends RecyclerView.Adapter<DemoAdapter.VH> {private List<Data> dataList;private Context context;public DemoAdapter(Context context, ArrayList<Data> datas) {this.dataList = datas;this.context = context;}@Overridepublic VH onCreateViewHolder(ViewGroup parent, int viewType) {return new VH(View.inflate(context, android.R.layout.simple_list_item_2, null));}@Overridepublic void onBindViewHolder(VH holder, int position) {holder.mTextView.setText(dataList.get(position).getNum());}@Overridepublic int getItemCount() {return dataList.size();}public static class VH extends RecyclerView.ViewHolder {TextView mTextView;public VH(View itemView) {super(itemView);mTextView = (TextView) itemView.findViewById(android.R.id.text1);}} }2. 更多方法
除了常用方法,當(dāng)然還有不常用的。
2.1 瀑布流與滾動(dòng)方向
前面已經(jīng)介紹過(guò),RecyclerView實(shí)現(xiàn)瀑布流,可以通過(guò)一句話設(shè)置:recycler.setLayoutManager(new StaggeredGridLayoutManager(2, VERTICAL))就可以了。
其中 StaggeredGridLayoutManager 第一個(gè)參數(shù)表示列數(shù),就好像 GridView 的列數(shù)一樣,第二個(gè)參數(shù)表示方向,可以很方便的實(shí)現(xiàn)橫向滾動(dòng)或者縱向滾動(dòng)。
使用 demo 可以查看:Github 【RecyclerView簡(jiǎn)單使用】
2.2 添加刪除 item 的動(dòng)畫
同 ListView 每次修改了數(shù)據(jù)源后,都要調(diào)用 notifyDataSetChanged() 刷新每項(xiàng) item 類似,只不過(guò)RecyclerView 還支持局部刷新 notifyItemInserted(index);、 notifyItemRemoved(position)、notifyItemChanged(position)。
在添加或刪除了數(shù)據(jù)后,RecyclerView 還提供了一個(gè)默認(rèn)的動(dòng)畫效果,來(lái)改變顯示。同時(shí),你也可以定制自己的動(dòng)畫效果:模仿 DefaultItemAnimator 或直接繼承這個(gè)類,實(shí)現(xiàn)自己的動(dòng)畫效果,并調(diào)用recyclerview.setItemAnimator(new DefaultItemAnimator()); 設(shè)置上自己的動(dòng)畫。
使用 demo 可以查看:Github 【RecyclerView默認(rèn)動(dòng)畫】
2.3 LayoutManager的常用方法
| findFirstVisibleItemPosition() | 返回當(dāng)前第一個(gè)可見 Item 的 position |
| findFirstCompletelyVisibleItemPosition() | 返回當(dāng)前第一個(gè)完全可見 Item 的 position |
| findLastVisibleItemPosition() | 返回當(dāng)前最后一個(gè)可見 Item 的 position |
| findLastCompletelyVisibleItemPosition() | 返回當(dāng)前最后一個(gè)完全可見 Item 的 position. |
| scrollBy() | 滾動(dòng)到某個(gè)位置。 |
2.4adapter封裝
其實(shí)很早之前寫過(guò)一篇關(guān)于 RecyclerView 適配器的封裝,所以這不再贅述了,傳送門:RecyclerView的通用適配器
使用 demo 可以查看:Github 【RecyclerView通用適配器演示】
3. 吐槽
OnItemTouchListener 什么鬼?
用習(xí)慣了 ListView 的 OnItemClickListener ,RecyclerView 你的 OnItemClickListener 呢?
Tell me where do I find, something like ListView listener ?
好吧,翻遍了 API 列表,就找到了個(gè) OnItemTouchListener ,這特么什么鬼,我干嘛要對(duì)每個(gè) item 監(jiān)聽觸摸屏事件。
萬(wàn)萬(wàn)沒(méi)想到,最終我還是在 Google IO 里面的介紹找到了原因。原來(lái)是 Google 的工程師分不清究竟是改給 listview 的 item 添加點(diǎn)擊事件,還是應(yīng)該給每個(gè) item 的 view 添加點(diǎn)擊事件,索性就不給OnItemClickListener 了,然后在 support demo 里面,你就會(huì)發(fā)現(xiàn),RecyclerView 的 item 點(diǎn)擊事件都是寫在了 adapter 的 ViewHolder 里面。
當(dāng)然,除了 support demo 包里面使用的在 ViewHolder 里面設(shè)置點(diǎn)擊事件以外,我還寫好了一個(gè)RecyclerView 使用的 OnItemClickListener 代碼請(qǐng)見:RecyclerItemClickListener.java
需要一提的是,網(wǎng)上有很多這種類似的 ItemClickListener ,在使用的時(shí)候一定注意一個(gè)問(wèn)題,就是循環(huán)引用問(wèn)題。比如 listener 里面持有了一個(gè) recyclerview, 而這個(gè) recyclerview 在調(diào)用 setListener() 的時(shí)候又持有了一個(gè) listener。盡管 Java 虛擬機(jī)現(xiàn)在可以解決這種問(wèn)題了,但作為代碼編寫者,這種寫法還是應(yīng)該盡量避免的。
divider 跑哪了?
在ListView中設(shè)置 divider 非常簡(jiǎn)單,只需要在 XML 文件中設(shè)置就可以了,同時(shí)還可以設(shè)置 divider 高度。
android:divider="@android:color/black" android:dividerHeight="2dp"而在RecyclerView里面,想實(shí)現(xiàn)這兩種需求,稍微復(fù)雜一點(diǎn),需要自己繼承RecyclerView.ItemDecoration來(lái)實(shí)現(xiàn)想要實(shí)現(xiàn)的方法。
雖說(shuō)這樣寫靈活多了,但是要額外寫一個(gè)類去做難免麻煩,這里大家可以看我已經(jīng)實(shí)現(xiàn)好的一個(gè)封裝,包括顯示純色divider、顯示圖片divider、divider的上下左右的間距、寬高設(shè)置 應(yīng)該可以滿足基本需求了: Divider.java
使用 demo 可以查看:Github 【自定義 Divider 使用】
4. 五虎上將工作原理
借用 Google IO 視頻中的一張截圖,視頻的完整地址可查看: RecyclerView ins and outs - Google I/O 2016
其實(shí)上圖中并沒(méi)有寫完整,大 boss RecyclerView 應(yīng)該有這五虎上將:
| RecyclerView.LayoutManager | 負(fù)責(zé)Item視圖的布局的顯示管理 |
| RecyclerView.ItemDecoration | 給每一項(xiàng)Item視圖添加子View,例如可以進(jìn)行畫分隔線之類 |
| RecyclerView.ItemAnimator | 負(fù)責(zé)處理數(shù)據(jù)添加或者刪除時(shí)候的動(dòng)畫效果 |
| RecyclerView.Adapter | 為每一項(xiàng)Item創(chuàng)建視圖 |
| RecyclerView.ViewHolder | 承載Item視圖的子布局 |
LayoutManager工作原理
java.lang.Object ? android.view.View ? android.view.ViewGroup ? android.support.v7.widget.RecyclerView首先是 RecyclerView 繼承關(guān)系,可以看到,與 ListView 不同,他是一個(gè) ViewGroup。既然是一個(gè) View,那么就不可少的要經(jīng)歷 onMeasure()、onLayout()、onDraw() 這三個(gè)方法。 實(shí)際上,RecyclerView 就是將onMeasure()、onLayout() 交給了 LayoutManager 去處理,因此如果給 RecyclerView 設(shè)置不同的 LayoutManager 就可以達(dá)到不同的顯示效果,因?yàn)閛nMeasure()、onLayout()都不同了嘛。
ItemDecoration 工作原理
ItemDecoration 是為了顯示每個(gè) item 之間分隔樣式的。它的本質(zhì)實(shí)際上就是一個(gè) Drawable。當(dāng) RecyclerView 執(zhí)行到 onDraw() 方法的時(shí)候,就會(huì)調(diào)用到他的 onDraw(),這時(shí),如果你重寫了這個(gè)方法,就相當(dāng)于是直接在 RecyclerView 上畫了一個(gè) Drawable 表現(xiàn)的東西。 而最后,在他的內(nèi)部還有一個(gè)叫g(shù)etItemOffsets()的方法,從字面就可以理解,他是用來(lái)偏移每個(gè) item 視圖的。當(dāng)我們?cè)诿總€(gè) item 視圖之間強(qiáng)行插入繪畫了一段 Drawable,那么如果再照著原本的邏輯去繪 item 視圖,就會(huì)覆蓋掉 Decoration 了,所以需要getItemOffsets()這個(gè)方法,讓每個(gè) item 往后面偏移一點(diǎn),不要覆蓋到之前畫上的分隔樣式了。
ItemAnimator
每一個(gè) item 在特定情況下都會(huì)執(zhí)行的動(dòng)畫。說(shuō)是特定情況,其實(shí)就是在視圖發(fā)生改變,我們手動(dòng)調(diào)用notifyxxxx()的時(shí)候。通常這個(gè)時(shí)候我們會(huì)要傳一個(gè)下標(biāo),那么從這個(gè)標(biāo)記開始一直到結(jié)束,所有 item 視圖都會(huì)被執(zhí)行一次這個(gè)動(dòng)畫。
Adapter工作原理
首先是適配器,適配器的作用都是類似的,用于提供每個(gè) item 視圖,并返回給 RecyclerView 作為其子布局添加到內(nèi)部。
但是,與 ListView 不同的是,ListView 的適配器是直接返回一個(gè) View,將這個(gè) View 加入到 ListView 內(nèi)部。而 RecyclerView 是返回一個(gè) ViewHolder 并且不是直接將這個(gè) holder 加入到視圖內(nèi)部,而是加入到一個(gè)緩存區(qū)域,在視圖需要的時(shí)候去緩存區(qū)域找到 holder 再間接的找到 holder 包裹的 View。
ViewHolder
每個(gè) ViewHolder 的內(nèi)部是一個(gè) View,并且 ViewHolder 必須繼承自RecyclerView.ViewHolder類。 這主要是因?yàn)?RecyclerView 內(nèi)部的緩存結(jié)構(gòu)并不是像 ListView 那樣去緩存一個(gè) View,而是直接緩存一個(gè) ViewHolder ,在 ViewHolder 的內(nèi)部又持有了一個(gè) View。既然是緩存一個(gè) ViewHolder,那么當(dāng)然就必須所有的 ViewHolder 都繼承同一個(gè)類才能做到了。
緩存與復(fù)用的原理
還是一張截圖
RecyclerView 的內(nèi)部維護(hù)了一個(gè)二級(jí)緩存,滑出界面的 ViewHolder 會(huì)暫時(shí)放到 cache 結(jié)構(gòu)中,而從 cache 結(jié)構(gòu)中移除的 ViewHolder,則會(huì)放到一個(gè)叫做 RecycledViewPool 的循環(huán)緩存池中。
順帶一說(shuō),RecycledView 的性能并不比 ListView 要好多少,它最大的優(yōu)勢(shì)在于其擴(kuò)展性。但是有一點(diǎn),在 RecycledView 內(nèi)部的這個(gè)第二級(jí)緩存池 RecycledViewPool 是可以被多個(gè) RecyclerView 共用的,這一點(diǎn)比起直接緩存 View 的 ListView 就要高明了很多,但也正是因?yàn)樾枰欢鄠€(gè) RecyclerView 公用,所以我們的 ViewHolder 必須繼承自同一個(gè)基類(即RecyclerView.ViewHolder)。
默認(rèn)的情況下,cache 緩存 2 個(gè) holder,RecycledViewPool 緩存 5 個(gè) holder。對(duì)于二級(jí)緩存池中的 holder 對(duì)象,會(huì)根據(jù) viewType 進(jìn)行分類,不同類型的 viewType 之間互不影響。
5. 源碼解析
onMeasure
既然是一個(gè) View,我們先從onMeasure()開始看。
之前我們就說(shuō)了 RecyclerView 的 measure 和 layout 都是交給了 LayoutManager 去做的,來(lái)看一下為什么:
不論是否啟用 mAutoMeasure 最終都會(huì)執(zhí)行到 mLayout.onMeasure() 方法中,而這個(gè) mLayout 就是一個(gè) LayoutManager 對(duì)象。
我們挑選 LinearLayoutManager 來(lái)看
發(fā)現(xiàn)它并沒(méi)有onMeasure()方法,LinearLayoutManager 直接繼承自 LayoutManager,所以又回到了父類 LayoutManager 中。
有一句非常奇葩的注釋:在這里直接調(diào)用 LayoutManager 靜態(tài)方法并不完美,因?yàn)楸旧砭褪窃陬悆?nèi)部,更好的辦法調(diào)用一個(gè)單獨(dú)的方法。但反正這段代碼也已經(jīng)公開了,你們自己看著辦
如果這不是歷史遺留問(wèn)題,那肯定是臨時(shí)工寫的,你寫的時(shí)候都意識(shí)到這問(wèn)題了,你還把一大堆類都寫在一個(gè)類里面,造成了 RecyclerView 一個(gè)類有一萬(wàn)多行代碼。我猜你是為了類之間跨類調(diào)用方便一點(diǎn),可是你就不能設(shè)置一個(gè)包訪問(wèn)權(quán)限,所有類成員方法都包內(nèi)調(diào)用嗎,一個(gè)類干了六個(gè)類的活,網(wǎng)上居然還有人說(shuō)這是高內(nèi)聚的表現(xiàn)。
接著是chooseSize()方法,很簡(jiǎn)單,直接根據(jù)測(cè)量值和模式返回了最適大小。
public static int chooseSize(int spec, int desired, int min) {final int mode = View.MeasureSpec.getMode(spec);final int size = View.MeasureSpec.getSize(spec);switch (mode) {case View.MeasureSpec.EXACTLY:return size;case View.MeasureSpec.AT_MOST:return Math.min(size, Math.max(desired, min));case View.MeasureSpec.UNSPECIFIED:default:return Math.max(desired, min);} }緊接著是對(duì)子控件 measure ,調(diào)用了:dispatchLayoutStep2() 調(diào)用了相同的方法,子控件的 measure 在 layout 過(guò)程中講解
onLayout
然后我們來(lái)看 layout 過(guò)程. 在onLayout()方法中間接的調(diào)用到了這么一個(gè)方法:dispatchLayoutStep2(),在它之中又調(diào)用到了mLayout.onLayoutChildren(mRecycler, mState);
我們重點(diǎn)看這個(gè)onLayoutChildren()方法。
這個(gè)方法在 LayoutManager 中的實(shí)現(xiàn)是空的,那么想必是在子類中實(shí)現(xiàn)了吧。還是找LinearLayoutManager ,跟上面 measure 過(guò)程一樣,調(diào)用了dispatchLayoutStep2() 跟進(jìn)去發(fā)現(xiàn)這么一個(gè)方法:
fill(recycler, mLayoutState, state, false);
onLayoutChildren() 中有一個(gè)非常重要的方法:fill()
recycler,是一個(gè)全局的回收復(fù)用池,用于對(duì)每個(gè)itemview回收以及復(fù)用提供支持。稍后會(huì)詳細(xì)講這個(gè)。
fill() 作用就是根據(jù)當(dāng)前狀態(tài)決定是應(yīng)該從緩存池中取 itemview 填充 還是應(yīng)該回收當(dāng)前的 itemview。
其中,layoutChunk() 負(fù)責(zé)從緩存池 recycler 中取 itemview,并調(diào)用View.addView() 將獲取到的 ItemView 添加到 RecyclerView 中去,并調(diào)用 itemview 自身的 layout 方法去布局 item 位置。
同時(shí)在這里,還調(diào)用了measureChildWithMargins()來(lái)測(cè)繪子控件大小以及設(shè)置顯示位置。這一步,我們到下面的 draw 過(guò)程還要講。
而這全部的添加邏輯都放在一個(gè) while 循環(huán)里面,不停的添加 itemview 到 recyclerview 里面,直到塞滿所有可見區(qū)域?yàn)橹埂?/p>
onDraw
@Override public void onDraw(Canvas c) {super.onDraw(c);final int count = mItemDecorations.size();for (int i = 0; i < count; i++) {mItemDecorations.get(i).onDraw(c, this, mState);} }在 onDraw() 中,除了繪制自己以外,還多調(diào)了一個(gè)mItemDecorations 的 onDraw() 方法,這個(gè)mItemDecorations 就是前面吐槽的分隔線的集合。
之前在講 RecyclerView 的五虎上將的時(shí)候就講過(guò)這個(gè) ItemDecoration。 當(dāng)時(shí)我們還重寫了一個(gè)方法叫g(shù)etItemOffsets()目的是為了不讓 itemview 擋住分隔線。那他是在哪調(diào)用的呢?
還記得 layout 時(shí)說(shuō)的那個(gè)measureChildWithMargins()嗎,就是在這里:
public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);widthUsed += insets.left + insets.right;heightUsed += insets.top + insets.bottom;if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {child.measure(widthSpec, heightSpec);} }在 itemview measure 的時(shí)候,會(huì)把偏移量也計(jì)算進(jìn)來(lái),也就是說(shuō):其實(shí) ItemDecoration 的寬高是計(jì)算在 itemview 中的,只不過(guò) itemview 本身繪制區(qū)域沒(méi)有那么大,留出來(lái)的地方正好的透明的,于是就透過(guò) itemview 顯示出了 ItemDecoration。那么就很有意思了,如果我故意在 ItemDecoration 的偏移量中寫成0,那么 itemview 就會(huì)擋住 ItemDecoration,而在 itemview 的增加或刪除的時(shí)候,會(huì)短暫的消失(透明),這時(shí)候就又可以透過(guò) itemview 看到 ItemDecoration 的樣子。使用這種組合還可以做出意想不到的動(dòng)畫效果。
滾動(dòng)
前面我們已經(jīng)完整的走完了 RecyclerView 的繪制流程。接下來(lái)我們?cè)倏纯此跐L動(dòng)的時(shí)候代碼又是怎么調(diào)用的。
說(shuō)到滾動(dòng),自然要看 onTouch() 方法的 MOVE 狀態(tài)。
case MotionEvent.ACTION_MOVE: {final int index = MotionEventCompat.findPointerIndex(e, mScrollPointerId);final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);int dx = mLastTouchX - x;int dy = mLastTouchY - y;if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {...}if (mScrollState != SCROLL_STATE_DRAGGING) {...if (startScroll) {setScrollState(SCROLL_STATE_DRAGGING);}}if (mScrollState == SCROLL_STATE_DRAGGING) {mLastTouchX = x - mScrollOffset[0];mLastTouchY = y - mScrollOffset[1];if (scrollByInternal(canScrollHorizontally ? dx : 0,canScrollVertically ? dy : 0,vtev)) {getParent().requestDisallowInterceptTouchEvent(true);}} } break;看到這段代碼的時(shí)候,特意去搜了一下,MotionEventCompat 這個(gè)類是干嘛的。 他是 v4 包里面提供的一個(gè)工具類,用于兼容低版本的觸摸屏手勢(shì)。平時(shí)用的時(shí)候更多的是用它來(lái)處理多點(diǎn)觸控的情況,當(dāng)成MotionEvent就可以了。
dispatchNestedPreScroll() 用于處理嵌套邏輯,例如在 ScrollView 里面放一個(gè) RecyclerView ,如果是以前用 ListView ,還得要把高度寫死,禁止 ListView 的復(fù)用和滾動(dòng)邏輯,而 RecyclerView 則完全不需要更多處理,直接用就是了。而且有一個(gè)非常好的地方,如果放到 ScrollView 里面,ListView 的 ItemView 是不會(huì)復(fù)用的,而 RecyclerView 因?yàn)槭侨止靡惶拙彺娉?#xff0c;雖說(shuō)嵌套到 ScrollView 效率會(huì)低很多,但比起 ListView 嵌套要好很多,之后講緩存池的時(shí)候,我們繼續(xù)講。
再之后,如果在相應(yīng)方向上手指move的距離達(dá)到最大值,則認(rèn)為需要滾動(dòng),并設(shè)置為滾動(dòng)狀態(tài)(SCROLL_STATE_DRAGGING),這個(gè)最大距離默認(rèn)是 8 個(gè)像素。
接著走出 if 塊,如果是滾動(dòng)狀態(tài),則調(diào)用滾動(dòng)方法scrollByInternal()執(zhí)行相應(yīng)方向的滾動(dòng)。滾動(dòng)的距離當(dāng)然就是手指移動(dòng)的距離。跟進(jìn)去看,果然是調(diào)用了LinearLayoutManager.scrollBy()方法,又印證了前面【更多操作】里面講 LayoutManager 可以滾動(dòng) RecyclerView 的方法。
以上就是滾動(dòng)的邏輯了。 但是沒(méi)完,就像 ListView,在手指劃過(guò)以后,手指離開了屏幕,相關(guān)性一樣,View 自己依舊可以自己滾動(dòng)一段距離。
既然手指離開了屏幕,那就去 UP 或者 CANCEL 狀態(tài)去找。
ACTION_CANCEL 里面只有一個(gè) cancelTouch() ,那么自然是在 UP 狀態(tài)里面實(shí)現(xiàn)的慣性滾動(dòng)。
看到了一個(gè) mVelocityTracker 對(duì)象,大概原理也就清楚了,慣性滾動(dòng)多長(zhǎng),肯定是跟手指移動(dòng)的速度有關(guān)了。
再往下,跟進(jìn)fling()方法里面看:調(diào)用了mViewFlinger.fling(velocityX, velocityY);
再進(jìn):
原來(lái)是調(diào)用了 Scroller 類的fling()方法,再仔細(xì)看一下,發(fā)現(xiàn)是ScrollerCompat 看名字,估計(jì)又是用來(lái)兼容舊版本的 support 包里面的 Scroller 類。關(guān)于這個(gè)Scroller類,他是一個(gè)可以用來(lái)實(shí)現(xiàn)平滑滾動(dòng)效果的類,其實(shí)內(nèi)部實(shí)現(xiàn)也是通過(guò)一點(diǎn)一點(diǎn)移動(dòng) view,利用了人眼的視覺(jué)暫留。
回收與復(fù)用
前面講 layout、滾動(dòng)的時(shí)候,都出現(xiàn)了一個(gè)東西,叫 Recycler,現(xiàn)在我們就來(lái)看看他到底是個(gè)什么。
public final class Recycler { final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>(); private ArrayList<ViewHolder> mChangedScrap = null;final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();private final List<ViewHolder>mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);private RecycledViewPool mRecyclerPool;private ViewCacheExtension mViewCacheExtension;這么多的集合,還有什么Pool,ViewCache。看來(lái)他就是一個(gè)超大型的緩存器了。
事實(shí)上他確實(shí)就是一個(gè)超大型的緩存器,擁有三級(jí)緩存(如果算上創(chuàng)建的那一次,應(yīng)該是四級(jí)了),這么大的緩存系統(tǒng),究竟是如何完成的?
第一級(jí)緩存
就是上面的一系列 mCachedViews。如果仍依賴于 RecyclerView (比如已經(jīng)滑動(dòng)出可視范圍,但還沒(méi)有被移除掉),但已經(jīng)被標(biāo)記移除的 ItemView 集合會(huì)被添加到 mAttachedScrap 中。然后如果 mAttachedScrap 中不再依賴時(shí)會(huì)被加入到 mCachedViews 中。 mChangedScrap 則是存儲(chǔ) notifXXX 方法時(shí)需要改變的 ViewHolder 。
第二級(jí)緩存
ViewCacheExtension 是一個(gè)抽象靜態(tài)類,用于充當(dāng)附加的緩存池,當(dāng) RecyclerView 從第一級(jí)緩存找不到需要的 View 時(shí),將會(huì)從 ViewCacheExtension 中找。不過(guò)這個(gè)緩存是由開發(fā)者維護(hù)的,如果沒(méi)有設(shè)置它,則不會(huì)啟用。通常我們也不會(huì)去設(shè)置他,系統(tǒng)已經(jīng)預(yù)先提供了兩級(jí)緩存了,除非有特殊需求,比如要在調(diào)用系統(tǒng)的緩存池之前,返回一個(gè)特定的視圖,才會(huì)用到他。
第三級(jí)緩存
最強(qiáng)大的緩存器。之前講了,與 ListView 直接緩存 ItemView 不同,從上面代碼里我們也能看到,RecyclerView 緩存的是 ViewHolder。而 ViewHolder 里面包含了一個(gè) View 這也就是為什么在寫 Adapter 的時(shí)候 必須繼承一個(gè)固定的 ViewHolder 的原因。首先來(lái)看一下 RecycledViewPool:
public static class RecycledViewPool {// 根據(jù) viewType 保存的被廢棄的 ViewHolder 集合,以便下次使用private SparseArray<ArrayList<ViewHolder>> mScrap = new SparseArray<ArrayList<ViewHolder>>();/*** 從緩存池移除并返回一個(gè) ViewHolder*/public ViewHolder getRecycledView(int viewType) {final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);if (scrapHeap != null && !scrapHeap.isEmpty()) {final int index = scrapHeap.size() - 1;final ViewHolder scrap = scrapHeap.get(index);scrapHeap.remove(index);return scrap;}return null;}public void putRecycledView(ViewHolder scrap) {final int viewType = scrap.getItemViewType();final ArrayList scrapHeap = getScrapHeapForType(viewType);if (mMaxScrap.get(viewType) <= scrapHeap.size()) {return;}scrap.resetInternal();scrapHeap.add(scrap);}/*** 根據(jù) viewType 獲取對(duì)應(yīng)緩存池*/private ArrayList<ViewHolder> getScrapHeapForType(int viewType) {ArrayList<ViewHolder> scrap = mScrap.get(viewType);if (scrap == null) {scrap = new ArrayList<>();mScrap.put(viewType, scrap);if (mMaxScrap.indexOfKey(viewType) < 0) {mMaxScrap.put(viewType, DEFAULT_MAX_SCRAP);}}return scrap;} }從名字來(lái)看,他是一個(gè)緩存池,實(shí)現(xiàn)上,是通過(guò)一個(gè)默認(rèn)為 5 大小的 ArrayList 實(shí)現(xiàn)的。這一點(diǎn),同 ListView 的 RecyclerBin 這個(gè)類一樣。很奇怪為什么不用 LinkedList 來(lái)做,按理說(shuō)這種不需要索引讀取的緩存池,用鏈表是最合適的。
然后每一個(gè) ArrayList 又都是放在一個(gè) Map 里面的,SparseArray 這個(gè)類我們?cè)谥v性能優(yōu)化的時(shí)候已經(jīng)多次提到了,就是兩個(gè)數(shù)組,用來(lái)替代 Map 的。
把所有的 ArrayList 放在一個(gè) Map 里面,這也是 RecyclerView 最大的亮點(diǎn),這樣根據(jù) itemType 來(lái)取不同的緩存 Holder,每一個(gè) Holder 都有對(duì)應(yīng)的緩存,而只需要為這些不同 RecyclerView 設(shè)置同一個(gè) Pool 就可以了。
這一點(diǎn)我們?cè)?Pool 的 setter 方法上可以看到注釋:
/*** Recycled view pools allow multiple RecyclerViews to share a common pool of scrap views.* This can be useful if you have multiple RecyclerViews with adapters that use the same* view types, for example if you have several data sets with the same kinds of item views* displayed by a {@link android.support.v4.view.ViewPager ViewPager}.** @param pool Pool to set. If this parameter is null a new pool will be created and used.*/ public void setRecycledViewPool(RecycledViewPool pool) {mRecycler.setRecycledViewPool(pool); }在類似 ViewPager 這種視圖中,所有 RecyclerView 的 holder 是共存于同一個(gè) Pool 中的。
寫了這么多累死我了,就這樣吧,最后發(fā)一個(gè) demo 地址:RecyclerViewDemo
和一份內(nèi)部分享的 PPT 地址:RecyclerView PPT
原文出處:http://kymjs.com/code/2016/07/10/01,作者:張濤
RecyclerView優(yōu)秀文集
總結(jié)
以上是生活随笔為你收集整理的深入浅出 RecyclerView的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: RecyclerView
- 下一篇: swapCursor vs change