深入淺出 RecyclerView

這篇文章分三個部分,簡單跟大家講一下 RecyclerView 的常用方法與奇葩用法;工作原理與ListView比較;源碼解析;

常用方法

RecyclerView 與 ListView、GridView 類似,都是可以顯示同一種類型 View 的集合的控件。
首先看看最簡單的用法,四步走:

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

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

1.創建對象

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

2.設置顯示規則

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

RecyclerView 將所有的顯示規則交給一個叫 LayoutManager 的類去完成了。
LayoutManager 是一個抽象類,系統已經爲我們提供了三個默認的實現類,分別是 LinearLayoutManager 、 GridLayoutManager 、 StaggeredGridLayoutManager,從名字我們就能看出來了,分別是,線性顯示、網格顯示、瀑布流顯示。當然你也可以通過繼承這些類來擴展實現自己的 LayougManager

3.設置適配器

recyclerview.setAdapter(adapter);

適配器,同 ListView 一樣,用來設置每個item顯示內容的。 
通常,我們寫 ListView 適配器,都是首先繼承 BaseAdapter,實現四個抽象方法,創建一個靜態 ViewHolder, getView() 方法中判斷 convertView 是否爲空,創建還是獲取 viewholder 對象。
而 RecyclerView 也是類似的步驟,首先繼承RecyclerView.Adapter<VH>類,實現三個抽象方法,創建一個靜態的 ViewHolder。不過 RecyclerView 的 ViewHolder 創建稍微有些限制,類名就是上面繼承的時候泛型中聲明的類名(好像反了,應該是上面泛型中的類名應該是這個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;
    }

    @Override
    public VH onCreateViewHolder(ViewGroup parent, int viewType) {
        return new VH(View.inflate(context, android.R.layout.simple_list_item_2, null));
    }

    @Override
    public void onBindViewHolder(VH holder, int position) {
        holder.mTextView.setText(dataList.get(position).getNum());
    }

    @Override
    public 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);
        }
    }
}

更多方法

除了常用方法,當然還有不常用的。

  • 瀑布流與滾動方向

前面已經介紹過,RecyclerView實現瀑布流,可以通過一句話設置:recycler.setLayoutManager(new StaggeredGridLayoutManager(2, VERTICAL))就可以了。
其中 StaggeredGridLayoutManager 第一個參數表示列數,就好像 GridView 的列數一樣,第二個參數表示方向,可以很方便的實現橫向滾動或者縱向滾動。  
使用 demo 可以查看:Github 【RecyclerView簡單使用

  • 添加刪除 item 的動畫

同 ListView 每次修改了數據源後,都要調用 notifyDataSetChanged() 刷新每項 item 類似,只不過 RecyclerView 還支持局部刷新 notifyItemInserted(index);、 notifyItemRemoved(position)notifyItemChanged(position)
在添加或刪除了數據後,RecyclerView 還提供了一個默認的動畫效果,來改變顯示。同時,你也可以定製自己的動畫效果:模仿 DefaultItemAnimator 或直接繼承這個類,實現自己的動畫效果,並調用recyclerview.setItemAnimator(new DefaultItemAnimator()); 設置上自己的動畫。 
使用 demo 可以查看:Github 【RecyclerView默認動畫

  • LayoutManager的常用方法

findFirstVisibleItemPosition() 返回當前第一個可見 Item 的 position
findFirstCompletelyVisibleItemPosition() 返回當前第一個完全可見 Item 的 position
findLastVisibleItemPosition() 返回當前最後一個可見 Item 的 position
findLastCompletelyVisibleItemPosition() 返回當前最後一個完全可見 Item 的 position. 
scrollBy() 滾動到某個位置。

  • adapter封裝

其實很早之前寫過一篇關於 RecyclerView 適配器的封裝,所以這不再贅述了,傳送門:RecyclerView的通用適配器
使用 demo 可以查看:Github 【RecyclerView通用適配器演示

吐槽

  • OnItemTouchListener 什麼鬼?

用習慣了 ListView 的 OnItemClickListener ,RecyclerView 你的 OnItemClickListener 呢? 
Tell me where do I find, something like ListView listener ?

