14年Google發(fā)布了萬眾期待的Android 5.0 。隨之而來的還有新的設(shè)計方案 Material Design。為了在5.0以下的版本中也兼容這種設(shè)計方案, Google在新的support包中放出了大量控件,這其中就包括我們今天要講的RecyclerView。
這篇文章并不會講RecyclerView怎么用,而且通過分析RecyclerView內(nèi)部的運行機制,以便于能恰當(dāng)、正確的使用RecyclerView。
先看看RecyclerView都包含什么
** 先想想RecyclerView有什么特點:**
- 可以顯示列表、網(wǎng)格、瀑布流等布局
- 可以添加Item的動畫
- Item 可以回收再利用
- 可以顯示多種類型的 Item
- Item 支持拖拽操作
- 等等
那這么厲害的RecyclerView代碼都有多復(fù)雜啊。
錯了,一個好漢三個幫,弱蜀還有五虎上將呢,RecyclerView這么好用的控件怎么可能單槍匹馬逞英雄?
** 那我們看看RecyclerView都有那些猛將:**
- LayoutManager
LayoutManager是用來管理RecyclerView的布局。RecyclerView的onMeasure 和 onLayout都會被LayoutManager全權(quán)代理。不同的LayoutManager展示的布局樣式也不一樣。android默認(rèn)提供三種布局樣式,我們也可以自定義特殊的布局樣式。- ItemAnimator
ItemAnimator是處理在Item的add、remove、change的時候,展示相應(yīng)的動畫。RecyclerView是會帶著一個默認(rèn)動畫的。- Adapter
Adapter 不用多說。根據(jù)數(shù)據(jù)適配不用的View。- Recycler
Recycler相當(dāng)于Item的緩存池。- ChildHelper
由于RecyclerView在處理Item的操作時,會有動畫。比如我們移除一個View.對于ItemAnimator來說,需要讓這個View動起來。而對于RecyclerView來說,它希望可以移除View并且回收這個View。ChildHelper就是用來處理這個沖突。- AdapterHelper
AdapterHelper 維護一組UpdateOp。判斷那些操作需要預(yù)處理,那些不需要,分別給出他們相應(yīng)的動畫執(zhí)行順序。由于移動會打斷整體的連續(xù)性,所以把移動操作放在執(zhí)行隊列的最后面。- SnapHelper
SnapHelper 是控制RecyclerView滑動的。你控制他的滑動范圍,可以控制Fling,也就是慣性停止的地方。- ItemTouchHelper
ItemTouchHelper 控制Item的手勢,比如側(cè)滑刪除,比如拖動排序之類。- DiffUtil
DiffUtil通過對比兩組數(shù)據(jù)前后的差異,提供RecyclerView局部刷新的能力。
接下來我會一一介紹它們
LayoutManager
LayoutManager作為布局先鋒,負(fù)責(zé)RecyclerView內(nèi)部Item的測量和布局。說白了就是,RecyclerView自己不再負(fù)責(zé)Measure、Layout,全權(quán)委托給LayoutManager來處理。這樣做的好處就是職責(zé)清晰,開發(fā)者不但可以自由的使用列表、網(wǎng)格、瀑布流等常規(guī)的布局,還可以自定義LayoutManager來滿足特殊的列表需求。比如:
上圖中的這個復(fù)雜列表,在阿里系的APP上比較常見,比如優(yōu)酷、天貓。大致結(jié)構(gòu)是的:
public class Pager {
List<Card> mCards;
class Card {
List<Item> mItems;
class Item {
public int mId;
public String mName;
}
}
}
后端會返回List<Card> mCards 。其中每一個卡片,又是一個列表List<Item> mItems。這個時候LayoutManager就派上用場了,我們可以繼承LinearLayoutManager,來處理每一個卡片如何布局,同時,我們需要卡片重的Item打平,這樣就可以有效利用RecyclerView的緩存機制。在之后的系列文章中,我會詳細(xì)解釋。
阿里爸爸開源的復(fù)雜列表VLayout
這個是阿里開發(fā)的一個用于顯示復(fù)雜列表的LayoutManager,有興趣的可以看一眼。
** 總結(jié)來說,整個LayoutManager需要處理的任務(wù)如下:**
是否需要支持 wrap_content
如果支持,就需要先計算Adapter中所有item的大小,然后在計算RecyclerView自己的大小。整個過程比較消耗性能,迫不得已,不要使用。
預(yù)判動畫
item 添加、刪除、大小變化都可能觸發(fā)動畫,舉個例子,如果RecyclerView使用默認(rèn)的動畫,刪除Position為0的Item,其余的Item就會整體向上移動。這個時候就需要知道,item的偏移量,只之后真正的Layout做準(zhǔn)備。
開始真正的Layout
真正的布局需要RecyclerView的大小,Item的起始位置,布局方向,每一次布局之后的偏移量。
處理一些滾動
如果想讓我們RecyclerView滾動起來,就需要在LayoutManager來做特殊的處理。
自定義LayoutManger相對比較復(fù)雜。也不是我短短幾句話就能講清楚的,需要開發(fā)者不斷的寫Demo,查閱源碼或者相關(guān)文章,才能完成一個完整的LayoutManger。下面介紹自定義LayoutManger必須知道的幾個知識點:
public class DemoLayoutManager extends RecyclerView.LayoutManager {
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
//返回ItemView的默認(rèn)大小
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
/**
* 處理Item布局的問題
*/
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// 第一步:移除當(dāng)前界面中的item,并添加到回收站中
detachAndScrapAttachedViews(recycler);
int xOffset = 0;
int yOffset = 0;
// 第二步:把所有的Item放在他們應(yīng)該放的位置
for (int i = 0; i < getItemCount(); i++) {
View view = recycler.getViewForPosition(i); // 從回收站中取出View
addView(view);
measureChildWithMargins(view, 0, 0); // 計算View的大小
int width = getDecoratedMeasuredWidth(view);
int height = getDecoratedMeasuredHeight(view);
// 將view放置在正確的位置 (這個位置會收到ItemDecoration的影響)
layoutDecorated(view, xOffset, yOffset, xOffset + width, yOffset + height);
if (i % 6 == 5) {
xOffset = 0;
yOffset += height;
} else {
xOffset += width;
}
}
}
@Override
public boolean canScrollVertically() {
// 控制能否上下滾動
return true;
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
// 在滾動的時候移動view
offsetChildrenVertical(-dy);
return dy;
}
Recycler
Recycler作為回收站,Item的回收和復(fù)用都是由Recycler來控制的。那他是如何來處理的呢?
我們來看上面這張圖。圖中展示來RecyclerView復(fù)用ItemView的機制。其中 #、1、2、3、4 分別代表:
- "#":代表LayoutManager在處理布局時,需要從RecyclerView中獲得每一個Item的View。getViewForPosition。
- "1": Scap 直譯是廢料 暫且理解為廢品回收站。在Layout過程中暫時處于detached狀態(tài)的Views。屬于一級緩存。
- "2": Cache 代表當(dāng)前已經(jīng)不可見但并沒有從布局中移除的View。
屬于二級緩存。- "3":ViewCacheExtension 是留給開發(fā)者自定義的緩存池。屬于第三級緩存。官方并沒有給出默認(rèn)示例。我在目前的開發(fā)中,也沒有遇到使用這種緩存池的場景,如果大家有使用這種緩存池的場景,可以在留言中告訴我。
- "4":RecycledViewPool 最終的緩存池。也就是第四級緩存。RecycledViewPool提供按照不同Type緩存不用View的能力。
上面這個圖,我稍微說明一下。
1.當(dāng)RecyclerView需要更新數(shù)據(jù)的時候,包括 add、move、remove、change操作,如果當(dāng)前數(shù)據(jù)在可視范圍之內(nèi),就會直接從Scrap中獲取。
2.上下小幅度的滑動的時候,就需要用到cache中緩存的View了。
3.如果大幅度滾動,cache中緩存數(shù)據(jù)不夠用,或者調(diào)用了notifyDataSetChanged后,需要重新布局時,這時候就會調(diào)用Pool中的緩存。
4.值得注意的是,Scrap 和 cache 的數(shù)據(jù),是不需要重新綁定的。除了ChangedScarp這個特例。
ItemAnimator
ItemAnimator主要是處理動畫。這個動畫主要添加、刪除、移動的動畫。國內(nèi)開發(fā)者,需要特別絢麗多彩的動畫并不多。同樣我也沒有遇到這種需求。我認(rèn)為默認(rèn)的動畫就還不錯。
如果你真的想自定義ItemAnimator。我推薦Github的一個開源庫,大家可以參考參考。我之后有時間,也會詳細(xì)介紹這方面的知識。
https://github.com/wasabeef/recyclerview-animators
但是需要注意,一個Item的操作,可能會觸發(fā)多個動畫,比如,在中間位置插入一條數(shù)據(jù),這個時候,就會觸發(fā)插入的動畫,和原來這個位置以后的Item都向下移動的動畫。
DiffUtil
DiffUtil是最近版本中推出的一個工具。主要是幫助RecyclerView提升刷新效率的問題。我們舉一個例子來說這個問題。
這樣的搜索功能是很常見的。每次輸入不同的文字,都要給出該文字相對應(yīng)的搜索熱詞推薦。如果直接使用notifyDataSetChanged(),就會導(dǎo)致整個RecyclerView發(fā)生RequestLayout。我們都知道RequsetLayout會引起整個View樹重新遍歷一邊Measure和Layout。這樣非常消耗性能。而且,RecyclerView重新加載時,只會從RecyclerViewPool中拿緩存的Item。RecyclerViewPool默認(rèn)只會緩存5個Item。剩下的Item都需要重新走Create和inflate。之后他們還要重寫計算寬高,重新計算布局。這個過程非常耗時。
為了提供性能,我們就可以使用DiffUtil來對比兩組數(shù)據(jù),得到數(shù)組A切換到數(shù)組B的最少移動步驟。
“尋找diff”這件事,被抽象成了“尋找圖的路徑”了。那么,“最短的直觀的”diff對應(yīng)的路徑有什么特點呢?
路徑長度最短(對角線不算長度)
先向右,再向下(先刪除,后新增)
其實Myers算法是一個典型的”動態(tài)規(guī)劃“算法,也就是說,父問題的求解歸結(jié)為子問題的求解。要知道d=5時所有k對應(yīng)的最優(yōu)坐標(biāo),必須先要知道d=4時所有k對應(yīng)的最優(yōu)坐標(biāo),要知道d=4時的答案,必須先求解d=3,以此類推,和01背包問題很是相似。
public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
dispatchUpdatesTo(new ListUpdateCallback() {
@Override
public void onInserted(int position, int count) {
adapter.notifyItemRangeInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
adapter.notifyItemRangeRemoved(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
adapter.notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count, Object payload) {
adapter.notifyItemRangeChanged(position, count, payload);
}
});
}
在使用DiffUtil得到變化之后,我們可以調(diào)用RecyclerView的局部刷新機制。這樣不需要RequestLayout。刷新效率非常高。
那DiffUtil的對比數(shù)據(jù)的效率怎么樣呢。
這里有一組官方的數(shù)據(jù):
100 items and 10 modifications: avg: 0.39 ms, median: 0.35 ms
100 items and 100 modifications: 3.82 ms, median: 3.75 ms
100 items and 100 modifications without moves: 2.09 ms, median: 2.06 ms
1000 items and 50 modifications: avg: 4.67 ms, median: 4.59 ms
1000 items and 50 modifications without moves: avg: 3.59 ms, median: 3.50 ms
1000 items and 200 modifications: 27.07 ms, median: 26.92 ms
1000 items and 200 modifications without moves: 13.54 ms, median: 13.36 ms
都在16ms以內(nèi),看來完成可以在主線程執(zhí)行。
ItemDecoration
ItemDecoration 很簡單。就是RecyclerView的裝飾品。你可以想象RecyclerView是個小姑娘。ItemDecoration就是小姑娘的化妝品。
public void onDraw(Canvas c, RecyclerView parent, State state) {
//在畫Item之前。
}
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
//在畫Item之后
}
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
// 設(shè)置邊距
outRect.set(left, top, right, bottom);
}
** 上面這種圖中,分別體現(xiàn)了上面的三個方法。**
- 設(shè)置邊距,上圖中的灰色區(qū)域就是變局,分別代表左、上、右、下的邊界。
- 上圖中紅色999的便簽,就是在onDrawOver中畫動。
- 上圖中紅色五角星背景,則是onDraw畫的。
值得注意的事,在給Item畫裝飾品的時候,一定要注意Item本身的位置。
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
if (state.isMeasuring()) return;
c.save();
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
int position = parent.getChildAdapterPosition(child);
// 獲取你的標(biāo)簽中顯示的數(shù)量
int count = parent.getAdapter().getItemTag();
if (count == 0) continue;
String strCount = String.valueOf(count);
float textWidth = 0;
if (count >= 10) {
textWidth = mTextPaint.measureText(strCount) - mRadius;
}
mRoundRectF.left = child.getRight() - textWidth - mRadius - mLeft;
mRoundRectF.top = child.getTop() - mRadius + mTop;
mRoundRectF.right = child.getRight() + mRadius - mLeft;
mRoundRectF.bottom = child.getTop() + mRadius + mTop;
c.drawRoundRect(mRoundRectF, mRadius, mRadius, mPopPaint);
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
float baseline = child.getTop() - ((fontMetrics.descent + fontMetrics.ascent) / 2);
c.drawText(strCount, mRoundRectF.left + mRoundRectF.width() / 2, baseline + mTop, mTextPaint);
}
c.restore();
}
SnapHelper
有關(guān)SnapHelper的 請看這里。我之后會講。
https://github.com/rubensousa/RecyclerViewSnap
ItemTouchHelper
有關(guān)ItemTouchHelper的 請看這里。我之后會講。
https://github.com/iPaulPro/Android-ItemTouchHelper-Demo
AdapterHelper
以后補全
ChildHelper
在發(fā)生移除動畫時,對于ViewGroup來說,由于動畫還在發(fā)生,所以View并沒有被真正的從ViewGroup中移除。而對于LayoutManager來說,這個View已經(jīng)被移除,需要對他做回收處理。
這個時候,LayoutManager 操作View的時候,比如getChildAt()。這個方法并不是真正沖ViewGroup中獲取,而是從ChildHelper維護的View隊列中獲取。
使用RecyclerView的注意點
1.如何RecyclerView的寬高不隨著內(nèi)容的變化而變化,就可以使用如下方法來提高性能
mRecyclerView.setHasFixedSize(true);
2.如果想提高RecyclerView的滑動流暢性,可以適度增加Cache的大小,默認(rèn)大小是2。但是也不能太大,如果太大,會影響初始化的效率。
mRecyclerView. setItemViewCacheSize(5);
3.如果多個RecyclerView顯示的Item一樣。比如:
比如這種情況,就可以使用公告緩存池。
int type0 = 0;
int type1 = 1;
int type2 = 2;
RecyclerViewPool mPool = new RecyclerViewPool();
mPool.setMaxRecycledViews(type0, 10);
mPool.setMaxRecycledViews(type1, 10);
mPool.setMaxRecycledViews(type2, 10);
3.有時候我們會在Item上添加一些手勢處理,比如最常見的側(cè)滑刪除,Item拖拽等等。在有些特殊的手機上,你會發(fā)現(xiàn)拖拽不靈敏,這個時候就可以用
mRecyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);
4.RecyclerView 默認(rèn)是帶Item動畫的。如果你不需要動畫,或者性能要求嚴(yán)格,可以關(guān)閉動畫。
mRecyclerView.setItemAnimator(null);
5.RecyclerView如果要現(xiàn)實圖片,可以在慣性滾動的時候暫停圖片加載,這樣可以提升流暢度。
mRecyclerView.setOnFlingListener();
參考資料
1.http://v.youku.com/v_show/id_XMTU4MTQ1ODg2NA==.html?f=27314446&debug=flv
2.https://blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1/
3.com.android.support:recyclerview-v7:25.3.1