Android RecyclerView工作原理分析(上)

基本使用
  RecyclerView的基本使用并不復(fù)雜,只需要提供一個(gè)RecyclerView.Apdater的實(shí)現(xiàn)用于處理數(shù)據(jù)集與ItemView的綁定關(guān)系,和一個(gè)RecyclerView.LayoutManager的實(shí)現(xiàn)用于 測(cè)量并布局 ItemView。
繪制流程
  眾所周知,Android控件的繪制可以分為3個(gè)步驟:measure、layout、draw。RecyclerView的繪制自然也經(jīng)這3個(gè)步驟。但是,RecyclerView將它的measure與layout過(guò)程委托給了RecyclerView.LayoutManager來(lái)處理,并且,它對(duì)子控件的measure及l(fā)ayout過(guò)程是逐個(gè)處理的,也就是說(shuō),執(zhí)行完成一個(gè)子控件的measure及l(fā)ayout過(guò)程再去執(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的測(cè)量方法,再看下dispatchLayoutStep2()方法:

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

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

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

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

這里寫圖片描述

在上一段代碼中,fill()方法的作用就是填充ItemView,而圖(3)說(shuō)明了,在上段代碼中fill()方法調(diào)用2次的原因。雖然圖(3)是更為普遍的情況,而且在實(shí)現(xiàn)填充ItemView算法時(shí),也是按圖(3)所示來(lái)實(shí)現(xiàn)的,但是mAnchorInfo在賦值過(guò)程(updateAnchorInfoForLayout)中,只會(huì)出現(xiàn)圖(1)、圖(2)所示情況。現(xiàn)在來(lái)看下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()方法看名字就知道是用于測(cè)量子控件大小的,這里我先跳過(guò)這個(gè)方法的解釋,放在后面來(lái)做,目前就簡(jiǎn)單地理解為測(cè)量子控件大小就好了。下面是layoutDecoreated()方法:

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

總結(jié)上面代碼,在RecyclerView的measure及l(fā)ayout階段,填充ItemView的算法為:向父容器增加子控件,測(cè)量子控件大小,布局子控件,布局錨點(diǎn)向當(dāng)前布局方向平移子控件大小,重復(fù)上訴步驟至RecyclerView可繪制空間消耗完畢或子控件已全部填充。   這樣所有的子控件的measure及l(fā)ayout過(guò)程就完成了。回到RecyclerView的onMeasure方法,執(zhí)行mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec)這行代碼的作用就是根據(jù)子控件的大小,設(shè)置RecyclerView的大小。至此,RecyclerView的measure和layout實(shí)際上已經(jīng)完成了。   但是,你有可能已經(jīng)發(fā)現(xiàn)上面過(guò)程中的問(wèn)題了:如何確定RecyclerView的可繪制空間?不過(guò),如果你熟悉android控件的繪制機(jī)制的話,這就不是問(wèn)題。其實(shí),這里的可繪制空間,可以簡(jiǎn)單地理解為父容器的大小;更準(zhǔn)確的描述是,父容器對(duì)RecyclerView的布局大小的要求,可以通過(guò)MeasureSpec.getSize()方法獲得——這里不包括滑動(dòng)情況,滑動(dòng)情況會(huì)在后文描述。需要特別說(shuō)明的是在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();
    ...
}

