上篇测绘流程的核心逻辑传递到LayoutManager中,本篇我们会详细分析LinearLayoutManager的源码,分析完成这个测绘流程,并且为以后自己实现LayoutManager作准备。
几个比较重要的方法。
1. generateDefaultLayoutParams()
LinearLayoutManager继承了RecyclerView.LayoutManager,RecyclerView.LayoutManager是一个抽象类。只有一个抽象方法generateDefaultLayoutParams是必须实现的。
public abstract LayoutParams generateDefaultLayoutParams();
这个方法是设置每个item默认的LayoutParams的,我们必须指定这个方法的实现。
LinearLayoutManager的实现比较简单。横竖向都是自适应的。
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
2. onLayoutChildren
在分析RecycleView的测绘流程的时候,控件的测绘主要是调用了LayoutManager的onLayoutChildren方法,这是一个入口,内部完成了各个item的measure和layout,实现第一次item的填充。里面有两个参数,第一个就是RecycleView的复用item的重头戏控件Recycler,用于内部回收复用的方式获取view。第二个是保存各种测绘信息的集中单元。
public void onLayoutChildren(Recycler recycler, State state) {
Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
}
3. onMeasure()和isAutoMeasureEnabled()
onMeasure()和isAutoMeasureEnabled()上一篇也说过了,不要同时实现它们的逻辑,两个方法重写是互斥的。isAutoMeasureEnabled()是系统提供的自动测绘,onMeasure()如果重写我们自己的测量流程。如果重写isAutoMeasureEnabled()返回true就不应该重写onMeasure()。如果没有重写isAutoMeasureEnabled(),默认是返回false,这时应该重写onMeasure()。
4. canScrollHorizontally()、canScrollVertically()、scrollHorizontallyBy()、scrollVerticallyBy()
滑动系列方法,前两个是是否可以水平竖直方向进行滑动。后两个是滑动过程中进行处理的方法。只有在前两个方法中放开,也就是返回true,RecycleView才会把touchEvent下放到LayoutManager里面。后两个方法随着滑动不但要实现view的回收和复用,还要进行新view的填充,和滚动出屏幕的回收。
如果要自己实现一个LayoutManager(后面的章节我们会自己实现一个LayoutManager),我们重写上面的几个方法就可以完成了。所以在分析系统的LinearLayoutManager时,我们着重分析上面的几个方法,即可了解大部分功能。
LayoutManager运行的整体思路
- 通过isAutoMeasureEnabled()判断是否使用自动测量,没有开启那么就靠LayoutManager#onMeasure()进行测量,这时我们就要自己去实现了。如果开启了,就走系统的dispatchLayoutStep系列方法,这时我们不需要自定义onMeasure()。
- 通过onLayoutChildren完成第一次的填充和后面的notify刷新,内部会通过计算可用面积,只填充显示区域下的各个item。这时第一屏的效果已经完成了。
- 我们滑动RecycleView,通过canScrollHorizontally()、canScrollVertically()、scrollHorizontallyBy()、scrollVerticallyBy()一些列方法,配置滑动下的反应。完成动态的填充,和不可见item的回收,新item的复用。
LayoutManager内分工比较明确,onLayoutChildren负责整屏的填充,scrollHorizontallyBy()、scrollVerticallyBy()处理滑动过程中的填充。两种填充方式都有了。
LayoutManager集成了RV的滑动处理和测量布局处理。实际上底层就是测量布局的处理,因为滑动过程中也需要重新进行测量和布局。
本篇只分析onLayoutChildren整屏填充部分,下一篇分析滑动部分。
onLayoutChildren整屏填充
前置条件:state.isPreLayout()为false,上文讲了和是否完成第一次测量和是否有动画相关。所以这个条件判断为true下的代码会在动画篇章讲解。
onLayoutChildren的代码比较多,但是从整体看,只有两个步骤
- 确认锚点信息
- 根据锚点信息进行测量布局
确认锚点信息步骤
//处理SavedState数据
if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
if (state.getItemCount() == 0) {
removeAndRecycleAllViews(recycler);
return;
}
}
if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
}
ensureLayoutState();
mLayoutState.mRecycle = false;
resolveShouldLayoutReverse();
final View focused = getFocusedChild();
if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
|| mPendingSavedState != null) {
mAnchorInfo.reset();
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// 初始化锚点信息
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
} else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
>= mOrientationHelper.getEndAfterPadding()
|| mOrientationHelper.getDecoratedEnd(focused)
<= mOrientationHelper.getStartAfterPadding())) {
mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
}
我们依次看下需要分析的点
-
mPendingSavedState和mPendingScrollPosition
mPendingSavedStates是缓存恢复的数据,通过它可以恢复到之前的状态。通常发生在销毁重建的过程。mPendingScrollPosition是通过scrollToPosition设置的。也就是外部要求滚动到的位置。没有设置默认值就是RecyclerView.NO_POSITION。 -
resolveShouldLayoutReverse()
这个方法处理了布局处理的方向,从正方向或者反方向开始填充。如果是水平方向并且是从右往左的阅读方向(比如阿拉伯风格)默认就是开启了反转方向。mShouldReverseLayout为true则是反方向绘制,从底部或者右侧开始绘制。false则相反。private void resolveShouldLayoutReverse() { // A == B is the same result, but we rather keep it readable if (mOrientation == VERTICAL || !isLayoutRTL()) { mShouldReverseLayout = mReverseLayout; } else { //水平并且从右到左的方向,默认开启反转布局 mShouldReverseLayout = !mReverseLayout; } }
-
mLayoutFromEnd
首先是 mAnchorInfo.mLayoutFromEnd的设置。只有mShouldReverseLayout和 mStackFromEnd不同时,才为true。
mStackFromEnd是通过setStackFromEnd设置的,表示是否初始定位是否在数据末端开始,如果为true那么就从最后一条数据开始显示。 mAnchorInfo.mLayoutFromEnd为true,形象的表示就是是在反方向开始进行布局。必入垂直方向,就是屏幕下端开始布局。
-
updateAnchorInfoForLayout锚点配置
锚点配置在updateAnchorInfoForLayout(recycler, state, mAnchorInfo)方法中。
首先看下AnchorInfo
,这个类就是存储锚点的信息,看下内部的具体细节。class AnchorInfo { OrientationHelper mOrientationHelper; int mPosition; int mCoordinate; boolean mLayoutFromEnd; boolean mValid; }
属性 意义 mOrientationHelper 是一个帮助类,里面有很多测量布局帮助方法。 mPosition 就是锚点的锚,具体就是屏幕上第一个position mCoordinate 表示绘制起点,初始化就是顶部底部的padding mLayoutFromEnd 表示是否从定位到屏幕底部 mValid 锚点是否可用 updateAnchorInfoForLayout方法内部具体设置实现了锚点的配置。
private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) { if (updateAnchorFromPendingData(state, anchorInfo)) { return; } if (updateAnchorFromChildren(recycler, state, anchorInfo)) { return; } //设置默认的起点,也就是上下边距 anchorInfo.assignCoordinateFromPadding(); //默认的锚点位置如果是从顶部布局就是第一个数据,从底部就是最后一个数据。 anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0; } void assignCoordinateFromPadding() { mCoordinate = mLayoutFromEnd ? mOrientationHelper.getEndAfterPadding() : mOrientationHelper.getStartAfterPadding(); }
大体结构:可以看出先通过peding的数据设置锚点,内部处理了mPendingScrollPosition的数据给锚点。再通过各个children设置锚点。最后都没有成功配置,设置默认的锚点。
- 通过mPending数据设置锚点
private boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) { if (state.isPreLayout() || mPendingScrollPosition == RecyclerView.NO_POSITION) { return false; } if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) { // mPendingScrollPosition判断是否合法 mPendingScrollPosition = RecyclerView.NO_POSITION; mPendingScrollPositionOffset = INVALID_OFFSET; return false; } anchorInfo.mPosition = mPendingScrollPosition; if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { //处理 mPendingSavedState的数据 anchorInfo.mLayoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd; if (anchorInfo.mLayoutFromEnd) { anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding() - mPendingSavedState.mAnchorOffset; } else { anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding() + mPendingSavedState.mAnchorOffset; } return true; } if (mPendingScrollPositionOffset == INVALID_OFFSET) { //处理没有设置offset的情况 View child = findViewByPosition(mPendingScrollPosition); if (child != null) { final int childSize = mOrientationHelper.getDecoratedMeasurement(child); if (childSize > mOrientationHelper.getTotalSpace()) { // item does not fit. fix depending on layout direction anchorInfo.assignCoordinateFromPadding(); return true; } final int startGap = mOrientationHelper.getDecoratedStart(child) - mOrientationHelper.getStartAfterPadding(); if (startGap < 0) { anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding(); anchorInfo.mLayoutFromEnd = false; return true; } final int endGap = mOrientationHelper.getEndAfterPadding() - mOrientationHelper.getDecoratedEnd(child); if (endGap < 0) { anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding(); anchorInfo.mLayoutFromEnd = true; return true; } anchorInfo.mCoordinate = anchorInfo.mLayoutFromEnd ? (mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper .getTotalSpaceChange()) : mOrientationHelper.getDecoratedStart(child); } else { // item is not visible. if (getChildCount() > 0) { // get position of any child, does not matter int pos = getPosition(getChildAt(0)); anchorInfo.mLayoutFromEnd = mPendingScrollPosition < pos == mShouldReverseLayout; } anchorInfo.assignCoordinateFromPadding(); } return true; } // override layout from end values for consistency anchorInfo.mLayoutFromEnd = mShouldReverseLayout; // if this changes, we should update prepareForDrop as well if (mShouldReverseLayout) { anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding() - mPendingScrollPositionOffset; } else { anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding() + mPendingScrollPositionOffset; } return true; }
这个方法主要处理通过scrollToPositionWithOffset(int position, int offset)方法传入的数据的处理。也就是指定滚动位置和offset的处理点。具体逻辑比较清晰,这里就不细讲了。
-
通过children数据设置锚点
这里主要通过已有的childer,根据绘制从底部还是顶部,找到一个最接近的锚点。应用于notify系列方法进行刷新数据。因为当前已经有children了,所以直接找屏幕上显示的第一个view作为锚,以保存当前滑动的位置。我们自己定义的LayoutManager,当弹起软键盘时,如果没有通过children数据设置锚点。因为RecyclerView重新绘制,会回到初始进行刷新,而不是当前已经滑动到的位置。 -
默认锚点
上面两个步骤都没有确认锚点,那么就会使用默认的锚点。
anchorInfo.assignCoordinateFromPadding(); anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
短短的两行代码,第一行从方法名可以看出是设置绘制起点为padding的部分,这里也说明了RecycleView的padding属性怎么生效的。第二行设置了锚点的位置,如果setStackFromEnd设置了数据显示方向,如果是屏幕底部(true)第一屏显示的就是最后一条数据。如果屏幕顶部(false 默认)显示的就是第一条数据。
经过上面三个步骤,锚点就确定完了。也就是起始绘制的数据index就确认了。接下来就需要确认绘制数据和调用fill进行填充了。
确认绘制数据
确认绘制数据主要是通过updateLayoutStateToFillStart(mAnchorInfo)和updateLayoutStateToFillEnd(mAnchorInfo)两个方法进行的。
detachAndScrapAttachedViews(recycler);
mLayoutState.mInfinite = resolveIsInfinite();
mLayoutState.mIsPreLayout = state.isPreLayout();
mLayoutState.mNoRecycleSpace = 0;
if (mAnchorInfo.mLayoutFromEnd) {
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
final int firstElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForEnd += mLayoutState.mAvailable;
}
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForEnd;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
// end could not consume all. add more items towards start
extraForStart = mLayoutState.mAvailable;
updateLayoutStateToFillStart(firstElement, startOffset);
mLayoutState.mExtraFillSpace = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
} else {
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = 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.mExtraFillSpace = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
extraForEnd = mLayoutState.mAvailable;
// start could not consume all it should. add more items towards end
updateLayoutStateToFillEnd(lastElement, endOffset);
mLayoutState.mExtraFillSpace = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
}
}
if (!state.isPreLayout()) {
mOrientationHelper.onLayoutComplete();
} else {
mAnchorInfo.reset();
}
mLastStackFromEnd = mStackFromEnd;
- 通过detachAndScrapAttachedViews方法detach所有的child。可以理解为detach掉所有add的字View,恢复到了一个无字View的初始状态,在后面的布局中统一逻辑进行新的布局。这部分和回收有关,后面的章节讲回收复用会详细讲这部分。包括detach和remove的区别。
- 通过mLayoutFromEnd判断填充的方向,上面也提到过这个变量。他是从mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd设置的。如果mLayoutFromEnd为true就是代表从末端开始填充,垂直布局就是从底部,水平布局就是从右侧。
如果从末端布局,先通过updateLayoutStateToFillStart配置,调用fill方法往反方向初始段布局,再通过updateLayoutStateToFillEnd配置往末端布局,同样通过fill进行末端填充。从首段进行填充刚好相反。
为什么会是这样的设计呢?而不是直接从底部开始
我们通过反证法验证这个问题,如果是从末端填充,我们这里通过垂直方向,也就是从屏幕底部开始布局举例。
1D37112C-68F4-4306-AA3B-65E7C0EC0357.png
在这种情况,我们就需要确定距离屏幕顶部有几个可以填充的,才可以确定自己的位置,因为顶部填充不满剩余的空间,我们直接从底部绘制是错误,会开天窗。
B7438AE2-9773-4B0D-9FBF-7C7AC7470356.png
所以通过上面的分析我们知道为什么要这么设计了。相反,从首部开始填充的逻辑先填充下侧,因为这时第一个位置肯定是在屏幕最顶部的。填充完下侧再填充上侧的,这时没有地方填充了,但是如果我们通过scrollToPositionWithOffset
方法设置offset往上侧填充就可以有空间继续了。
调用fill进行填充
确认完锚点和绘制数据后,接下来就是重头戏了,通过fill进行填充,这是一个很重要的方法
fill(recycler, mLayoutState, state, false)
通过他的参数可以看出,recycler是RecycleView提供复用回收的统一控件。mLayoutState, state包含了绘制的各种参数,包括起始点、方向、可用空间等。所以这个方法大体的结构,通过recycler拿到待布局的view,通过参数进行填充。我们看下具体的代码。
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// max offset we should set is mFastScroll + available
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
//如果产生了滑动
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
// 统一进行回收
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.beginSection("LLM LayoutChunk");
}
// 根据可用空间进行布局
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.endSection();
}
if (layoutChunkResult.mFinished) {
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
|| !state.isPreLayout()) {
// 布局了新的view,减少可用空间
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
remainingSpace -= layoutChunkResult.mConsumed;
}
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
if (stopOnFocusable && layoutChunkResult.mFocusable) {
break;
}
}
if (DEBUG) {
validateChildOrder();
}
return start - layoutState.mAvailable;
}
逻辑比较简单
- 首先进行回收,这部分是进行滑动后的回收工作,下一章会讲滑动的部分。
- 算出remainingSpace,也就是可用的布局空间,while内部的判断也是通过remainingSpace判断是否有空间可以继续填充。消耗remainingSpace是通过layoutChunkResult这个变量。layoutChunkResult使用layoutChunk方法进行赋值的,所以主要的布局逻辑在layoutChunk中,这个也是一个很重要的方法。内部完成了指定postion的view测量和布局。
我们看下具体的layoutChunk代码:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
// 通过Recycler通过复用回收拿到下一个要填充postion的view
View view = layoutState.next(recycler);
if (view == null) {
result.mFinished = true;
return;
}
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
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);
}
}
// 测量
measureChildWithMargins(view, 0, 0);
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
int left, top, right, bottom;
if (mOrientation == VERTICAL) {
if (isLayoutRTL()) {
right = getWidth() - getPaddingRight();
left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
left = getPaddingLeft();
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;
}
} else {
top = getPaddingTop();
bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
right = layoutState.mOffset;
left = layoutState.mOffset - result.mConsumed;
} else {
left = layoutState.mOffset;
right = layoutState.mOffset + result.mConsumed;
}
}
// 布局
layoutDecoratedWithMargins(view, left, top, right, bottom);
}
// Consume the available space if the view is not removed OR changed
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable = view.hasFocusable();
}
- 首先提供next获取下一个要操作的view,这里直接调用了recycler.getViewForPosition方法,内部封装了复用的逻辑,下面的章节会细致的讲解。mItemDirection这里比较巧妙,他表示布局的方向,只能是1或者-1。
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
- 拿到view后,我们终于找到了子view测量布局的地方,通过measureChildWithMargins进行测量,通过layoutDecoratedWithMargins布局。
这样一步一步,直到RecycleView的可用空间填充满了,这时RecycleView就完成了第一屏的填充。
总结
-
LayoutManager
的职责主要集中在整屏布局填充、滑动布局填充、滑动处理、item测量布局等功能,我们实现自己的LayoutManager
时,也要实现这些功能 - 整屏填充的逻辑先确认锚点、确认布局参数,最终利用上面产生的数据进行填充。
- 填充填充可用区域的大小,也就是限制在RecyclerView的宽高内。
分析完LayoutManager的源码。并结合前两章的测绘流程,应该对整体的测绘,从表到里都有了很深的理解。
下一篇我们讲解RecycleView的滑动机制,分析滑动的处理和这是布局如何填充逻辑。
网友评论