Android RecyclerView工作原理分析(上)

基本使用
  RecyclerView的基本使用并不復(fù)雜嗦董,只需要提供一個(gè)RecyclerView.Apdater的實(shí)現(xiàn)用于處理數(shù)據(jù)集與ItemView的綁定關(guān)系局蚀,和一個(gè)RecyclerView.LayoutManager的實(shí)現(xiàn)用于 測量并布局 ItemView歉嗓。
繪制流程
  眾所周知,Android控件的繪制可以分為3個(gè)步驟:measure色建、layout奠旺、draw。RecyclerView的繪制自然也經(jīng)這3個(gè)步驟沾鳄。但是慨飘,RecyclerView將它的measure與layout過程委托給了RecyclerView.LayoutManager來處理,并且译荞,它對子控件的measure及l(fā)ayout過程是逐個(gè)處理的瓤的,也就是說,執(zhí)行完成一個(gè)子控件的measure及l(fā)ayout過程再去執(zhí)行下一個(gè)吞歼。下面看下這段代碼:

protected void onMeasure(int widthSpec, int heightSpec) {
    ...
    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);
        if (skipMeasure || mAdapter == null) {
            return;
        }
        ...
        dispatchLayoutStep2();

        mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
        ...
    } else {
        ...
    }
}

這是RecyclerView的測量方法圈膏,再看下dispatchLayoutStep2()方法:

private void dispatchLayoutStep2() {
    ...
    mLayout.onLayoutChildren(mRecycler, mState);
    ...
}

上面的mLayout就是一個(gè)RecyclerView.LayoutManager實(shí)例。通過以上代碼(和方法名稱)篙骡,不難推斷出稽坤,RecyclerView的measure及l(fā)ayout過程委托給了RecyclerView.LayoutManager。接著看onLayoutChildren方法糯俗,在兼容包中提供了3個(gè)RecyclerView.LayoutManager的實(shí)現(xiàn)尿褪,這里我就只以LinearLayoutManager來舉例說明:

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    // layout algorithm:
    // 1) by checking children and other variables, find an anchor coordinate and an anchor
    //  item position.
    // 2) fill towards start, stacking from bottom
    // 3) fill towards end, stacking from top
    // 4) scroll to fulfill requirements like stack from bottom.
    ...
    mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
    // calculate anchor position and coordinate
    updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
    ...
    if (mAnchorInfo.mLayoutFromEnd) {
        ...
    } else {
        // fill towards end
        updateLayoutStateToFillEnd(mAnchorInfo);
        mLayoutState.mExtra = extraForEnd;
        fill(recycler, mLayoutState, state, false);
        endOffset = mLayoutState.mOffset;
        final int lastElement = mLayoutState.mCurrentPosition;
        if (mLayoutState.mAvailable > 0) {
            extraForStart += mLayoutState.mAvailable;
        }
        // fill towards start
        updateLayoutStateToFillStart(mAnchorInfo);
        mLayoutState.mExtra = extraForStart;
        mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
        fill(recycler, mLayoutState, state, false);
        startOffset = mLayoutState.mOffset;
        ...
    }
    ...
}

源碼中的注釋部分我并沒有略去,它已經(jīng)解釋了此處的邏輯了得湘。這里我以垂直布局來說明杖玲,mAnchorInfo為布局錨點(diǎn)信息,包含了子控件在Y軸上起始繪制偏移量(coordinate)忽刽,ItemView在Adapter中的索引位置(position)和布局方向(mLayoutFromEnd)——這里是指start天揖、end方向。這部分代碼的功能就是:確定布局錨點(diǎn)跪帝,以此為起點(diǎn)向開始和結(jié)束方向填充ItemView今膊,如圖所示:

這里寫圖片描述