好吧,翻遍了 API 列表,就找到了個 OnItemTouchListener ,這特麼什麼鬼,我幹嘛要對每個 item 監聽觸摸屏事件。
萬萬沒想到,最終我還是在 Google IO 裏面的介紹找到了原因。原來是 Google 的工程師分不清究竟是改給 listview 的 item 添加點擊事件,還是應該給每個 item 的 view 添加點擊事件,索性就不給 OnItemClickListener了,然後在 support demo 裏面,你就會發現,RecyclerView 的 item 點擊事件都是寫在了 adapter 的 ViewHolder 裏面。

當然,除了 support demo 包裏面使用的在 ViewHolder 裏面設置點擊事件以外,我還寫好了一個 RecyclerView使用的 OnItemClickListener 代碼請見:RecyclerItemClickListener.java

需要一提的是,網上有很多這種類似的 ItemClickListener ,在使用的時候一定注意一個問題,就是循環引用問題。比如 listener 裏面持有了一個 recyclerview, 而這個 recyclerview 在調用 setListener() 的時候又持有了一個 listener。儘管 Java 虛擬機現在可以解決這種問題了,但作爲代碼編寫者,這種寫法還是應該儘量避免的。

  • divider 跑哪了?

ListView中設置 divider 非常簡單,只需要在 XML 文件中設置就可以了,同時還可以設置 divider 高度。

android:divider="@android:color/black"
android:dividerHeight="2dp"

而在RecyclerView裏面,想實現這兩種需求,稍微複雜一點,需要自己繼承RecyclerView.ItemDecoration來實現想要實現的方法。
雖說這樣寫靈活多了,但是要額外寫一個類去做難免麻煩,這裏大家可以看我已經實現好的一個封裝,包括顯示純色divider顯示圖片dividerdivider的上下左右的間距寬高設置 應該可以滿足基本需求了:Divider.java 
使用 demo 可以查看:Github 【自定義 Divider 使用

五虎上將工作原理

借用 Google IO 視頻中的一張截圖:
視頻的完整地址可查看: RecyclerView ins and outs - Google I/O 2016

RecyclerView

其實上圖中並沒有寫完整,大 boss RecyclerView 應該有這五虎上將:

類名 作用
RecyclerView.LayoutManager 負責Item視圖的佈局的顯示管理
RecyclerView.ItemDecoration 給每一項Item視圖添加子View,例如可以進行畫分隔線之類
RecyclerView.ItemAnimator 負責處理數據添加或者刪除時候的動畫效果
RecyclerView.Adapter 爲每一項Item創建視圖
RecyclerView.ViewHolder 承載Item視圖的子佈局

LayoutManager工作原理

java.lang.Object  
   ↳ android.view.View  
        ↳ android.view.ViewGroup  
            ↳ android.support.v7.widget.RecyclerView

首先是 RecyclerView 繼承關係,可以看到,與 ListView 不同,他是一個 ViewGroup。既然是一個 View,那麼就不可少的要經歷 onMeasure()onLayout()onDraw() 這三個方法。 實際上,RecyclerView 就是將 onMeasure()onLayout() 交給了 LayoutManager 去處理,因此如果給 RecyclerView 設置不同的 LayoutManager 就可以達到不同的顯示效果,因爲onMeasure()onLayout()都不同了嘛。

ItemDecoration 工作原理

ItemDecoration 是爲了顯示每個 item 之間分隔樣式的。它的本質實際上就是一個 Drawable。當 RecyclerView 執行到 onDraw() 方法的時候,就會調用到他的 onDraw(),這時,如果你重寫了這個方法,就相當於是直接在 RecyclerView 上畫了一個 Drawable 表現的東西。 而最後,在他的內部還有一個叫getItemOffsets()的方法,從字面就可以理解,他是用來偏移每個 item 視圖的。當我們在每個 item 視圖之間強行插入繪畫了一段 Drawable,那麼如果再照着原本的邏輯去繪 item 視圖,就會覆蓋掉 Decoration 了,所以需要getItemOffsets()這個方法,讓每個 item 往後面偏移一點,不要覆蓋到之前畫上的分隔樣式了。

ItemAnimator

每一個 item 在特定情況下都會執行的動畫。說是特定情況,其實就是在視圖發生改變,我們手動調用notifyxxxx()的時候。通常這個時候我們會要傳一個下標,那麼從這個標記開始一直到結束,所有 item 視圖都會被執行一次這個動畫。

Adapter工作原理

