RecyclerView

這篇文章分三個(gè)部分辆苔,簡(jiǎn)單跟大家講一下 RecyclerView 的常用方法與奇葩用法心铃;工作原理與ListView比較译断;源碼解析;

常用方法

RecyclerView 與 ListView积暖、GridView 類(lèi)似藤为,都是可以顯示同一種類(lèi)型 View 的集合的控件。

首先看看最簡(jiǎn)單的用法夺刑,四步走:

0.接入 build.gradle 文件中加入

compile'com.android.support:recyclerview-v7:24.0.0'

1.創(chuàng)建對(duì)象

RecyclerViewrecyclerview = (RecyclerView) findViewById(R.id.recyclerview);

2.設(shè)置顯示規(guī)則

recyclerview.setLayoutManager(newLinearLayoutManager(this, LinearLayoutManager.VERTICAL,false));

RecyclerView 將所有的顯示規(guī)則交給一個(gè)叫LayoutManager的類(lèi)去完成了缅疟。

LayoutManager是一個(gè)抽象類(lèi),系統(tǒng)已經(jīng)為我們提供了三個(gè)默認(rèn)的實(shí)現(xiàn)類(lèi)遍愿,分別是LinearLayoutManager存淫、GridLayoutManagerStaggeredGridLayoutManager沼填,從名字我們就能看出來(lái)了桅咆,分別是,線性顯示坞笙、網(wǎng)格顯示岩饼、瀑布流顯示。當(dāng)然你也可以通過(guò)繼承這些類(lèi)來(lái)擴(kuò)展實(shí)現(xiàn)自己的LayougManager薛夜。

3.設(shè)置適配器

recyclerview.setAdapter(adapter);

適配器籍茧,同ListView一樣,用來(lái)設(shè)置每個(gè)item顯示內(nèi)容的梯澜。

通常寞冯,我們寫(xiě)ListView適配器,都是首先繼承BaseAdapter晚伙,實(shí)現(xiàn)四個(gè)抽象方法吮龄,創(chuàng)建一個(gè)靜態(tài)ViewHoldergetView()方法中判斷convertView是否為空咆疗,創(chuàng)建還是獲取viewholder對(duì)象漓帚。

RecyclerView也是類(lèi)似的步驟,首先繼承RecyclerView.Adapter類(lèi)民傻,實(shí)現(xiàn)三個(gè)抽象方法胰默,創(chuàng)建一個(gè)靜態(tài)的ViewHolder。不過(guò)RecyclerViewViewHolder創(chuàng)建稍微有些限制漓踢,類(lèi)名就是上面繼承的時(shí)候泛型中聲明的類(lèi)名(好像反了牵署,應(yīng)該是上面泛型中的類(lèi)名應(yīng)該是這個(gè)holder的類(lèi)名);并且ViewHolder必須繼承自RecyclerView.ViewHolder類(lèi)喧半。

publicclassDemoAdapterextendsRecyclerView.Adapter{privateList dataList;privateContext context;publicDemoAdapter(Context context, ArrayList datas){this.dataList = datas;this.context = context;? ? }@OverridepublicVHonCreateViewHolder(ViewGroup parent,intviewType){returnnewVH(View.inflate(context, android.R.layout.simple_list_item_2,null));? ? }@OverridepublicvoidonBindViewHolder(VH holder,intposition){? ? ? ? holder.mTextView.setText(dataList.get(position).getNum());? ? }@OverridepublicintgetItemCount(){returndataList.size();? ? }publicstaticclassVHextendsRecyclerView.ViewHolder{? ? ? ? TextView mTextView;publicVH(View itemView){super(itemView);? ? ? ? ? ? mTextView = (TextView) itemView.findViewById(android.R.id.text1);? ? ? ? }? ? }}

更多方法

除了常用方法奴迅,當(dāng)然還有不常用的。

瀑布流與滾動(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)單使用

