這篇文章分三個(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存淫、GridLayoutManager、StaggeredGridLayoutManager沼填,從名字我們就能看出來(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)ViewHolder,getView()方法中判斷convertView是否為空咆疗,創(chuàng)建還是獲取viewholder對(duì)象漓帚。
而RecyclerView也是類(lèi)似的步驟,首先繼承RecyclerView.Adapter類(lèi)民傻,實(shí)現(xiàn)三個(gè)抽象方法胰默,創(chuàng)建一個(gè)靜態(tài)的ViewHolder。不過(guò)RecyclerView的ViewHolder創(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í)慣了ListView的OnItemClickListener,RecyclerView你的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