可以看出,這里也會(huì)執(zhí)行子控件的measure及l(fā)ayout過(guò)程。結(jié)合onMeasure方法對(duì)skipMeasure的判斷可以看出,如果要支持WRAP_CONTENT,那么子控件的measure及l(fā)ayout就會(huì)提前在RecyclerView的測(cè)量方法中執(zhí)行完成,也就是說(shuō),先確定了子控件的大小及位置后,再由此設(shè)置RecyclerView的大小;如果是其它情況(測(cè)量模式為EXACTLY),子控件的measure及l(fā)ayout過(guò)程就會(huì)延遲至RecyclerView的layout過(guò)程(RecyclerView.onLayout())中執(zhí)行。再看onMeasure方法中的mLayout.mAutoMeasure,它表示,RecyclerView的measure及l(fā)ayout過(guò)程是否要委托給RecyclerView.LayoutManager,在兼容包中提供的3種RecyclerView.LayoutManager的這個(gè)屬性默認(rèn)都是為true的。好了,以上就是RecyclerView的measure及l(fā)ayout過(guò)程,下面來(lái)看下它的draw過(guò)程。   RecyclerView的draw過(guò)程可以分為2部分來(lái)看:RecyclerView負(fù)責(zé)繪制所有decoration;ItemView的繪制由ViewGroup處理,這里的繪制是android常規(guī)繪制邏輯,本文就不再闡述了。下面來(lái)看看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);
    }
}

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

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()就是我們?cè)趯?shí)現(xiàn)一個(gè)RecyclerView.ItemDecoration時(shí)可以重寫的方法,通過(guò)mTempRect的大小,可以為每個(gè)ItemView設(shè)置位置偏移量,這個(gè)偏移量最終會(huì)參與計(jì)算ItemView的大小,也就是說(shuō)ItemView的大小是包含這個(gè)位置偏移量的。我們?cè)谥貙慻etItemOffsets()時(shí),可以指定任意數(shù)值的偏移量:


這里寫圖片描述

4個(gè)方向的位置偏移量對(duì)應(yīng)mTempRect的4個(gè)屬性(left,top,right,bottom),我以top offset的值在垂直線性布局中的應(yīng)用來(lái)舉例說(shuō)明下。如果top offset等于0,那么ItemView之間就沒(méi)有空隙;如果top offset大于0,那么ItemView之前就會(huì)有一個(gè)間隙;如果top offset小于0,那么ItemView之間就會(huì)有重疊的區(qū)域。   當(dāng)然,我們?cè)趯?shí)現(xiàn)RecyclerView.ItemDecoration時(shí),并不一定要重寫getItemOffsets(),同樣的對(duì)于RecyclerView.ItemDecoration.onDraw()或RecyclerView.ItemDecoration.onDrawOver()方法也不是一定要重寫,而且,這個(gè)繪制方法和我們所設(shè)置的位置偏移量沒(méi)有任何聯(lián)系。下面我來(lái)實(shí)現(xiàn)一個(gè)RecyclerView.ItemDecoration來(lái)加深下這里的理解:我將在垂直線性布局下,在ItemView間繪制一條5個(gè)像素寬、只有ItemView一半長(zhǎng)、與ItemView居中對(duì)齊的紅色分割線,這條分割線在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)吧。接下來(lái),我將分析ReyclerView的滑動(dòng)流程。
滑動(dòng)
  RecyclerView的滑動(dòng)過(guò)程可以分為2個(gè)階段:手指在屏幕上移動(dòng),使RecyclerView滑動(dòng)的過(guò)程,可以稱為scroll;手指離開(kāi)屏幕,RecyclerView繼續(xù)滑動(dòng)一段距離的過(guò)程,可以稱為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)來(lái)說(shuō)明。當(dāng)RecyclerView接收到ACTION_MOVE事件后,會(huì)先計(jì)算出手指移動(dòng)距離(dy),并與滑動(dòng)閥值(mTouchSlop)比較,當(dāng)大于此閥值時(shí)將滑動(dòng)狀態(tài)設(shè)置為SCROLL_STATE_DRAGGING,而后調(diào)用scrollByInternal()方法,使RecyclerView滑動(dòng),這樣RecyclerView的滑動(dòng)的第一階段scroll就完成了;當(dāng)接收到ACTION_UP事件時(shí),會(huì)根據(jù)之前的滑動(dòng)距離與時(shí)間計(jì)算出一個(gè)初速度yvel,這步計(jì)算是由VelocityTracker實(shí)現(xiàn)的,然后再以此初速度,調(diào)用方法fling(),完成RecyclerView滑動(dòng)的第二階段fling。顯然滑動(dòng)過(guò)程中關(guān)鍵的方法就2個(gè):scrollByInternal()與fling()。接下來(lái)同樣以垂直線性布局來(lái)說(shuō)明。先來(lái)說(shuō)明scrollByInternal(),跟蹤進(jìn)入后,會(huì)發(fā)現(xiàn)它最終會(huì)調(diào)用到LinearLayoutManager.scrollBy()方法,這個(gè)過(guò)程很簡(jiǎn)單,我就不列出源碼了,但是分析到這里先暫停下,去看看fling()方法:

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

