抽絲剝繭RecyclerView - LayoutManager

前言

抽絲剝繭RecyclerView系列文章的目的在於幫助Android開發者提升對RecyclerView的認知,本文是整個系列的第二篇。git

前文回顧github

第一篇:《抽絲剝繭RecyclerView - 化整爲零》緩存

LayoutManagerRecyclerView中的重要一環,使用LayoutManager就跟玩捏臉蛋的遊戲同樣,即便好看的五官(好看的子View)都具有了,也不必定能捏出漂亮的臉蛋,好在RecyclerView爲咱們提供了默認的模板:LinearLayoutManagerGridLayoutManagerStaggeredGridLayoutManagerbash

說來慚愧,若是不是看了GridLayoutManager的源碼,我還真不知道GridLayoutManager居然能夠這麼使用,圖片來自網絡: 網絡

GridLayoutManager

不過呢,今天咱們講解的源碼不是來自GridLayoutManager,而是線性佈局LinearLayoutManagerGridLayoutManager也是繼承自LinearLayoutManager),分析完源碼,我還將給你們帶來實戰,完成如下的效果: app

TwoSideLayoutManager
時間軸的效果來自 TimeLine,本身稍微處理了一下,如今開始進入正題。

代碼地址github.com/mCyp/Orient…ide

目錄

目錄

1、源碼分析

本着認真負責的精神,我把RecyclerView中用到LayoutManager的地方大體看了一遍,發現其負責的主要業務:源碼分析

  • 回收和複用子View(固然,這會交給Recyler處理)。
  • 測量和佈局子View。
  • 關於滑動的處理。

回收和複用子View顯然不是LayoutManager實際完成的,不過,子View的新增和刪除都是LayoutManager通知的,除此之外,滑動處理的本質仍是對子View進行管理,因此,本文要討論的只有測量和佈局子View的。佈局

測量和佈局子View發生在RecyclerView三大工做流程,又...又回到了最初的起點?這是咱們在上篇討論過的,若是不涉及到LayoutManager的知識,咱們將一筆帶過便可。post

1. 自動測量機制

RecyclerView#onMeasure方法中,LayoutManager是否支持自動測量會走不一樣的流程:

protected void onMeasure(int widthSpec, int heightSpec) {
	// ...
	if (mLayout.isAutoMeasureEnabled()) {
		final int widthMode = MeasureSpec.getMode(widthSpec);
		final int heightMode = MeasureSpec.getMode(heightSpec);
		// 未複寫的狀況下默認調用RecyclerView#defaultOnMeasure方法
		mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
		final Boolean measureSpecModeIsExactly =
		                    widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
      	        // 長和寬的MeasureSpec都爲EXACTLY的狀況下會return
		if (measureSpecModeIsExactly || mAdapter == null) {
			return;
		}
		if (mState.mLayoutStep == State.STEP_START) {
			dispatchLayoutStep1();
		}
		// 1. 計算寬度和長度等
		mLayout.setMeasureSpecs(widthSpec, heightSpec);
		mState.mIsMeasuring = true;
      	        // 2. 佈局子View
		dispatchLayoutStep2();
		// 3. 測量子View的寬和高,並再次測量父佈局
		mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
		if (mLayout.shouldMeasureTwice()) {
		// 再走一遍1,2,3
		}
	} else {
		// ...
		mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
		// ....
	}
}
複製代碼

從代碼上來看,使用自動測量機制須要具有:

  1. RecyclerView佈局的長和寬的SpecMode不能是MeasureSpec.EXACTLY(大機率指的是佈局中RecyclerView長或寬中有WrapContent)。
  2. RecyclerView設置的LayoutMangerisAutoMeasureEnabled返回爲true

當設置自動測量機制的時候,咱們的流程以下:

自動測量機制
從上圖能夠看出,是否使用自動測量機制帶來的差距仍是挺明顯的,使用自動測量機制須要經歷那麼多流程,反正都要使用 LayoutManager#onMeasure方法,還不如不使用測量機制呢!

顯然,這種想法是不對的,由於官方是這麼說的,若是不使用自動測量機制,須要在自定義LayoutManager過程當中複寫LayoutManager#onMeasure方法,因此呢,這個方法應該是包括自動測量機制的所有過程,包括:測量父佈局-佈置子View-從新測量子View-從新測量父佈局,而使用自動測量機制是不須要複寫這個方法的,該方法默認測量父佈局。

須要說起的是,咱們平時使用的三大LayoutManager都開啓了自動測量機制。

2. onLayoutChildren