添加刪除 item 的動(dòng)畫(huà)

ListView每次修改了數(shù)據(jù)源后块仆,都要調(diào)用notifyDataSetChanged()刷新每項(xiàng) item 類(lèi)似,只不過(guò)RecyclerView還支持局部刷新notifyItemInserted(index);王暗、notifyItemRemoved(position)悔据、notifyItemChanged(position)。

在添加或刪除了數(shù)據(jù)后俗壹,RecyclerView還提供了一個(gè)默認(rèn)的動(dòng)畫(huà)效果科汗,來(lái)改變顯示。同時(shí)绷雏,你也可以定制自己的動(dòng)畫(huà)效果:模仿DefaultItemAnimator或直接繼承這個(gè)類(lèi)头滔,實(shí)現(xiàn)自己的動(dòng)畫(huà)效果,并調(diào)用recyclerview.setItemAnimator(new DefaultItemAnimator());設(shè)置上自己的動(dòng)畫(huà)涎显。

使用 demo 可以查看:Github 【RecyclerView默認(rèn)動(dòng)畫(huà)

LayoutManager的常用方法

findFirstVisibleItemPosition()返回當(dāng)前第一個(gè)可見(jiàn) Item 的 position

findFirstCompletelyVisibleItemPosition()返回當(dāng)前第一個(gè)完全可見(jiàn) Item 的 position

findLastVisibleItemPosition()返回當(dāng)前最后一個(gè)可見(jiàn) Item 的 position

findLastCompletelyVisibleItemPosition()返回當(dāng)前最后一個(gè)完全可見(jiàn) Item 的 position.

scrollBy()滾動(dòng)到某個(gè)位置拙毫。

adapter封裝

其實(shí)很早之前寫(xiě)過(guò)一篇關(guān)于RecyclerView適配器的封裝,所以這不再贅述了棺禾,傳送門(mén):RecyclerView的通用適配器

使用 demo 可以查看:Github 【RecyclerView通用適配器演示

吐槽

OnItemTouchListener 什么鬼缀蹄?

用習(xí)慣了ListViewOnItemClickListenerRecyclerView你的OnItemClickListener呢膘婶?

Tell me where do I find, something like ListView listener ?

好吧缺前,翻遍了 API 列表,就找到了個(gè)OnItemTouchListener悬襟,這特么什么鬼衅码,我干嘛要對(duì)每個(gè) item 監(jiān)聽(tīng)觸摸屏事件。

萬(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)擊事件都是寫(xiě)在了 adapter 的 ViewHolder 里面嘹黔。

當(dāng)然账嚎,除了 support demo 包里面使用的在 ViewHolder 里面設(shè)置點(diǎn)擊事件以外,我還寫(xiě)好了一個(gè)RecyclerView使用的OnItemClickListener代碼請(qǐng)見(jiàn):RecyclerItemClickListener.java

需要一提的是儡蔓,網(wǎng)上有很多這種類(lèi)似的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)題了召锈,但作為代碼編寫(xiě)者,這種寫(xiě)法還是應(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ō)這樣寫(xiě)靈活多了较解,但是要額外寫(xiě)一個(gè)類(lèi)去做難免麻煩畜疾,這里大家可以看我已經(jīng)實(shí)現(xiàn)好的一個(gè)封裝,包括顯示純色divider印衔、顯示圖片divider啡捶、divider的上下左右的間距寬高設(shè)置應(yīng)該可以滿足基本需求了:Divider.java

使用 demo 可以查看:Github 【自定義 Divider 使用

五虎上將工作原理

借用 Google IO 視頻中的一張截圖:

視頻的完整地址可查看:RecyclerView ins and outs - Google I/O 2016

其實(shí)上圖中并沒(méi)有寫(xiě)完整奸焙,大 bossRecyclerView應(yīng)該有這五虎上將:

類(lèi)名作用