首先是適配器,適配器的作用都是類似的,用於提供每個 item 視圖,並返回給 RecyclerView 作爲其子佈局添加到內部。
但是,與 ListView 不同的是,ListView 的適配器是直接返回一個 View,將這個 View 加入到 ListView 內部。而 RecyclerView 是返回一個 ViewHolder 並且不是直接將這個 holder 加入到視圖內部,而是加入到一個緩存區域,在視圖需要的時候去緩存區域找到 holder 再間接的找到 holder 包裹的 View。

ViewHolder

每個 ViewHolder 的內部是一個 View,並且 ViewHolder 必須繼承自RecyclerView.ViewHolder類。 這主要是因爲 RecyclerView 內部的緩存結構並不是像 ListView 那樣去緩存一個 View,而是直接緩存一個 ViewHolder ,在 ViewHolder 的內部又持有了一個 View。既然是緩存一個 ViewHolder,那麼當然就必須所有的 ViewHolder 都繼承同一個類才能做到了。

緩存與複用的原理

還是一張截圖
RecyclerView

RecyclerView 的內部維護了一個二級緩存,滑出界面的 ViewHolder 會暫時放到 cache 結構中,而從 cache 結構中移除的 ViewHolder,則會放到一個叫做 RecycledViewPool 的循環緩存池中。
順帶一說,RecycledView 的性能並不比 ListView 要好多少,它最大的優勢在於其擴展性。但是有一點,在 RecycledView 內部的這個第二級緩存池 RecycledViewPool 是可以被多個 RecyclerView 共用的,這一點比起直接緩存 View 的 ListView 就要高明瞭很多,但也正是因爲需要被多個 RecyclerView 公用,所以我們的 ViewHolder 必須繼承自同一個基類(即RecyclerView.ViewHolder)。
默認的情況下,cache 緩存 2 個 holder,RecycledViewPool 緩存 5 個 holder。對於二級緩存池中的 holder 對象,會根據 viewType 進行分類,不同類型的 viewType 之間互不影響。

源碼解析

onMeasure

既然是一個 View,我們先從onMeasure()開始看。
之前我們就說了 RecyclerView 的 measure 和 layout 都是交給了 LayoutManager 去做的,來看一下爲什麼:

if (mLayout.mAutoMeasure) {
    final int widthMode = MeasureSpec.getMode(widthSpec);
    final int heightMode = MeasureSpec.getMode(heightSpec);
    final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
            && heightMode == MeasureSpec.EXACTLY;
    mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
} else {
    mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
}

不論是否啓用 mAutoMeasure 最終都會執行到 mLayout.onMeasure() 方法中,而這個 mLayout 就是一個 LayoutManager 對象。

我們挑選 LinearLayoutManager 來看
發現它並沒有onMeasure()方法,LinearLayoutManager 直接繼承自 LayoutManager,所以又回到了父類 LayoutManager 中。

void defaultOnMeasure(int widthSpec, int heightSpec) {
    // calling LayoutManager here is not pretty but that API is already public and it is better
    // than creating another method since this is internal.
    final int width = LayoutManager.chooseSize(widthSpec,
            getPaddingLeft() + getPaddingRight(),
            ViewCompat.getMinimumWidth(this));
    final int height = LayoutManager.chooseSize(heightSpec,
            getPaddingTop() + getPaddingBottom(),
            ViewCompat.getMinimumHeight(this));

    setMeasuredDimension(width, height);
}

有一句非常奇葩的註釋:在這裏直接調用 LayoutManager 靜態方法並不完美,因爲本身就是在類內部,更好的辦法調用一個單獨的方法。但反正這段代碼也已經公開了,你們自己看着辦。。。。。。
如果這不是歷史遺留問題,那肯定是臨時工寫的,你寫的時候都意識到這問題了,你還把一大堆類都寫在一個類裏面,造成了 RecyclerView 一個類有一萬多行代碼。我猜你是爲了類之間跨類調用方便一點,可是你就不能設置一個包訪問權限,所有類成員方法都包內調用嗎,一個類幹了六個類的活,網上居然還有人說這是高內聚的表現。

接着是chooseSize()方法,很簡單,直接根據測量值和模式返回了最適大小。

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);
    }
}

緊接着是對子控件 measure ,調用了:dispatchLayoutStep2() 調用了相同的方法,子控件的 measure 在 layout 過程中講解

