基本使用
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ò)的地方,歡迎指正。