在上一段代碼中,fill()方法的作用就是填充ItemView伞剑,而圖(3)說明了斑唬,在上段代碼中fill()方法調(diào)用2次的原因。雖然圖(3)是更為普遍的情況,而且在實(shí)現(xiàn)填充ItemView算法時(shí)恕刘,也是按圖(3)所示來實(shí)現(xiàn)的缤谎,但是mAnchorInfo在賦值過程(updateAnchorInfoForLayout)中,只會出現(xiàn)圖(1)褐着、圖(2)所示情況】涝瑁現(xiàn)在來看下fill()方法:

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    ...
    int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
    LayoutChunkResult layoutChunkResult = new LayoutChunkResult();
    while (...&&layoutState.hasMore(state)) {
        ...
        layoutChunk(recycler, state, layoutState, layoutChunkResult);

        ...
        if (...) {
            layoutState.mAvailable -= layoutChunkResult.mConsumed;
            remainingSpace -= layoutChunkResult.mConsumed;
        }
        if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
            layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            recycleByLayoutState(recycler, layoutState);
        }
    }
    ...
}

下面是layoutChunk()方法:

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    View view = layoutState.next(recycler);
    ...
    if (layoutState.mScrapList == null) {
        if (mShouldReverseLayout == (layoutState.mLayoutDirection
                == LayoutState.LAYOUT_START)) {
            addView(view);
        } else {
            addView(view, 0);
        }
    }
    ...
    measureChildWithMargins(view, 0, 0);
    ...
    // We calculate everything with View's bounding box (which includes decor and margins)
    // To calculate correct layout position, we subtract margins.
    layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
            right - params.rightMargin, bottom - params.bottomMargin);
    ...
}

這里的addView()方法,其實(shí)就是ViewGroup的addView()方法含蓉;measureChildWithMargins()方法看名字就知道是用于測量子控件大小的频敛,這里我先跳過這個(gè)方法的解釋,放在后面來做馅扣,目前就簡單地理解為測量子控件大小就好了斟赚。下面是layoutDecoreated()方法:

public void layoutDecorated(...) {
        ...
        child.layout(...);
}

總結(jié)上面代碼,在RecyclerView的measure及l(fā)ayout階段差油,填充ItemView的算法為:向父容器增加子控件拗军,測量子控件大小,布局子控件蓄喇,布局錨點(diǎn)向當(dāng)前布局方向平移子控件大小发侵,重復(fù)上訴步驟至RecyclerView可繪制空間消耗完畢或子控件已全部填充。   這樣所有的子控件的measure及l(fā)ayout過程就完成了公罕∑鹘簦回到RecyclerView的onMeasure方法,執(zhí)行mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec)這行代碼的作用就是根據(jù)子控件的大小楼眷,設(shè)置RecyclerView的大小。至此熊尉,RecyclerView的measure和layout實(shí)際上已經(jīng)完成了罐柳。   但是,你有可能已經(jīng)發(fā)現(xiàn)上面過程中的問題了:如何確定RecyclerView的可繪制空間狰住?不過张吉,如果你熟悉android控件的繪制機(jī)制的話,這就不是問題催植。其實(shí)肮蛹,這里的可繪制空間,可以簡單地理解為父容器的大写茨稀伦忠;更準(zhǔn)確的描述是,父容器對RecyclerView的布局大小的要求稿辙,可以通過MeasureSpec.getSize()方法獲得——這里不包括滑動情況昆码,滑動情況會在后文描述。需要特別說明的是在23.2.0版本之前,RecyclerView是不支持WRAP_CONTENT的赋咽。先看下RecyclerView的onLayout()方法:

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    ...
    dispatchLayout();
    ...
}

這是dispatchLayout()方法:

void dispatchLayout() {
    ...
    if (mState.mLayoutStep == State.STEP_START) {
        dispatchLayoutStep1();
        ...
        dispatchLayoutStep2();
    }
    dispatchLayoutStep3();
    ...
}