RecyclerView.LayoutManager負(fù)責(zé)Item視圖的布局的顯示管理

RecyclerView.ItemDecoration給每一項(xiàng)Item視圖添加子View,例如可以進(jìn)行畫(huà)分隔線之類(lèi)

RecyclerView.ItemAnimator負(fù)責(zé)處理數(shù)據(jù)添加或者刪除時(shí)候的動(dòng)畫(huà)效果

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í)灶伊,如果你重寫(xiě)了這個(gè)方法,就相當(dāng)于是直接在 RecyclerView 上畫(huà)了一個(gè) Drawable 表現(xiàn)的東西寒跳。 而最后聘萨,在他的內(nèi)部還有一個(gè)叫g(shù)etItemOffsets()的方法,從字面就可以理解童太,他是用來(lái)偏移每個(gè) item 視圖的米辐。當(dāng)我們?cè)诿總€(gè) item 視圖之間強(qiáng)行插入繪畫(huà)了一段 Drawable,那么如果再照著原本的邏輯去繪 item 視圖书释,就會(huì)覆蓋掉 Decoration 了翘贮,所以需要getItemOffsets()這個(gè)方法,讓每個(gè) item 往后面偏移一點(diǎn)爆惧,不要覆蓋到之前畫(huà)上的分隔樣式了狸页。

ItemAnimator

每一個(gè) item 在特定情況下都會(huì)執(zhí)行的動(dòng)畫(huà)。說(shuō)是特定情況扯再,其實(shí)就是在視圖發(fā)生改變芍耘,我們手動(dòng)調(diào)用notifyxxxx()的時(shí)候。通常這個(gè)時(shí)候我們會(huì)要傳一個(gè)下標(biāo)熄阻,那么從這個(gè)標(biāo)記開(kāi)始一直到結(jié)束斋竞,所有 item 視圖都會(huì)被執(zhí)行一次這個(gè)動(dòng)畫(huà)。

Adapter工作原理

首先是適配器秃殉,適配器的作用都是類(lèi)似的坝初,用于提供每個(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類(lèi)。 這主要是因?yàn)?RecyclerView 內(nèi)部的緩存結(jié)構(gòu)并不是像 ListView 那樣去緩存一個(gè) View勾栗,而是直接緩存一個(gè) ViewHolder 惨篱,在 ViewHolder 的內(nèi)部又持有了一個(gè) View。既然是緩存一個(gè) ViewHolder围俘,那么當(dāng)然就必須所有的 ViewHolder 都繼承同一個(gè)類(lèi)才能做到了砸讳。

緩存與復(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è)基類(lèi)(即RecyclerView.ViewHolder)漠另。

默認(rèn)的情況下,cache 緩存 2 個(gè) holder跃赚,RecycledViewPool 緩存 5 個(gè) holder酗钞。對(duì)于二級(jí)緩存池中的 holder 對(duì)象,會(huì)根據(jù) viewType 進(jìn)行分類(lèi)来累,不同類(lèi)型的 viewType 之間互不影響砚作。

源碼解析

onMeasure

既然是一個(gè)View,我們先從onMeasure()開(kāi)始看。

之前我們就說(shuō)了RecyclerView的 measure 和 layout 都是交給了LayoutManager去做的嘹锁,來(lái)看一下為什么:

if(mLayout.mAutoMeasure) {finalintwidthMode = MeasureSpec.getMode(widthSpec);finalintheightMode = MeasureSpec.getMode(heightSpec);finalbooleanskipMeasure = widthMode == MeasureSpec.EXACTLY? ? ? ? ? ? && heightMode == MeasureSpec.EXACTLY;? ? mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);}else{? ? mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);}

不論是否啟用 mAutoMeasure 最終都會(huì)執(zhí)行到 mLayout.onMeasure() 方法中葫录,而這個(gè) mLayout 就是一個(gè) LayoutManager 對(duì)象。

我們挑選LinearLayoutManager來(lái)看