onLayout

然後我們來看 layout 過程. 在onLayout()方法中間接的調用到了這麼一個方法:dispatchLayoutStep2(),在它之中又調用到了mLayout.onLayoutChildren(mRecycler, mState); 
我們重點看這個onLayoutChildren()方法。 
這個方法在 LayoutManager 中的實現是空的,那麼想必是在子類中實現了吧。還是找 LinearLayoutManager,跟上面 measure 過程一樣,調用了dispatchLayoutStep2() 跟進去發現這麼一個方法:

fill(recycler, mLayoutState, state, false);

onLayoutChildren() 中有一個非常重要的方法:fill()
recycler,是一個全局的回收複用池,用於對每個itemview回收以及複用提供支持。稍後會詳細講這個。

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() 作用就是根據當前狀態決定是應該從緩存池中取 itemview 填充 還是應該回收當前的 itemview。 
其中,layoutChunk() 負責從緩存池 recycler 中取 itemview,並調用View.addView() 將獲取到的 ItemView 添加到 RecyclerView 中去,並調用 itemview 自身的 layout 方法去佈局 item 位置。
同時在這裏,還調用了measureChildWithMargins()來測繪子控件大小以及設置顯示位置。這一步,我們到下面的 draw 過程還要講。
而這全部的添加邏輯都放在一個 while 循環裏面,不停的添加 itemview 到 recyclerview 裏面,直到塞滿所有可見區域爲止。

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() 中,除了繪製自己以外,還多調了一個mItemDecorations 的 onDraw() 方法,這個 mItemDecorations 就是前面吐槽的分隔線的集合。
之前在講 RecyclerView 的五虎上將的時候就講過這個 ItemDecoration。 當時我們還重寫了一個方法叫getItemOffsets()目的是爲了不讓 itemview 擋住分隔線。那他是在哪調用的呢? 
還記得 layout 時說的那個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 的時候,會把偏移量也計算進來,也就是說:其實 ItemDecoration 的寬高是計算在 itemview 中的,只不過 itemview 本身繪製區域沒有那麼大,留出來的地方正好的透明的,於是就透過 itemview 顯示出了 ItemDecoration。那麼就很有意思了,如果我故意在 ItemDecoration 的偏移量中寫成0,那麼 itemview 就會擋住 ItemDecoration,而在 itemview 的增加或刪除的時候,會短暫的消失(透明),這時候就又可以透過 itemview 看到 ItemDecoration 的樣子。使用這種組合還可以做出意想不到的動畫效果。

滾動

前面我們已經完整的走完了 RecyclerView 的繪製流程。接下來我們再看看它在滾動的時候代碼又是怎麼調用的。
說到滾動,自然要看 onTouch() 方法的 MOVE 狀態。

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;

看到這段代碼的時候,特意去搜了一下,MotionEventCompat 這個類是幹嘛的。 他是 v4 包裏面提供的一個工具類,用於兼容低版本的觸摸屏手勢。平時用的時候更多的是用它來處理多點觸控的情況,當成MotionEvent就可以了。

dispatchNestedPreScroll() 用於處理嵌套邏輯,例如在 ScrollView 裏面放一個 RecyclerView ,如果是以前用 ListView ,還得要把高度寫死,禁止 ListView 的複用和滾動邏輯,而 RecyclerView 則完全不需要更多處理,直接用就是了。而且有一個非常好的地方,如果放到 ScrollView 裏面,ListView 的 ItemView 是不會複用的,而 RecyclerView 因爲是全局公用一套緩存池,雖說嵌套到 ScrollView 效率會低很多,但比起 ListView 嵌套要好很多,之後講緩存池的時候,我們繼續講。

再之後,如果在相應方向上手指move的距離達到最大值,則認爲需要滾動,並設置爲滾動狀態(SCROLL_STATE_DRAGGING),這個最大距離默認是 8 個像素。

接着走出 if 塊,如果是滾動狀態,則調用滾動方法scrollByInternal()執行相應方向的滾動。滾動的距離當然就是手指移動的距離。跟進去看,果然是調用了LinearLayoutManager.scrollBy()方法,又印證了前面【更多操作】裏面講 LayoutManager 可以滾動 RecyclerView 的方法。