可以看出旧噪,這里也會執(zhí)行子控件的measure及l(fā)ayout過程。結(jié)合onMeasure方法對skipMeasure的判斷可以看出脓匿,如果要支持WRAP_CONTENT淘钟,那么子控件的measure及l(fā)ayout就會提前在RecyclerView的測量方法中執(zhí)行完成,也就是說陪毡,先確定了子控件的大小及位置后日月,再由此設(shè)置RecyclerView的大小缤骨;如果是其它情況(測量模式為EXACTLY)爱咬,子控件的measure及l(fā)ayout過程就會延遲至RecyclerView的layout過程(RecyclerView.onLayout())中執(zhí)行。再看onMeasure方法中的mLayout.mAutoMeasure绊起,它表示精拟,RecyclerView的measure及l(fā)ayout過程是否要委托給RecyclerView.LayoutManager,在兼容包中提供的3種RecyclerView.LayoutManager的這個(gè)屬性默認(rèn)都是為true的虱歪。好了蜂绎,以上就是RecyclerView的measure及l(fā)ayout過程,下面來看下它的draw過程笋鄙。   RecyclerView的draw過程可以分為2部分來看:RecyclerView負(fù)責(zé)繪制所有decoration师枣;ItemView的繪制由ViewGroup處理,這里的繪制是android常規(guī)繪制邏輯萧落,本文就不再闡述了践美。下面來看看RecyclerView的draw()和onDraw()方法:

@Override
public void draw(Canvas c) {
    super.draw(c);

    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDrawOver(c, this, mState);
    }
    ...
}

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

可以看出對于decoration的繪制代碼上十分簡單。但是這里找岖,我必須要抱怨一下RecyclerView.ItemDecoration的設(shè)計(jì)陨倡,它實(shí)在是太過于靈活了,雖然理論上我們可以使用它在RecyclerView內(nèi)的任何地方繪制你想要的任何東西——到這一步许布,RecyclerView的大小位置已經(jīng)確定的哦兴革。但是過于靈活,太難使用蜜唾,以至往往使我們無從下手杂曲。 好了,題外話就不多說了袁余,來看看decoration的繪制吧擎勘。還記得上面提到過的measureChildWithMargins()方法嗎?先來看看它:

public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();

        final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
        widthUsed += insets.left + insets.right;
        heightUsed += insets.top + insets.bottom;

        final int widthSpec = ...
        final int heightSpec = ...
        if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
            child.measure(widthSpec, heightSpec);
        }
    }

這里是getItemDecorInsetsForChild()方法:

 Rect getItemDecorInsetsForChild(View child) {
    ...
    final Rect insets = lp.mDecorInsets;
    insets.set(0, 0, 0, 0);
    final int decorCount = mItemDecorations.size();
    for (int i = 0; i < decorCount; i++) {
        mTempRect.set(0, 0, 0, 0);
        mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
        insets.left += mTempRect.left;
        insets.top += mTempRect.top;
        insets.right += mTempRect.right;
        insets.bottom += mTempRect.bottom;
    }
    lp.mInsetsDirty = false;
    return insets;
}

方法getItemOffsets()就是我們在實(shí)現(xiàn)一個(gè)RecyclerView.ItemDecoration時(shí)可以重寫的方法泌霍,通過mTempRect的大小货抄,可以為每個(gè)ItemView設(shè)置位置偏移量述召,這個(gè)偏移量最終會參與計(jì)算ItemView的大小,也就是說ItemView的大小是包含這個(gè)位置偏移量的蟹地。我們在重寫getItemOffsets()時(shí)积暖,可以指定任意數(shù)值的偏移量:


這里寫圖片描述

4個(gè)方向的位置偏移量對應(yīng)mTempRect的4個(gè)屬性(left,top,right,bottom),我以top offset的值在垂直線性布局中的應(yīng)用來舉例說明下怪与。如果top offset等于0夺刑,那么ItemView之間就沒有空隙;如果top offset大于0分别,那么ItemView之前就會有一個(gè)間隙遍愿;如果top offset小于0,那么ItemView之間就會有重疊的區(qū)域耘斩。   當(dāng)然沼填,我們在實(shí)現(xiàn)RecyclerView.ItemDecoration時(shí),并不一定要重寫getItemOffsets()括授,同樣的對于RecyclerView.ItemDecoration.onDraw()或RecyclerView.ItemDecoration.onDrawOver()方法也不是一定要重寫坞笙,而且,這個(gè)繪制方法和我們所設(shè)置的位置偏移量沒有任何聯(lián)系荚虚。下面我來實(shí)現(xiàn)一個(gè)RecyclerView.ItemDecoration來加深下這里的理解:我將在垂直線性布局下薛夜,在ItemView間繪制一條5個(gè)像素寬、只有ItemView一半長版述、與ItemView居中對齊的紅色分割線梯澜,這條分割線在ItemView內(nèi)部top位置。