發(fā)現(xiàn)它并沒(méi)有onMeasure()方法领猾,LinearLayoutManager 直接繼承自 LayoutManager米同,所以又回到了父類(lèi) LayoutManager 中。

voiddefaultOnMeasure(intwidthSpec,intheightSpec){// calling LayoutManager here is not pretty but that API is already public and it is better// than creating another method since this is internal.finalintwidth = LayoutManager.chooseSize(widthSpec,? ? ? ? ? ? getPaddingLeft() + getPaddingRight(),? ? ? ? ? ? ViewCompat.getMinimumWidth(this));finalintheight = LayoutManager.chooseSize(heightSpec,? ? ? ? ? ? getPaddingTop() + getPaddingBottom(),? ? ? ? ? ? ViewCompat.getMinimumHeight(this));? ? setMeasuredDimension(width, height);}

有一句非常奇葩的注釋?zhuān)涸谶@里直接調(diào)用 LayoutManager 靜態(tài)方法并不完美摔竿,因?yàn)楸旧砭褪窃陬?lèi)內(nèi)部面粮,更好的辦法調(diào)用一個(gè)單獨(dú)的方法。但反正這段代碼也已經(jīng)公開(kāi)了继低,你們自己看著辦熬苍。。。柴底。婿脸。。

如果這不是歷史遺留問(wèn)題柄驻,那肯定是臨時(shí)工寫(xiě)的狐树,你寫(xiě)的時(shí)候都意識(shí)到這問(wèn)題了,你還把一大堆類(lèi)都寫(xiě)在一個(gè)類(lèi)里面鸿脓,造成了 RecyclerView 一個(gè)類(lèi)有一萬(wàn)多行代碼抑钟。我猜你是為了類(lèi)之間跨類(lèi)調(diào)用方便一點(diǎn),可是你就不能設(shè)置一個(gè)包訪問(wèn)權(quán)限野哭,所有類(lèi)成員方法都包內(nèi)調(diào)用嗎在塔,一個(gè)類(lèi)干了六個(gè)類(lèi)的活,網(wǎng)上居然還有人說(shuō)這是高內(nèi)聚的表現(xiàn)虐拓。

接著是chooseSize()方法心俗,很簡(jiǎn)單傲武,直接根據(jù)測(cè)量值和模式返回了最適大小蓉驹。

