android图片布局填冲满,图文详解LinearLayoutManager填充、测量、布局过程

LinearLayoutManager并不是一个View,而是一个工具类,但是LinearLayoutManager承担了一个View(当然指的是RecyclerView)的布局、测量、子View 创建 复用 回收 缓存 滚动等等操作。

一、回忆一下

上一篇文章Android Render(三)supportVersion 27.0.0源码RecyclerView绘制流程解析已经说了 RecyclerView的绘制流程,dispatchLayoutStep1 dispatchLayoutStep2 dispatchLayoutStep3这三步都一定会执行,只是在RecyclerView的宽高是写死或者是match_parent的时候会提前执行dispatchLayoutStep1 dispatchLayoutStep2者两个方法。会在onLayout阶段执行dispatchLayoutStep3第三步。在RecyclerView 写死宽高的时候onMeasure阶段很容易,直接设定宽高。但是在onLayout阶段会把dispatchLayoutStep1 dispatchLayoutStep2 dispatchLayoutStep3三步依次执行。

e9752f8890c8

1 - LayoutManager绘制三步骤

二、onLayoutChildren开始布局准备工作

上图是在RecyclerView中绘制三步骤对dispatchLayoutStep三个方法的调用。看到代码我们可以知道是在dispatchLayoutStep2方法中调用LayoutManager的onLayoutChildren方法来布局ItemView的。

private void dispatchLayoutStep2() {

......略

// Step 2: Run layout

mState.mInPreLayout = false;

// 调用`LayoutManager`的`onLayoutChildren`方法来布局`ItemView`

mLayout.onLayoutChildren(mRecycler, mState);

......略

}

下图是LinearLayoutManager对循环布局所有的ItemView的流程图:

e9752f8890c8

2 - LinearLayoutManager绘制分析

虽然在RecyclerView的源码中会三步绘制处理,但是都不是真正做绘制布局测量的地方,真正的绘制布局测量都放在了不同的LayoutManager中了,我们就以LinearLayoutManager为例来分析一下。

在三中LayoutManager中,LinearLayoutManager应该是最为简单的一种了吧。GridLayoutManager也是继承LinearLayoutManager实现的,只是在layoutChunk方法中实现了不同的布局。

LinearLayoutManager布局从onLayoutChildren方法开始:

//LinearLayoutManager布局从onLayoutChildren方法开始

@Override

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.

// 通过检查孩子和其他变量,找到锚坐标和锚点项目位置 mAnchor为布局锚点 理解为不具有的起点.

// mAnchor包含了子控件在Y轴上起始绘制偏移量(coordinate),ItemView在Adapter中的索引位置(position)和布局方向(mLayoutFromEnd)

// 2) fill towards start, stacking from bottom 开始填充, 从底部堆叠

// 3) fill towards end, stacking from top 结束填充,从顶部堆叠

// 4) scroll to fulfill requirements like stack from bottom. 滚动以满足堆栈从底部的要求

......略

ensureLayoutState();

mLayoutState.mRecycle = false;

// resolve layout direction 设置布局方向(VERTICAL/HORIZONTAL)

resolveShouldLayoutReverse();

//重置绘制锚点信息

mAnchorInfo.reset();

// mStackFromEnd需要我们开发者主动调用,不然一直未false

// VERTICAL方向为mLayoutFromEnd为false HORIZONTAL方向是为true

mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;

// calculate anchor position and coordinate

// ====== 布局算法第 1 步 ======: 计算更新保存绘制锚点信息

updateAnchorInfoForLayout(recycler, state, mAnchorInfo);

......略

// HORIZONTAL方向时开始绘制

if (mAnchorInfo.mLayoutFromEnd) {

// ====== 布局算法第 2 步 ======: fill towards start 锚点位置朝start方向填充ItemView

updateLayoutStateToFillStart(mAnchorInfo);

mLayoutState.mExtra = extraForStart;

// 填充第一次

fill(recycler, mLayoutState, state, false);

startOffset = mLayoutState.mOffset;

final int firstElement = mLayoutState.mCurrentPosition;

if (mLayoutState.mAvailable > 0) {

extraForEnd += mLayoutState.mAvailable;

}

// ====== 布局算法第 3 步 ======: fill towards end 锚点位置朝end方向填充ItemView

updateLayoutStateToFillEnd(mAnchorInfo);

mLayoutState.mExtra = extraForEnd;

mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;

// 填充第二次

fill(recycler, mLayoutState, state, false);

endOffset = mLayoutState.mOffset;

......略

} else {

// VERTICAL方向开始绘制

// ====== 布局算法第 2 步 ======: fill towards end 锚点位置朝end方向填充ItemView

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;

}

// ====== 布局算法第 3 步 ======: fill towards start 锚点位置朝start方向填充ItemView

updateLayoutStateToFillStart(mAnchorInfo);

mLayoutState.mExtra = extraForStart;

mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;

// 填充第二次

fill(recycler, mLayoutState, state, false);

startOffset = mLayoutState.mOffset;

......略

}

