抽丝剥茧RecyclerView
系列文章的目的在于帮助Android开发者提升对RecyclerView的认知,本文是整个系列的第二篇。git
前文回顾:github
LayoutManager
是RecyclerView
中的重要一环,使用LayoutManager
就跟玩捏脸蛋的游戏同样,即便好看的五官(好看的子View)都具有了,也不必定能捏出漂亮的脸蛋,好在RecyclerView
为咱们提供了默认的模板:LinearLayoutManager
、GridLayoutManager
和StaggeredGridLayoutManager
。bash
说来惭愧,若是不是看了GridLayoutManager
的源码,我还真不知道GridLayoutManager
居然能够这么使用,图片来自网络: 网络
不过呢,今天咱们讲解的源码不是来自GridLayoutManager
,而是线性布局LinearLayoutManager
(GridLayoutManager
也是继承自LinearLayoutManager
),分析完源码,我还将给你们带来实战,完成如下的效果: app
代码地址:github.com/mCyp/Orient…ide
本着认真负责的精神,我把RecyclerView
中用到LayoutManager
的地方大体看了一遍,发现其负责的主要业务:源码分析
Recyler
处理)。回收和复用子View
显然不是LayoutManager
实际完成的,不过,子View
的新增和删除都是LayoutManager
通知的,除此之外,滑动处理的本质仍是对子View
进行管理,因此,本文要讨论的只有测量和布局子View
的。布局
测量和布局子View
发生在RecyclerView
三大工做流程,又...又回到了最初的起点?这是咱们在上篇讨论过的,若是不涉及到LayoutManager
的知识,咱们将一笔带过便可。post
在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);
// ....
}
}
复制代码
从代码上来看,使用自动测量机制须要具有:
RecyclerView
布局的长和宽的SpecMode
不能是MeasureSpec.EXACTLY
(大几率指的是布局中RecyclerView
长或宽中有WrapContent
)。RecyclerView
设置的LayoutManger
的isAutoMeasureEnabled
返回为true
。当设置自动测量机制的时候,咱们的流程以下:
LayoutManager#onMeasure
方法,还不如不使用测量机制呢!
显然,这种想法是不对的,由于官方是这么说的,若是不使用自动测量机制,须要在自定义LayoutManager
过程当中复写LayoutManager#onMeasure
方法,因此呢,这个方法应该是包括自动测量机制的所有过程,包括:测量父布局
-布置子View
-从新测量子View
-从新测量父布局
,而使用自动测量机制是不须要复写这个方法的,该方法默认测量父布局。
须要说起的是,咱们平时使用的三大LayoutManager
都开启了自动测量机制。
即便RecyclerView
在onMeasure
方法中逃过了布局子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
第一步是建立LayoutState
,第二步是获取屏幕中的焦点子View
,代码比较简单,感兴趣的同窗们能够本身查询。
在填充子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);
}
}
复制代码
上面的英文注释其实就是我开始所说的,暂时保存被detach
的ViewHolder
,至于Recycler
如何保存,咱们在上一篇博客中已经讨论过,这里再也不赘述。
最复杂的就是子View
的填充过程,回到LinearLayoutManager#onLayoutChildren
方法,咱们假设mAnchorInfo.mLayoutFromEnd
为false
,那么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
的填充结果作一些处理,不作过多介绍。
看了Vivian的TimeLine,你可能会这么吐槽,人家的库借助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();
}
复制代码
总体逻辑在注释中已经写得很清楚了,挨个看一下主要方法。
测量子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
同样,宽度控制在屏幕可用空间的一半。
布局子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, 
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
。只要在回收的时候稍稍处理就好了,具体的代码再也不贴出了。