publicstaticintchooseSize(intspec,intdesired,intmin){finalintmode = View.MeasureSpec.getMode(spec);finalintsize = View.MeasureSpec.getSize(spec);switch(mode) {caseView.MeasureSpec.EXACTLY:returnsize;caseView.MeasureSpec.AT_MOST:returnMath.min(size, Math.max(desired, min));caseView.MeasureSpec.UNSPECIFIED:default:returnMath.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)是空的疟位,那么想必是在子類(lèi)中實(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è)傻铣。

while((layoutState.mInfinite || remainingSpace >0) && layoutState.hasMore(state)) {? ? layoutChunk(recycler,state, layoutState, layoutChunkResult);? ? layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;if(layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {? ? ? ? layoutState.mScrollingOffset += layoutChunkResult.mConsumed;if(layoutState.mAvailable <0) {? ? ? ? ? ? layoutState.mScrollingOffset += layoutState.mAvailable;? ? ? ? }? ? ? ? recycleByLayoutState(recycler, layoutState);? ? }}

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 里面朴皆,直到塞滿所有可見(jiàn)區(qū)域?yàn)橹埂?/p>

onDraw

@OverridepublicvoidonDraw(Canvas c){super.onDraw(c);finalintcount = mItemDecorations.size();for(inti =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í)我們還重寫(xiě)了一個(gè)方法叫g(shù)etItemOffsets()目的是為了不讓 itemview 擋住分隔線。那他是在哪調(diào)用的呢族吻?

還記得 layout 時(shí)說(shuō)的那個(gè)measureChildWithMargins()嗎帽借,就是在這里:

publicvoidmeasureChildWithMargins(View child,intwidthUsed,intheightUsed){finalRect 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 的偏移量中寫(xiě)成0懊悯,那么 itemview 就會(huì)擋住 ItemDecoration蜓谋,而在 itemview 的增加或刪除的時(shí)候,會(huì)短暫的消失(透明)炭分,這時(shí)候就又可以透過(guò) itemview 看到 ItemDecoration 的樣子桃焕。使用這種組合還可以做出意想不到的動(dòng)畫(huà)效果。

滾動(dòng)

前面我們已經(jīng)完整的走完了 RecyclerView 的繪制流程捧毛。接下來(lái)我們?cè)倏纯此跐L動(dòng)的時(shí)候代碼又是怎么調(diào)用的观堂。

說(shuō)到滾動(dòng),自然要看onTouch()方法的 MOVE 狀態(tài)呀忧。

caseMotionEvent.ACTION_MOVE: {finalintindex = MotionEventCompat.findPointerIndex(e, mScrollPointerId);finalintx = (int) (MotionEventCompat.getX(e, index) +0.5f);finalinty = (int) (MotionEventCompat.getY(e, index) +0.5f);intdx = mLastTouchX - x;intdy = 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è)類(lèi)是干嘛的而账。 他是 v4 包里面提供的一個(gè)工具類(lèi)胰坟,用于兼容低版本的觸摸屏手勢(shì)。平時(shí)用的時(shí)候更多的是用它來(lái)處理多點(diǎn)觸控的情況福扬,當(dāng)成MotionEvent就可以了腕铸。

dispatchNestedPreScroll()用于處理嵌套邏輯,例如在 ScrollView 里面放一個(gè) RecyclerView 铛碑,如果是以前用 ListView 狠裹,還得要把高度寫(xiě)死,禁止 ListView 的復(fù)用和滾動(dòng)邏輯汽烦,而 RecyclerView 則完全不需要更多處理涛菠,直接用就是了。而且有一個(gè)非常好的地方,如果放到 ScrollView 里面俗冻,ListView 的 ItemView 是不會(huì)復(fù)用的礁叔,而 RecyclerView 因?yàn)槭侨止靡惶拙彺娉兀m說(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ò)以后,手指離開(kāi)了屏幕精续,相關(guān)性一樣坝锰,View 自己依舊可以自己滾動(dòng)一段距離。

既然手指離開(kāi)了屏幕重付,那就去 UP 或者 CANCEL 狀態(tài)去找顷级。

caseMotionEvent.ACTION_CANCEL: {? ? cancelTouch();}break;caseMotionEvent.ACTION_UP: {? ? mVelocityTracker.addMovement(vtev);? ? mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);finalfloatyvel = canScrollVertically ?? ? ? ? ? ? -VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) :0;if(!((xvel !=0|| yvel !=0) && fling((int) xvel, (int) yvel))) {? ? ? ? setScrollState(SCROLL_STATE_IDLE);? ? }? ? resetTouch();}break;

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):

mScroller.fling(0, 0, velocityX, velocityY,

Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);

原來(lái)是調(diào)用了 Scroller 類(lèi)的fling()方法,再仔細(xì)看一下披泪,發(fā)現(xiàn)是ScrollerCompat看名字纤子,估計(jì)又是用來(lái)兼容舊版本的 support 包里面的 Scroller 類(lèi)。關(guān)于這個(gè)Scroller類(lèi),他是一個(gè)可以用來(lái)實(shí)現(xiàn)平滑滾動(dòng)效果的類(lèi)控硼,其實(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è)什么。

publicfinalclassRecycler{finalArrayList mAttachedScrap =newArrayList<>();privateArrayList mChangedScrap =null;finalArrayList mCachedViews =newArrayList();privatefinalList? ? ? ? mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);privateRecycledViewPool mRecyclerPool;privateViewCacheExtension mViewCacheExtension;