即便RecyclerViewonMeasure方法中逃過了佈局子View,那麼在onLayout中也不可避免,在上一篇博客中,咱們瞭解到RecyclerView經過LayoutManager#onLayoutChildren方法實現給子View佈局,咱們以LinearLayoutManager爲例,看看其中的奧祕。

在正式開始以前,咱們先看看LinearLayoutManager中幾個重要的類:

重要的類 解釋
LinearLayoutManager 這個你們都懂,線性佈局。
AnchorInfo 繪製子View的時候,記錄其位置、偏移量、方向等基礎信息。
LayoutChunkResult 加載子View結果狀況的記錄,好比已經填充的子View的數量。
LayoutState 當前加載的狀態記錄,好比當前繪製的偏移量,屏幕還剩餘多少空間等

直接看最重要的LinearLayoutManager#onLayoutChildren,代碼被我一刪再刪後以下:

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
	//... 省略的代碼爲:數據爲0的狀況下移除全部的子View,將子View加入到緩存
	// 第一步:初始化LayoutState 配置LayoutState參數
	ensureLayoutState();
	mLayoutState.mRecycle = false;
	// ... 
	// 第二步:尋找焦點子View
	final View focused = getFocusedChild();
	// ...
	// 第三步:移除界面中已經存在的子View,並放入緩存
	detachAndScrapAttachedViews(recycler);
	if (mAnchorInfo.mLayoutFromEnd) {
		// ...
	} else {
	// 第四步:更新LayoutSatete,填充子View
      	// 填充也分爲兩步:1.從錨點處向結束方向填充 2.從錨點處向開始方向填充
      
		// fill towards end 往結束方向填充子View
		// 更新LayoutState
		updateLayoutStateToFillEnd(mAnchorInfo);
		fill(recycler, mLayoutState, state, false);
		//...
		// fill towards start 往開始方向填充子View
		// 更新LayoutState等信息
		updateLayoutStateToFillStart(mAnchorInfo);
		fill(recycler, mLayoutState, state, false);
		if (mLayoutState.mAvailable > 0) {
			// 若是還有剩餘空間
			updateLayoutStateToFillEnd(lastElement, endOffset);
			fill(recycler, mLayoutState, state, false);
			// ...
		}
	}
	// ...
	// 第五步:整理一些參數,以及作一下結束處理
  	// 不是預佈局的狀態下結束給子View佈局,不然,重置錨點信息
	if (!state.isPreLayout()) {
		mOrientationHelper.onLayoutComplete();
	} else {
		mAnchorInfo.reset();
	}
	//...
}
複製代碼

整個onLayoutChildren能夠分爲以下五個過程:

  • 第一步:建立LayoutState
  • 第二步:獲取焦點子View
  • 第三步:移除視圖中已經存在的View,回收ViewHolder
  • 第四步:填充子View
  • 第五步:填充結束後的處理
2.1 第一步、第二步

第一步是建立LayoutState,第二步是獲取屏幕中的焦點子View,代碼比較簡單,感興趣的同窗們能夠本身查詢。

2.2 第三步

在填充子View前,若是當前已經存在子View並將繼續存在的時候,會先從屏幕中暫時移除,將ViewHolder暫存在Recycler的一級緩存mAttachedScrap中:

/**
 * Temporarily detach and scrap all currently attached child views. Views will be scrapped
 * into the given Recycler. The Recycler may prefer to reuse scrap views before
 * other views that were previously recycled.
 *
 * @param recycler Recycler to scrap views into
 */
public void detachAndScrapAttachedViews(Recycler recycler) {
	final int childCount = getChildCount();
	for (int i = childCount - 1; i >= 0; i--) {
		final View v = getChildAt(i);
		scrapOrRecycleView(recycler, i, v);
	}
}

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
	final ViewHolder viewHolder = getChildViewHolderint(view);
	if (viewHolder.shouldIgnore()) {
		return;
	}
	if (viewHolder.isInvalid() && !viewHolder.isRemoved()
	                    && !mRecyclerView.mAdapter.hasStableIds()) {
      	// 無效的ViewHolder會被添加進RecyclerPool
		removeViewAt(index);
		recycler.recycleViewHolderInternal(viewHolder);
	} else {
      	// 添加進一級緩存
		detachViewAt(index);
		recycler.scrapView(view);
		mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
	}
}
複製代碼

上面的英文註釋其實就是我開始所說的,暫時保存被detachViewHolder,至於Recycler如何保存,咱們在上一篇博客中已經討論過,這裏再也不贅述。

2.3 第四步

