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