這么多的集合,還有什么Pool彼妻,ViewCache∪甯龋看來(lái)他就是一個(gè)超大型的緩存器了桥嗤。

事實(shí)上他確實(shí)就是一個(gè)超大型的緩存器,擁有三級(jí)緩存(如果算上創(chuàng)建的那一次埂伦,應(yīng)該是四級(jí)了)煞额,這么大的緩存系統(tǒng),究竟是如何完成的沾谜?

第一級(jí)緩存:

就是上面的一系列 mCachedViews膊毁。如果仍依賴(lài)于 RecyclerView (比如已經(jīng)滑動(dòng)出可視范圍,但還沒(méi)有被移除掉)基跑,但已經(jīng)被標(biāo)記移除的 ItemView 集合會(huì)被添加到 mAttachedScrap 中婚温。然后如果 mAttachedScrap 中不再依賴(lài)時(shí)會(huì)被加入到 mCachedViews 中。 mChangedScrap 則是存儲(chǔ) notifXXX 方法時(shí)需要改變的 ViewHolder 媳否。

第二級(jí)緩存:

ViewCacheExtension是一個(gè)抽象靜態(tài)類(lèi)栅螟,用于充當(dāng)附加的緩存池,當(dāng) RecyclerView 從第一級(jí)緩存找不到需要的 View 時(shí)篱竭,將會(huì)從ViewCacheExtension中找力图。不過(guò)這個(gè)緩存是由開(kāi)發(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 這也就是為什么在寫(xiě) Adapter 的時(shí)候 必須繼承一個(gè)固定的 ViewHolder 的原因觅彰。首先來(lái)看一下 RecycledViewPool:

publicstaticclassRecycledViewPool{// 根據(jù) viewType 保存的被廢棄的 ViewHolder 集合吩蔑,以便下次使用privateSparseArray> mScrap =newSparseArray>();/**

* 從緩存池移除并返回一個(gè) ViewHolder

*/publicViewHoldergetRecycledView(intviewType){? ? final ArrayList scrapHeap = mScrap.get(viewType);if(scrapHeap !=null&& !scrapHeap.isEmpty()) {? ? ? finalintindex = scrapHeap.size() -1;? ? ? final ViewHolder scrap = scrapHeap.get(index);? ? ? scrapHeap.remove(index);returnscrap;? ? }returnnull;? ? }publicvoidputRecycledView(ViewHolder scrap){? ? finalintviewType = scrap.getItemViewType();? ? final ArrayList scrapHeap = getScrapHeapForType(viewType);if(mMaxScrap.get(viewType) <= scrapHeap.size()) {return;? ? }? ? scrap.resetInternal();? ? scrapHeap.add(scrap);? }/**

* 根據(jù) viewType 獲取對(duì)應(yīng)緩存池

*/privateArrayListgetScrapHeapForType(intviewType){? ? ArrayList scrap = mScrap.get(viewType);if(scrap ==null) {? ? ? ? scrap =newArrayList<>();? ? ? ? mScrap.put(viewType, scrap);if(mMaxScrap.indexOfKey(viewType) <0) {? ? ? ? ? ? mMaxScrap.put(viewType, DEFAULT_MAX_SCRAP);? ? ? ? ? }? ? ? }returnscrap;? }}

從名字來(lái)看,他是一個(gè)緩存池填抬,實(shí)現(xiàn)上烛芬,是通過(guò)一個(gè)默認(rèn)為 5 大小的 ArrayList 實(shí)現(xiàn)的。這一點(diǎn)飒责,同 ListView 的RecyclerBin這個(gè)類(lèi)一樣赘娄。很奇怪為什么不用 LinkedList 來(lái)做,按理說(shuō)這種不需要索引讀取的緩存池宏蛉,用鏈表是最合適的遣臼。

然后每一個(gè) ArrayList 又都是放在一個(gè) Map 里面的,SparseArray這個(gè)類(lèi)我們?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 方法上可以看到注釋?zhuān)?/p>