@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
  Paint paint = new Paint();
  paint.setColor(Color.RED);

  for (int i = 0; i < parent.getLayoutManager().getChildCount(); i++) {
    final View child = parent.getChildAt(i);

    float left = child.getLeft() + (child.getRight() - child.getLeft()) / 4;
    float top = child.getTop();
    float right = left + (child.getRight() - child.getLeft()) / 2;
    float bottom = top + 5;

    c.drawRect(left,top,right,bottom,paint);
  }
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
  outRect.set(0, 0, 0, 0);
}

代碼不是很嚴(yán)謹(jǐn)渴析,大家姑且一看吧晚伙,當(dāng)然這里getItemOffsets()方法可以省略的。   以上就是RecyclerView的整個(gè)繪制流程了檬某,值得注意的地方也就是在23.2.0中RecyclerView支持WRAP_CONTENT屬性了撬腾;還有就是ItemView的填充算法fill()算是一個(gè)亮點(diǎn)吧。接下來恢恼,我將分析ReyclerView的滑動流程。
滑動
  RecyclerView的滑動過程可以分為2個(gè)階段:手指在屏幕上移動胰默,使RecyclerView滑動的過程场斑,可以稱為scroll;手指離開屏幕牵署,RecyclerView繼續(xù)滑動一段距離的過程漏隐,可以稱為fling。現(xiàn)在先看看RecyclerView的觸屏事件處理onTouchEvent()方法:

public boolean onTouchEvent(MotionEvent e) {
    ...
    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    ...
    switch (action) {
        ...
        case MotionEvent.ACTION_MOVE: {
            ...
            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 (mScrollState != SCROLL_STATE_DRAGGING) {
                ...
                if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                    if (dy > 0) {
                        dy -= mTouchSlop;
                    } else {
                        dy += mTouchSlop;
                    }
                    startScroll = true;
                }
                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;
        ...
        case MotionEvent.ACTION_UP: {
            ...
            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;
        ...
    }
    ...
}

這里我以垂直方向的滑動來說明奴迅。當(dāng)RecyclerView接收到ACTION_MOVE事件后青责,會先計(jì)算出手指移動距離(dy)挺据,并與滑動閥值(mTouchSlop)比較,當(dāng)大于此閥值時(shí)將滑動狀態(tài)設(shè)置為SCROLL_STATE_DRAGGING脖隶,而后調(diào)用scrollByInternal()方法扁耐,使RecyclerView滑動,這樣RecyclerView的滑動的第一階段scroll就完成了产阱;當(dāng)接收到ACTION_UP事件時(shí)婉称,會根據(jù)之前的滑動距離與時(shí)間計(jì)算出一個(gè)初速度yvel,這步計(jì)算是由VelocityTracker實(shí)現(xiàn)的构蹬,然后再以此初速度王暗,調(diào)用方法fling(),完成RecyclerView滑動的第二階段fling庄敛。顯然滑動過程中關(guān)鍵的方法就2個(gè):scrollByInternal()與fling()俗壹。接下來同樣以垂直線性布局來說明。先來說明scrollByInternal()藻烤,跟蹤進(jìn)入后绷雏,會發(fā)現(xiàn)它最終會調(diào)用到LinearLayoutManager.scrollBy()方法,這個(gè)過程很簡單隐绵,我就不列出源碼了之众,但是分析到這里先暫停下,去看看fling()方法:

public boolean fling(int velocityX, int velocityY) {
    ...
    mViewFlinger.fling(velocityX, velocityY);
    ...
}

有用的就這一行依许,其它亂七八糟的不看也罷棺禾。mViewFlinger是一個(gè)Runnable的實(shí)現(xiàn)ViewFlinger的對象,就是它來控件著ReyclerView的fling過程的算法的峭跳。下面來看下類ViewFlinger的一段代碼:

void postOnAnimation() {
    if (mEatRunOnAnimationRequest) {
        mReSchedulePostAnimationCallback = true;
    } else {
        removeCallbacks(this);
        ViewCompat.postOnAnimation(RecyclerView.this, this);
    }
}

public void fling(int velocityX, int velocityY) {
    setScrollState(SCROLL_STATE_SETTLING);
    mLastFlingX = mLastFlingY = 0;
    mScroller.fling(0, 0, velocityX, velocityY,
            Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
    postOnAnimation();
}

可以看到膘婶,其實(shí)RecyclerView的fling是借助Scroller實(shí)現(xiàn)的;然后postOnAnimation()方法的作用就是在將來的某個(gè)時(shí)刻會執(zhí)行我們給定的一個(gè)Runnable對象蛀醉,在這里就是這個(gè)mViewFlinger對象悬襟,這部分原理我就不再深入分析了,它已經(jīng)不屬于本文的范圍了拯刁。并且脊岳,關(guān)于Scroller的作用及原理,本文也不會作過多解釋垛玻。對于這兩點(diǎn)各位可以自行查閱割捅,有很多文章對于作過詳細(xì)闡述的。接下來看看ViewFlinger.run()方法:

public void run() {
    ...
    if (scroller.computeScrollOffset()) {
        final int x = scroller.getCurrX();
        final int y = scroller.getCurrY();
        final int dx = x - mLastFlingX;
        final int dy = y - mLastFlingY;
        ...
        if (mAdapter != null) {
            ...
            if (dy != 0) {
                vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
                overscrollY = dy - vresult;
            }
            ...
        }
        ...
        if (!awakenScrollBars()) {
            invalidate();//刷新界面
        }
        ...
        if (scroller.isFinished() || !fullyConsumedAny) {
            setScrollState(SCROLL_STATE_IDLE);
        } else {
            postOnAnimation();
        }
    }
    ...
}

本段代碼中有個(gè)方法mLayout.scrollVerticallyBy()帚桩,跟蹤進(jìn)入你會發(fā)現(xiàn)它最終也會走到LinearLayoutManager.scrollBy()亿驾,這樣雖說RecyclerView的滑動可以分為兩階段,但是它們的實(shí)現(xiàn)最終其實(shí)是一樣的账嚎。這里我先解釋下上段代碼莫瞬。第一儡蔓,dy表示滑動偏移量若锁,它是由Scroller根據(jù)時(shí)間偏移量(Scroller.fling()開始時(shí)間到當(dāng)前時(shí)刻)計(jì)算出的料扰,當(dāng)然如果是RecyclerView的scroll階段,這個(gè)偏移量也就是手指滑動距離悍抑。第二檩小,上段代碼會多次執(zhí)行开呐,至到Scroller判斷滑動結(jié)束或已經(jīng)滑動到邊界。再多說一下规求,postOnAnimation()保證了RecyclerView的滑動是流暢筐付,這里涉及到著名的“android 16ms”機(jī)制,簡單來說理想狀態(tài)下阻肿,上段代碼會以16毫秒一次的速度執(zhí)行瓦戚,這樣其實(shí),Scroller每次計(jì)算的滑動偏移量是很小的一部分丛塌,而RecyclerView就會根據(jù)這個(gè)偏移量较解,確定是平移ItemView,還是除了平移還需要再創(chuàng)建新ItemView赴邻。


這里寫圖片描述

現(xiàn)在就來看看LinearLayoutManager.scrollBy()方法:

int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    final int absDy = Math.abs(dy);
    updateLayoutState(layoutDirection, absDy, true, state);
    final int consumed = mLayoutState.mScrollingOffset
            + fill(recycler, mLayoutState, state, false);
    ...
    final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
    mOrientationHelper.offsetChildren(-scrolled);
    ...
}

如上文所講到的fill()方法印衔,作用就是向可繪制區(qū)間填充ItemView,那么在這里姥敛,可繪制區(qū)間就是滑動偏移量奸焙!再看方法mOrientationHelper.offsetChildren()作用就是平移ItemView。好了整個(gè)滑動過程就分析完成了彤敛,當(dāng)然RecyclerView的滑動還有個(gè)特性叫平滑滑動(smooth scroll)与帆,其實(shí)它的實(shí)現(xiàn)就是一個(gè)fling滑動,所以就不再贅述了墨榄。
Recycler
  Recycler的作用就是重用ItemView玄糟。在填充ItemView的時(shí)候,ItemView是從它獲取的袄秩;滑出屏幕的ItemView是由它回收的阵翎。對于不同狀態(tài)的ItemView存儲在了不同的集合中,比如有scrapped之剧、cached贮喧、exCached、recycled猪狈,當(dāng)然這些集合并不是都定義在同一個(gè)類里。   回到之前的layoutChunk方法中辩恼,有行代碼layoutState.next(recycler)雇庙,它的作用自然就是獲取ItemView谓形,我們進(jìn)入這個(gè)方法查看,最終它會調(diào)用到RecyclerView.Recycler.getViewForPosition()方法:

View getViewForPosition(int position, boolean dryRun) {
    ...
    // 0) If there is a changed scrap, try to find from there
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrap = holder != null;
    }
    // 1) Find from scrap by position
    if (holder == null) {
        holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
        ...
    }
    if (holder == null) {
        ...
        // 2) Find from scrap via stable ids, if exists
        if (mAdapter.hasStableIds()) {
            holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
            ...
        }
        if (holder == null && mViewCacheExtension != null) {
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
                ...
            }
        }
        if (holder == null) {
            ...
            holder = getRecycledViewPool().getRecycledView(type);
            ...
        }
        if (holder == null) {
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            ...
        }
    }
    ...
    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        // do not update unless we absolutely have to.
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        ...
        mAdapter.bindViewHolder(holder, offsetPosition);
        ...
    }
    ...
}