最複雜的就是子View的填充過程,回到LinearLayoutManager#onLayoutChildren方法,咱們假設mAnchorInfo.mLayoutFromEndfalse,那麼LinearLayoutManager會先從錨點處往下填充,直至填滿,往下填充前,會先更新LayoutState

private void updateLayoutStateToFillEnd(AnchorInfo anchorInfo) {
	updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate);
}

private void updateLayoutStateToFillEnd(int itemPosition, int offset) {
  	// mAvailable:能夠填充的距離
	mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;
  	// 填充方向
	mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD :
	                LayoutState.ITEM_DIRECTION_TAIL;
  	// 當前位置
	mLayoutState.mCurrentPosition = itemPosition;
	mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END;
  	// 當前位置的偏移量
	mLayoutState.mOffset = offset;
	mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
}
複製代碼

更新完LayoutState之後,就是子View的真實填充過程LinearLayoutManager#fill

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, Boolean stopOnFocusable) {
	// 獲取可使用的空間
	final int start = layoutState.mAvailable;
	if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
		// ...
      	// 滑動發生時回收ViewHolder
		recycleByLayoutState(recycler, layoutState);
	}
	int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
	LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
  	// 核心加載過程
	while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
		//...
		layoutChunk(recycler, state, layoutState, layoutChunkResult);
		//... 省略的是:加載一個ViewHolder以後處理狀態信息
	}
	// 返回消費的空間
	return start - layoutState.mAvailable;
}
複製代碼

最核心的就是while循環裏面的LinearLayoutManager#layoutChunk,最後來看一下該方法如何實現的:

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
	// 利用緩存策略獲取 與Recycler相關
	View view = layoutState.next(recycler);
	// 添加或者刪除 最後會通知父佈局新增或者移除子View
	if (layoutState.mScrapList == null) {
		if (mShouldReverseLayout == (layoutState.mLayoutDirection
				                    == LayoutState.LAYOUT_START)) {
			addView(view);
		} else {
			addView(view, 0);
		}
	} else {
		if (mShouldReverseLayout == (layoutState.mLayoutDirection
				                    == LayoutState.LAYOUT_START)) {
			addDisappearingView(view);
		} else {
			addDisappearingView(view, 0);
		}
	}
	// 測量子View
	measureChildWithMargins(view, 0, 0);
  	// 佈局子View
	layoutDecoratedWithMargins(view, left, top, right, bottom);
	// ... 設置LayoutChunkResult參數
}
複製代碼

首先,View view = layoutState.next(recycler);就是咱們在上一節中討論利用緩存Recycler去獲取ViewHolder,接着獲取ViewHolder中綁定的子View,給它添加進父佈局RecyclerView,而後給子View測量一下寬高,最後,有了寬高信息,給它放置到具體的位置就完事了,過程清晰明瞭。

回到上個方法LinearLayoutManager#fill,在While循環而且有數據的狀況下,不斷的將子View填充至RecyclerView中,直至該方向填滿。

再回到一開始的LinearLayoutManager#onLayoutChildren方法,除了調用了咱們第四步一開始介紹的LinearLayoutManager#updateLayoutStateToFillEnd,還調用了LinearLayoutManager#updateLayoutStateToFillStart,因此從總體上來看,它是先填充錨點至結束的方向,再填充錨點至開始的方向(不絕對),若是用一圖表示,我以爲能夠是這樣:

填充邏輯
先從錨點向下填充,再從錨點向上填充,不過,也有多是先向上,再向下,由一些參數決定。

第五步

第五步就是對以前的子View的填充結果作一些處理,不作過多介紹。

2、實戰

看了VivianTimeLine,你可能會這麼吐槽,人家的庫藉助StaggeredGridLayoutManager就能夠實現時間軸,爲什麼還要畫蛇添足,使用個人TwoSideLayoutManager(我給實現的佈局方式起名叫TwoSideLayoutManager)呢?由於使用瀑布流StaggeredGridLayoutManager想要在時間軸上實現子View平均分佈的效果仍是比較困難的,可是,使用TwoSideLayoutManager實現起來就簡單多了。

那麼咱們如何實現RecyclerView的兩側佈局呢?一張圖來打開思路:

實現思路
顯然, TwoSideLayoutManager的佈局實現能夠利用 LinearLayoutManager的實現方式,僅須要修改添加子 View之後的測量邏輯和佈局邏輯便可。

上面咱們提到過,添加子View,給子View測量,佈局都在LinearLayoutManager#layoutChunk中實現,那咱們徹底能夠照搬LinearLayoutManager的填充邏輯,稍微改幾處代碼,限於篇幅,咱們就看一下核心方法TwoSideLayoutManager#layoutChunk