以上就是滾動的邏輯了。 但是沒完,就像 ListView,在手指劃過以後,手指離開了屏幕,相關性一樣,View 自己依舊可以自己滾動一段距離。 
既然手指離開了屏幕,那就去 UP 或者 CANCEL 狀態去找。

case MotionEvent.ACTION_CANCEL: {
    cancelTouch();
} break;

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

ACTION_CANCEL 裏面只有一個 cancelTouch() ,那麼自然是在 UP 狀態裏面實現的慣性滾動。
看到了一個 mVelocityTracker 對象,大概原理也就清楚了,慣性滾動多長,肯定是跟手指移動的速度有關了。
再往下,跟進fling()方法裏面看:調用了mViewFlinger.fling(velocityX, velocityY);
再進:

mScroller.fling(0, 0, velocityX, velocityY,
                    Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);

原來是調用了 Scroller 類的fling()方法,再仔細看一下,發現是ScrollerCompat 看名字,估計又是用來兼容舊版本的 support 包裏面的 Scroller 類。關於這個Scroller類,他是一個可以用來實現平滑滾動效果的類,其實內部實現也是通過一點一點移動 view,利用了人眼的視覺暫留。

回收與複用

前面講 layout、滾動的時候,都出現了一個東西,叫 Recycler,現在我們就來看看他到底是個什麼。

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。看來他就是一個超大型的緩存器了。
事實上他確實就是一個超大型的緩存器,擁有三級緩存(如果算上創建的那一次,應該是四級了),這麼大的緩存系統,究竟是如何完成的?

  • 第一級緩存:

就是上面的一系列 mCachedViews。如果仍依賴於 RecyclerView (比如已經滑動出可視範圍,但還沒有被移除掉),但已經被標記移除的 ItemView 集合會被添加到 mAttachedScrap 中。然後如果 mAttachedScrap 中不再依賴時會被加入到 mCachedViews 中。 mChangedScrap 則是存儲 notifXXX 方法時需要改變的 ViewHolder 。

  • 第二級緩存:

ViewCacheExtension 是一個抽象靜態類,用於充當附加的緩存池,當 RecyclerView 從第一級緩存找不到需要的 View 時,將會從 ViewCacheExtension 中找。不過這個緩存是由開發者維護的,如果沒有設置它,則不會啓用。通常我們也不會去設置他,系統已經預先提供了兩級緩存了,除非有特殊需求,比如要在調用系統的緩存池之前,返回一個特定的視圖,纔會用到他。

  • 第三級緩存:

最強大的緩存器。之前講了,與 ListView 直接緩存 ItemView 不同,從上面代碼裏我們也能看到,RecyclerView 緩存的是 ViewHolder。而 ViewHolder 裏面包含了一個 View 這也就是爲什麼在寫 Adapter 的時候 必須繼承一個固定的 ViewHolder 的原因。首先來看一下 RecycledViewPool:

public static class RecycledViewPool {
 
 // 根據 viewType 保存的被廢棄的 ViewHolder 集合,以便下次使用
 private SparseArray<ArrayList<ViewHolder>> mScrap = new SparseArray<ArrayList<ViewHolder>>();
  /**
   * 從緩存池移除並返回一個 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);
  }
 
  /**
   * 根據 viewType 獲取對應緩存池
   */
  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;
  }
}

從名字來看,他是一個緩存池,實現上,是通過一個默認爲 5 大小的 ArrayList 實現的。這一點,同 ListView 的 RecyclerBin 這個類一樣。很奇怪爲什麼不用 LinkedList 來做,按理說這種不需要索引讀取的緩存池,用鏈表是最合適的。 
然後每一個 ArrayList 又都是放在一個 Map 裏面的,SparseArray 這個類我們在講性能優化的時候已經多次提到了,就是兩個數組,用來替代 Map 的。
把所有的 ArrayList 放在一個 Map 裏面,這也是 RecyclerView 最大的亮點,這樣根據 itemType 來取不同的緩存 Holder,每一個 Holder 都有對應的緩存,而只需要爲這些不同 RecyclerView 設置同一個 Pool 就可以了。
這一點我們在 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 是共存於同一個 Pool 中的。

寫了這麼多累死我了,就這樣吧,最後發一個 demo 地址:RecyclerViewDemo
和一份內部分享的 PPT 地址:RecyclerView PPT

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章