這個(gè)方法比較長疆前,我先解釋下它的邏輯吧寒跳。根據(jù)列表位置獲取ItemView,先后從scrapped竹椒、cached童太、exCached、recycled集合中查找相應(yīng)的ItemView胸完,如果沒有找到书释,就創(chuàng)建(Adapter.createViewHolder()),最后與數(shù)據(jù)集綁定赊窥。其中scrapped爆惧、cached和exCached集合定義在RecyclerView.Recycler中,分別表示將要在RecyclerView中刪除的ItemView锨能、一級緩存ItemView和二級緩存ItemView扯再,cached集合的大小默認(rèn)為2,exCached是需要我們通過RecyclerView.ViewCacheExtension自己實(shí)現(xiàn)的址遇,默認(rèn)沒有熄阻;recycled集合其實(shí)是一個(gè)Map,定義在RecyclerView.RecycledViewPool中倔约,將ItemView以ItemType分類保存了下來秃殉,這里算是RecyclerView設(shè)計(jì)上的亮點(diǎn),通過RecyclerView.RecycledViewPool可以實(shí)現(xiàn)在不同的RecyclerView之間共享ItemView跺株,只要為這些不同RecyclerView設(shè)置同一個(gè)RecyclerView.RecycledViewPool就可以了复濒。 上面解釋了ItemView從不同集合中獲取的方式,那么RecyclerView又是在什么時(shí)候向這些集合中添加ItemView的呢乒省?下面我逐個(gè)介紹下巧颈。 scrapped集合中存儲的其實(shí)是正在執(zhí)行REMOVE操作的ItemView,這部分會在后文進(jìn)一步描述袖扛。 在fill()方法的循環(huán)體中有行代碼recycleByLayoutState(recycler, layoutState);砸泛,最終這個(gè)方法會執(zhí)行到RecyclerView.Recycler.recycleViewHolderInternal()方法:

void recycleViewHolderInternal(ViewHolder holder) {
        ...
        if (forceRecycle || holder.isRecyclable()) {
            if (!holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED
                    | ViewHolder.FLAG_UPDATE)) {
                // Retire oldest cached view
                final int cachedViewSize = mCachedViews.size();
                if (cachedViewSize == mViewCacheMax && cachedViewSize > 0) {
                    recycleCachedViewAt(0);
                }
                if (cachedViewSize < mViewCacheMax) {
                    mCachedViews.add(holder);
                    cached = true;
                }
            }
            if (!cached) {
                addViewHolderToRecycledViewPool(holder);
                recycled = true;
            }
        }
        ...
    }