/**

* 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.

*/publicvoidsetRecycledViewPool(RecycledViewPool pool){? ? mRecycler.setRecycledViewPool(pool);}

在類(lèi)似 ViewPager 這種視圖中,所有 RecyclerView 的 holder 是共存于同一個(gè) Pool 中的褪那。

寫(xiě)了這么多累死我了幽纷,就這樣吧,最后發(fā)一個(gè) demo 地址:RecyclerViewDemo

和一份內(nèi)部分享的 PPT 地址:RecyclerView PPT

本文原創(chuàng)武通,轉(zhuǎn)載請(qǐng)以鏈接形式注明地址:http://kymjs.com/code/2016/07/10/01

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末霹崎,一起剝皮案震驚了整個(gè)濱河市珊搀,隨后出現(xiàn)的幾起案子冶忱,更是在濱河造成了極大的恐慌,老刑警劉巖境析,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件囚枪,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡劳淆,警方通過(guò)查閱死者的電腦和手機(jī)链沼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)沛鸵,“玉大人括勺,你說(shuō)我怎么就攤上這事缆八。” “怎么了疾捍?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵奈辰,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我乱豆,道長(zhǎng)奖恰,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任宛裕,我火速辦了婚禮瑟啃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘揩尸。我一直安慰自己蛹屿,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布岩榆。 她就那樣靜靜地躺著蜡峰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪朗恳。 梳的紋絲不亂的頭發(fā)上湿颅,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音粥诫,去河邊找鬼油航。 笑死,一個(gè)胖子當(dāng)著我的面吹牛怀浆,可吹牛的內(nèi)容都是我干的谊囚。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼执赡,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼镰踏!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起沙合,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤奠伪,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后首懈,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體绊率,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年究履,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了滤否。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡最仑,死狀恐怖藐俺,靈堂內(nèi)的尸體忽然破棺而出炊甲,到底是詐尸還是另有隱情,我是刑警寧澤欲芹,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布蜜葱,位于F島的核電站,受9級(jí)特大地震影響耀石,放射性物質(zhì)發(fā)生泄漏牵囤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一滞伟、第九天 我趴在偏房一處隱蔽的房頂上張望揭鳞。 院中可真熱鬧,春花似錦梆奈、人聲如沸野崇。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)乓梨。三九已至,卻和暖如春清酥,著一層夾襖步出監(jiān)牢的瞬間扶镀,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工焰轻, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留臭觉,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓辱志,卻偏偏與公主長(zhǎng)得像蝠筑,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子揩懒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • RecyclerView 源碼分析 本文原創(chuàng)什乙,轉(zhuǎn)載請(qǐng)注明出處。歡迎關(guān)注我的 簡(jiǎn)書(shū) 已球,關(guān)注我的專(zhuān)題 Android ...
    MeloDev閱讀 10,082評(píng)論 6 49
  • 簡(jiǎn)介: 提供一個(gè)讓有限的窗口變成一個(gè)大數(shù)據(jù)集的靈活視圖臣镣。 術(shù)語(yǔ)表: Adapter:RecyclerView的子類(lèi)...
    酷泡泡閱讀 5,138評(píng)論 0 16
  • 一、概述 對(duì)于RecyclerView的學(xué)習(xí)和悦,主要是需要掌握以下幾點(diǎn): 數(shù)據(jù):Adapter 使用:Recycle...
    澤毛閱讀 7,272評(píng)論 1 23
  • 3.14 RecyclerView詳解 RecyclerView作為L(zhǎng)istView和GridView的替代退疫,但是...
    jianhuih閱讀 6,627評(píng)論 1 5
  • It rained again today, that the second day in a row so th...
    李子菲閱讀 150評(píng)論 1 1