private void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
                             LayoutState layoutState, LayoutChunkResult result) {
	View view = layoutState.next(recycler);
	if (view == null) {
		// 沒有更多的數據用來生成子View
		result.mFinished = true;
		return;
	}
	RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
	// 添加進RecyclerView
	if (layoutState.mLayoutDirection != LayoutState.LAYOUT_START) {
		addView(view);
	} else {
		addView(view, 0);
	}
	// 第一遍測量子View
	measureChild(view);
	// 佈局子View
	layoutChild(view, result, params, layoutState, state);
	// Consume the available space if the view is not removed OR changed
	if (params.isItemRemoved() || params.isItemChanged()) {
		result.mIgnoreConsumed = true;
	}
	result.mFocusable = view.hasFocusable();
}
複製代碼

總體邏輯在註釋中已經寫得很清楚了,挨個看一下主要方法。

1. measureChild

測量子View

private void measureChild(View view) {
	final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams();
	int verticalUsed = lp.bottomMargin + lp.topMargin;
	int horizontalUsed = lp.leftMargin + lp.rightMargin;
  	// 設置測量的長度爲可用空間的一半
	final int availableSpace = (getWidth() - (getPaddingLeft() + getPaddingRight())) / 2;
	int widthSpec = getChildMeasureSpec(availableSpace, View.MeasureSpec.EXACTLY
	                , horizontalUsed, lp.width, true);
	int heightSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(), getHeightMode(),
	                verticalUsed, lp.height, true);
	measureChildWithDecorationsAndMargin(view, widthSpec, heightSpec, false);
}
複製代碼

高度的使用方式跟LinearLayoutManager同樣,寬度控制在屏幕可用空間的一半。

2. layoutChild

佈局子View

private void layoutChild(View view, LayoutChunkResult result
            , RecyclerView.LayoutParams params, LayoutState layoutState, RecyclerView.State state) {
	final int size = mOrientationHelper.getDecoratedMeasurement(view);
	final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams();
	result.mConsumed = size;
	int left, top, right, bottom;
	int num = params.getViewAdapterPosition() % 2;
	// 根據位置 奇偶位來進行佈局
	// 若是起始位置爲左側,那麼偶數位爲左側,奇數位爲右側
	if (isLayoutRTL()) {
		if (num == mStartSide) {
			right = (getWidth() - getPaddingRight()) / 2;
			left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
		} else {
			right = getWidth() - getPaddingRight();
			left = right - mOrientationHelper.getDecoratedMeasurementInOther(view) - (getWidth() - getPaddingRight()) / 2;
		}
	} else {
		if (num == mStartSide) {
			left = getPaddingLeft();
			right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
		} else {
			left = getPaddingLeft() + (getWidth() - getPaddingRight()) / 2;
			right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
		}
	}
	if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
		bottom = layoutState.mOffset;
		top = layoutState.mOffset - result.mConsumed;
	} else {
		top = layoutState.mOffset;
		bottom = layoutState.mOffset + result.mConsumed;
		if (mLayoutState.mCurrentPosition == state.getItemCount() && lastViewOffset != 0) {
			lp.setMargins(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin + lastViewOffset);
			view.setLayoutParams(lp);
			bottom += lastViewOffset;
		}
	}
	layoutDecoratedWithMargins(view, left, top, right, bottom);
}

public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, ![總結.png](https://upload-images.jianshu.io/upload_images/9271486-9440574ea525a11a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
int right, int bottom) {
	RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)child.getLayoutParams();
	Rect insets = lp.mDecorInsets;
	child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin, right - insets.right - lp.rightMargin, bottom - insets.bottom - lp.bottomMargin);
}
複製代碼

給子View測量完寬高以後,根據奇偶位初始設置的一側mStartSide佈局子View。若是須要顯示時間軸的結束節點,那麼須要在建立TwoSideLayoutManager對象的時候設置lastViewOffset,預留最後位置的空間,不過,須要注意的是,若是設置了時間軸的結束節點,那麼,最後一個子View最好仍是不要回收,否則,最後一個子View回收給其餘數據使用的時候還得處理Margin。只要在回收的時候稍稍處理就好了,具體的代碼再也不貼出了。

3、總結

總結
寫這個佈局花的時間還挺多的,說明本身須要提高的地方還不少,有的時候代碼雖然能看懂,本身卻不必定能寫出來,下週須要提高效率,保證每週產出。本人水平有限,不免有誤,歡迎指出喲。
相關文章
相關標籤/搜索