// ===布局算法第 4 步===: 计算滚动偏移量,如果有必要会在调用fill方法去填充新的ItemView

layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);

}

layout algorithm: 布局算法:

1.通过检查孩子和其他变量,找到锚坐标和锚点项目位置 mAnchor为布局锚点 理解为不具有的起点,mAnchor包含了子控件在Y轴上起始绘制偏移量(coordinate),ItemView在Adapter中的索引位置(position)和布局方向(mLayoutFromEnd)。

2.开始填充, 从底部堆叠

3.结束填充,从顶部堆叠

4.滚动以满足堆栈从底部的要求

这四步骤我都在代码中标记出来了。

至于为什么有好几次会调用到fill方法,什么formEnd,formStart,这个请看图:

e9752f8890c8

3 - RecyclerView的ItemView填充方向

圆形红点就是我们布局算法在第一步updateAnchorInfoForLayout方法中计算出来的填充锚点位置。

第一种情况是屏幕显示的位置在RecyclerView的最底部,那么就只有一种填充方向为formEnd

第二种情况是屏幕显示的位置在RecyclerView的顶部,那么也只有一种填充方向为formStart

第三种情况应该是最常见的,屏幕显示的位置在RecyclerView的中间,那么填充方向就有formEnd和formStart两种情况,这就是 fill 方法调用两次的原因。

上面是RecyclerView的方向为VERTICAL的情况,当为HORIZONTAL方向的时候填充算法是不变的。

二、fill 开始布局ItemView

fill核心就是一个while循环,while循环执行了一个很核心的方法就是:

layoutChunk ,此方法执行一次就填充一个ItemView到屏幕。

看一下 fill 方法的代码:

// fill填充方法, 返回的是填充ItemView需要的像素,以便拿去做滚动

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,

RecyclerView.State state, boolean stopOnFocusable) {

// 填充起始位置

final int start = layoutState.mAvailable;

if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {

//如果有滚动就执行一次回收

recycleByLayoutState(recycler, layoutState);

}

// 计算剩余可用的填充空间

int remainingSpace = layoutState.mAvailable + layoutState.mExtra;

// 用于记录每一次while循环的填充结果

LayoutChunkResult layoutChunkResult = mLayoutChunkResult;

// ================== 核心while循环 ====================

while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {

layoutChunkResult.resetInternal();

// ====== 填充itemView核心填充方法 ====== 屏幕还有剩余可用空间并且还有数据就继续执行

layoutChunk(recycler, state, layoutState, layoutChunkResult);

}

......略

// 填充完成后修改起始位置

return start - layoutState.mAvailable;

}

代码看起来还是很简洁明了的。解释都加了注释,就不再罗列出来了。看到这里我们就知道了 fill 下一步的核心方法就是 layoutChunk , 此方法执行一次就是填充一个ItemView。

三、layoutChunk 创建 填充 测量 布局 ItemView

layoutChunk 方法主要功能标题已经说了 创建、填充 、测量 、布局 一个ItemView,一共有四步:

1 layoutState.next(recycler) 方法从一二级缓存中获取或者是创建一个ItemView。

2 addView方法加入一个ItemView到ViewGroup中。

3 measureChildWithMargins方法测量一个ItemView。

4 layoutDecoratedWithMargins方法布局一个ItemView。布局之前会计算好一个ItemView的left, top, right, bottom位置。

其实就是这四个关键步骤:

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,

LayoutState layoutState, LayoutChunkResult result) {

// ====== 第 1 步 ====== 从一二级缓存中获取或者是创建一个ItemView

View view = layoutState.next(recycler);

if (view == null) {

if (DEBUG && layoutState.mScrapList == null) {

throw new RuntimeException("received null view when unexpected");

}

// if we are laying out views in scrap, this may return null which means there is

// no more items to layout.

result.mFinished = true;

return;

}

// ====== 第 2 步 ====== 根据情况来添加ItemV,最终调用的还是ViewGroup的addView方法

LayoutParams params = (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);

}

}

// ====== 第 3 步 ====== 测量一个ItemView的大小包含其margin值

measureChildWithMargins(view, 0, 0);

result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);

// 计算一个ItemView的left, top, right, bottom坐标值

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;

}

}

// We calculate everything with View's bounding box (which includes decor and margins)

// To calculate correct layout position, we subtract margins.

// 根据得到的一个ItemView的left, top, right, bottom坐标值来确定其位置

// ====== 第 4 步 ====== 确定一个ItemView的位置

layoutDecoratedWithMargins(view, left, top, right, bottom);

if (DEBUG) {

Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"

+ (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"

+ (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));

}

// Consume the available space if the view is not removed OR changed

if (params.isItemRemoved() || params.isItemChanged()) {

result.mIgnoreConsumed = true;

}

result.mFocusable = view.hasFocusable();

}

四、LinearLayoutManager填充、测量、布局过程总结

从 RecyclerView 绘制触发的一开始,就会把需要绘制的ItemView做一次while循环绘制一次,中间要经历好多个步骤,还设计到缓存。RecyclerView的绘制处理等还是比较复杂的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值