這個(gè)方法的邏輯是這樣的:首先判斷集合cached是否満了,如果已満就從cached集合中移出一個(gè)到recycled集合中去蛆封,再把新的ItemView添加到cached集合唇礁;如果不満就將ItemView直接添加到cached集合。 最后exCached集合是我們自己創(chuàng)建的惨篱,所以添加刪除元素也要我們自己實(shí)現(xiàn)盏筐。
數(shù)據(jù)集、動畫
  RecyclerView定義了4種針對數(shù)據(jù)集的操作砸讳,分別是ADD琢融、REMOVE界牡、UPDATE、MOVE漾抬,封裝在了AdapterHelper.UpdateOp類中宿亡,并且所有操作由一個(gè)大小為30的對象池管理著。當(dāng)我們要對數(shù)據(jù)集作任何操作時(shí)纳令,都會從這個(gè)對象池中取出一個(gè)UpdateOp對象挽荠,放入一個(gè)等待隊(duì)列中,最后調(diào)用RecyclerView.RecyclerViewDataObserver.triggerUpdateProcessor()方法平绩,根據(jù)這個(gè)等待隊(duì)列中的信息圈匆,對所有子控件重新測量、布局并繪制且執(zhí)行動畫馒过。以上就是我們調(diào)用Adapter.notifyItemXXX()系列方法后發(fā)生的事臭脓。 顯然當(dāng)我們對某個(gè)ItemView做操作時(shí),它很有可以會影響到其它ItemView腹忽。下面我以REMOVE為例來梳理下這個(gè)流程来累。   

這里寫圖片描述

首先調(diào)用Adapter.notifyItemRemove(),追溯到方法RecyclerView.RecyclerViewDataObserver.onItemRangeRemoved():

public void onItemRangeRemoved(int positionStart, int itemCount) {
    assertNotInLayoutOrScroll(null);
    if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
        triggerUpdateProcessor();
    }
}

這里的mAdapterHelper.onItemRangeRemoved()就是向之前提及的等待隊(duì)列添加一個(gè)類型為REMOVE的UpdateOp對象窘奏, triggerUpdateProcessor()方法就是調(diào)用View.requestLayout()方法嘹锁,這會導(dǎo)致界面重新布局,也就是說方法RecyclerView.onLayout()會隨后調(diào)用着裹,這之后的流程就和在繪制流程一節(jié)中所描述的一致了领猾。但是動畫在哪是執(zhí)行的呢?查看之前所列出的onLayout()方法發(fā)現(xiàn)dispatchLayoutStepX方法共有3個(gè)骇扇,前文只解釋了dispatchLayoutStep2()的作用摔竿,這里就其它2個(gè)方法作進(jìn)一步說明。不過dispatchLayoutStep1()沒有過多要說明的東西少孝,它的作用只是初始化數(shù)據(jù)继低,需要詳細(xì)說明的是dispatchLayoutStep3()方法:

private void dispatchLayoutStep3() {
    ...
    if (mState.mRunSimpleAnimations) {
        // Step 3: Find out where things are now, and process change animations.
        ...
        // Step 4: Process view info lists and trigger animations
        mViewInfoStore.process(mViewInfoProcessCallback);
    }
    ...
}

代碼注釋已經(jīng)說明得很清楚了,這里我沒有列出step 3相關(guān)的代碼是因?yàn)檫@部分只是初始化或賦值一些執(zhí)行動畫需要的中間數(shù)據(jù)稍走,process()方法最終會執(zhí)行到RecyclerView.animateDisappearance()方法:

private void animateDisappearance(...) {
    addAnimatingView(holder);
    holder.setIsRecyclable(false);
    if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
        postAnimationRunner();
    }
}

這里的animateDisappearance()會把一個(gè)動畫與ItemView綁定袁翁,并添加到待執(zhí)行隊(duì)列中, postAnimationRunner()調(diào)用后就會執(zhí)行這個(gè)隊(duì)列中的動畫婿脸,注意方法addAnimatingView():

private void addAnimatingView(ViewHolder viewHolder) {
    final View view = viewHolder.itemView;
    ...
    mChildHelper.addView(view, true);
    ...
}

這里最終會向ChildHelper中的一個(gè)名為mHiddenViews的集合添加給定的ItemView粱胜,那么這個(gè)mHiddenViews又是什么東西?上節(jié)中的getViewForPosition()方法中有個(gè)getScrapViewForPosition()狐树,作用是從scrapped集合中獲取ItemView:

ViewHolder getScrapViewForPosition(int position, int type, boolean dryRun) {
    ...
    View view = mChildHelper.findHiddenNonRemovedView(position, type);
    ...
}

接下來是findHiddenNonRemovedView()方法:

View findHiddenNonRemovedView(int position, int type) {
    final int count = mHiddenViews.size();
    for (int i = 0; i < count; i++) {
        final View view = mHiddenViews.get(i);
        RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
        if (holder.getLayoutPosition() == position && !holder.isInvalid() && !holder.isRemoved()
                && (type == RecyclerView.INVALID_TYPE || holder.getItemViewType() == type)) {
            return view;
        }
    }
    return null;
}

Oops焙压!看到這里就我之前所講的scrapped集合聯(lián)系起來了,雖然繞了個(gè)圈。所以這里就論證我之前對于scrapped集合的理解冗恨。   文章到這里也快結(jié)束了答憔,最后關(guān)于動畫,本節(jié)提到的對數(shù)據(jù)集的4種操作掀抹,在DefalutItemAnimator中給出了對應(yīng)的默認(rèn)實(shí)現(xiàn),就是改變透明度心俗,實(shí)現(xiàn)淡入淡出效果傲武。如果要自定義ItemView的動畫可以參考這里的實(shí)現(xiàn)來做。好了城榛,以上就是我對于RecyclerView的全部剖析了揪利,也許還有我沒有提及的方面,或是我講錯(cuò)的地方狠持,歡迎指正疟位。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市喘垂,隨后出現(xiàn)的幾起案子甜刻,更是在濱河造成了極大的恐慌,老刑警劉巖正勒,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件得院,死亡現(xiàn)場離奇詭異,居然都是意外死亡章贞,警方通過查閱死者的電腦和手機(jī)祥绞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鸭限,“玉大人蜕径,你說我怎么就攤上這事“芫” “怎么了兜喻?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長喧枷。 經(jīng)常有香客問我虹统,道長,這世上最難降的妖魔是什么隧甚? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任车荔,我火速辦了婚禮,結(jié)果婚禮上戚扳,老公的妹妹穿的比我還像新娘忧便。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布珠增。 她就那樣靜靜地躺著超歌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蒂教。 梳的紋絲不亂的頭發(fā)上巍举,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機(jī)與錄音凝垛,去河邊找鬼懊悯。 笑死,一個(gè)胖子當(dāng)著我的面吹牛梦皮,可吹牛的內(nèi)容都是我干的炭分。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼剑肯,長吁一口氣:“原來是場噩夢啊……” “哼捧毛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起让网,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤呀忧,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后寂祥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體荐虐,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年丸凭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了福扬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡惜犀,死狀恐怖铛碑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情虽界,我是刑警寧澤汽烦,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站莉御,受9級特大地震影響撇吞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜礁叔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一牍颈、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧琅关,春花似錦煮岁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽冶伞。三九已至,卻和暖如春步氏,著一層夾襖步出監(jiān)牢的瞬間响禽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工戳护, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留金抡,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓腌且,卻偏偏與公主長得像,于是被迫代替她去往敵國和親榛瓮。 傳聞我的和親對象是個(gè)殘疾皇子铺董,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,162評論 25 707
  • 這篇文章分三個(gè)部分,簡單跟大家講一下 RecyclerView 的常用方法與奇葩用法禀晓;工作原理與ListView比...
    LucasAdam閱讀 4,391評論 0 27
  • 多思者必心累精续,心重者必心苦。
    FezDirk閱讀 229評論 0 0
  • 耶利瓦勒(G?llivare)是瑞典北博滕省耶利瓦勒市的一個(gè)北部城鎮(zhèn)粹懒,在北極圈以北100公里之外重付。城鎮(zhèn)始建于17世...
    慕溪北歐旅游閱讀 1,183評論 0 0
  • 元宵節(jié),出來一轉(zhuǎn)凫乖,可能是大學(xué)里面唯一一次了确垫。 里面的燈會都是收錢的,沒進(jìn)去 有月亮 這次帶了攝影師帽芽,難得拍了一張比...
    霓衣風(fēng)馬閱讀 220評論 0 2