有用的就這一行,其它亂七八糟的不看也罷。mViewFlinger是一個(gè)Runnable的實(shí)現(xiàn)ViewFlinger的對(duì)象,就是它來(lái)控件著ReyclerView的fling過(guò)程的算法的。下面來(lái)看下類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()方法的作用就是在將來(lái)的某個(gè)時(shí)刻會(huì)執(zhí)行我們給定的一個(gè)Runnable對(duì)象,在這里就是這個(gè)mViewFlinger對(duì)象,這部分原理我就不再深入分析了,它已經(jīng)不屬于本文的范圍了。并且,關(guān)于Scroller的作用及原理,本文也不會(huì)作過(guò)多解釋。對(duì)于這兩點(diǎn)各位可以自行查閱,有很多文章對(duì)于作過(guò)詳細(xì)闡述的。接下來(lái)看看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)入你會(huì)發(fā)現(xiàn)它最終也會(huì)走到LinearLayoutManager.scrollBy(),這樣雖說(shuō)RecyclerView的滑動(dòng)可以分為兩階段,但是它們的實(shí)現(xiàn)最終其實(shí)是一樣的。這里我先解釋下上段代碼。第一,dy表示滑動(dòng)偏移量,它是由Scroller根據(jù)時(shí)間偏移量(Scroller.fling()開(kāi)始時(shí)間到當(dāng)前時(shí)刻)計(jì)算出的,當(dāng)然如果是RecyclerView的scroll階段,這個(gè)偏移量也就是手指滑動(dòng)距離。第二,上段代碼會(huì)多次執(zhí)行,至到Scroller判斷滑動(dòng)結(jié)束或已經(jīng)滑動(dòng)到邊界。再多說(shuō)一下,postOnAnimation()保證了RecyclerView的滑動(dòng)是流暢,這里涉及到著名的“android 16ms”機(jī)制,簡(jiǎn)單來(lái)說(shuō)理想狀態(tài)下,上段代碼會(huì)以16毫秒一次的速度執(zhí)行,這樣其實(shí),Scroller每次計(jì)算的滑動(dòng)偏移量是很小的一部分,而RecyclerView就會(huì)根據(jù)這個(gè)偏移量,確定是平移ItemView,還是除了平移還需要再創(chuàng)建新ItemView。


這里寫圖片描述

現(xiàn)在就來(lái)看看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ū)間就是滑動(dòng)偏移量!再看方法mOrientationHelper.offsetChildren()作用就是平移ItemView。好了整個(gè)滑動(dòng)過(guò)程就分析完成了,當(dāng)然RecyclerView的滑動(dòng)還有個(gè)特性叫平滑滑動(dòng)(smooth scroll),其實(shí)它的實(shí)現(xiàn)就是一個(gè)fling滑動(dòng),所以就不再贅述了。
Recycler
  Recycler的作用就是重用ItemView。在填充ItemView的時(shí)候,ItemView是從它獲取的;滑出屏幕的ItemView是由它回收的。對(duì)于不同狀態(tài)的ItemView存儲(chǔ)在了不同的集合中,比如有scrapped、cached、exCached、recycled,當(dāng)然這些集合并不是都定義在同一個(gè)類里。   回到之前的layoutChunk方法中,有行代碼layoutState.next(recycler),它的作用自然就是獲取ItemView,我們進(jìn)入這個(gè)方法查看,最終它會(huì)調(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è)方法比較長(zhǎng),我先解釋下它的邏輯吧。根據(jù)列表位置獲取ItemView,先后從scrapped、cached、exCached、recycled集合中查找相應(yīng)的ItemView,如果沒(méi)有找到,就創(chuàng)建(Adapter.createViewHolder()),最后與數(shù)據(jù)集綁定。其中scrapped、cached和exCached集合定義在RecyclerView.Recycler中,分別表示將要在RecyclerView中刪除的ItemView、一級(jí)緩存ItemView和二級(jí)緩存ItemView,cached集合的大小默認(rèn)為2,exCached是需要我們通過(guò)RecyclerView.ViewCacheExtension自己實(shí)現(xiàn)的,默認(rèn)沒(méi)有;recycled集合其實(shí)是一個(gè)Map,定義在RecyclerView.RecycledViewPool中,將ItemView以ItemType分類保存了下來(lái),這里算是RecyclerView設(shè)計(jì)上的亮點(diǎn),通過(guò)RecyclerView.RecycledViewPool可以實(shí)現(xiàn)在不同的RecyclerView之間共享ItemView,只要為這些不同RecyclerView設(shè)置同一個(gè)RecyclerView.RecycledViewPool就可以了。 上面解釋了ItemView從不同集合中獲取的方式,那么RecyclerView又是在什么時(shí)候向這些集合中添加ItemView的呢?下面我逐個(gè)介紹下。 scrapped集合中存儲(chǔ)的其實(shí)是正在執(zhí)行REMOVE操作的ItemView,這部分會(huì)在后文進(jìn)一步描述。 在fill()方法的循環(huán)體中有行代碼recycleByLayoutState(recycler, layoutState);,最終這個(gè)方法會(huì)執(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ù)集、動(dòng)畫
  RecyclerView定義了4種針對(duì)數(shù)據(jù)集的操作,分別是ADD、REMOVE、UPDATE、MOVE,封裝在了AdapterHelper.UpdateOp類中,并且所有操作由一個(gè)大小為30的對(duì)象池管理著。當(dāng)我們要對(duì)數(shù)據(jù)集作任何操作時(shí),都會(huì)從這個(gè)對(duì)象池中取出一個(gè)UpdateOp對(duì)象,放入一個(gè)等待隊(duì)列中,最后調(diào)用RecyclerView.RecyclerViewDataObserver.triggerUpdateProcessor()方法,根據(jù)這個(gè)等待隊(duì)列中的信息,對(duì)所有子控件重新測(cè)量、布局并繪制且執(zhí)行動(dòng)畫。以上就是我們調(diào)用Adapter.notifyItemXXX()系列方法后發(fā)生的事。 顯然當(dāng)我們對(duì)某個(gè)ItemView做操作時(shí),它很有可以會(huì)影響到其它ItemView。下面我以REMOVE為例來(lái)梳理下這個(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對(duì)象, triggerUpdateProcessor()方法就是調(diào)用View.requestLayout()方法,這會(huì)導(dǎo)致界面重新布局,也就是說(shuō)方法RecyclerView.onLayout()會(huì)隨后調(diào)用,這之后的流程就和在繪制流程一節(jié)中所描述的一致了。但是動(dòng)畫在哪是執(zhí)行的呢?查看之前所列出的onLayout()方法發(fā)現(xiàn)dispatchLayoutStepX方法共有3個(gè),前文只解釋了dispatchLayoutStep2()的作用,這里就其它2個(gè)方法作進(jìn)一步說(shuō)明。不過(guò)dispatchLayoutStep1()沒(méi)有過(guò)多要說(shuō)明的東西,它的作用只是初始化數(shù)據(jù),需要詳細(xì)說(shuō)明的是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)說(shuō)明得很清楚了,這里我沒(méi)有列出step 3相關(guān)的代碼是因?yàn)檫@部分只是初始化或賦值一些執(zhí)行動(dòng)畫需要的中間數(shù)據(jù),process()方法最終會(huì)執(zhí)行到RecyclerView.animateDisappearance()方法:

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

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

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

這里最終會(huì)向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);
    ...
}

接下來(lái)是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)系起來(lái)了,雖然繞了個(gè)圈。所以這里就論證我之前對(duì)于scrapped集合的理解。   文章到這里也快結(jié)束了,最后關(guān)于動(dòng)畫,本節(jié)提到的對(duì)數(shù)據(jù)集的4種操作,在DefalutItemAnimator中給出了對(duì)應(yīng)的默認(rèn)實(shí)現(xiàn),就是改變透明度,實(shí)現(xiàn)淡入淡出效果。如果要自定義ItemView的動(dòng)畫可以參考這里的實(shí)現(xiàn)來(lái)做。好了,以上就是我對(duì)于RecyclerView的全部剖析了,也許還有我沒(méi)有提及的方面,或是我講錯(cuò)的地方,歡迎指正。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,687評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 178,640評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 63,957評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,682評(píng)論 6 413
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 56,011評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評(píng)論 3 449
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 43,183評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,714評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,435評(píng)論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,665評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評(píng)論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,838評(píng)論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 35,251評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 36,588評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,379評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,627評(píng)論 2 380

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

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