RecyclerView处发布以来,就受到开发者的青睐,它良好的功能解耦,使我们在定制它的功能方面变得游刃有余。自从在项目中使用这个控件以来,我对它是不胜欢喜,以至于我想用一系列的文章来剖析它。本文就从最基本的显示入手来分析,为后面的分析打下坚实的基础。
基本使用
本文先分析
RecyclerView
从创建到显示的过程,这将为后面的系列文章打下坚实的基础,我们先看下它的基本使用
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.setAdapter(new RvAdapter());
复制代码
LayoutManager
和
Adapter
是
RecyclerView
必不可少的部分,本文就来分析这段代码。
为了方便,在后面的分析中,我将使用RV表示
RecyclerView
,用LM表示LayoutManager
,用LLM表示LinearLayoutManager
。
构造函数
View的构造函数通常就是用来解析属性和初始化变量,RV的构造函数也不例外,而与本文相关代码如下
public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
// ...
mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() {
// ...
});
mChildHelper = new ChildHelper(new ChildHelper.Callback() {
// ...
});
// ...
}
复制代码
从全名就大致可以猜测出这两个类的作用。
AdapterHelper
是
Adapter
的辅助类,用来处理
Adapter
的更新操作。
ChildHelper
是RV的辅助类,用来管理它的子View。
设置LayoutManager
public void setLayoutManager(@Nullable LayoutManager layout) {
// ...
// 保存LM
mLayout = layout;
if (layout != null) {
// LM保存RV引用
mLayout.setRecyclerView(this);
// 如果RV添加到Window中,那么就通知LM
if (mIsAttached) {
mLayout.dispatchAttachedToWindow(this);
}
}
// ...
// 请求重新布局
requestLayout();
}
复制代码
setLayoutManager()
方法最主要的操作就是RV和LM互相保存引用,由于RV的LM改变了,因此需要重新请求布局。
设置Adapter
setAdapter()
方法是由
setAdapterInternal()
实现
private void setAdapterInternal(@Nullable Adapter adapter, boolean compatibleWithPrevious,
boolean removeAndRecycleViews) {
// ...
// RV保存Adapter引用
mAdapter = adapter;
if (adapter != null) {
// 给新Adapter注册监听者
adapter.registerAdapterDataObserver(mObserver);
// 通知新Adapter已经添加到RV中
adapter.onAttachedToRecyclerView(this);
}
// 如果LM存在,就通知LM,Adapter改变了
if (mLayout != null) {
mLayout.onAdapterChanged(oldAdapter, mAdapter);
}
// 通知RV,Adapter改变了
mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
// 表示Adapter改变了
mState.mStructureChanged = true;
}
复制代码
RV保存
Adapter
引用并给新Adapter注册监听者,然后通知每一个关心
Adapter
的监听者,例如RV, LM。
测量
当一切准备就绪后,现在来分析测量的部分
protected void onMeasure(int widthSpec, int heightSpec) {
// ...
// 如果LM使用自动测量机制
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
// 为了兼容处理,实际调用了RV的defaultOnMeasure()方法测量
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
// 如果宽和高的测量模式都是EXACTLY,那么就使用默认测量值,并直接返回
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
// ... 省略剩余的测量代码
} else {
// ...
}
}
复制代码
首先根据LM是否支持RV的自动测量机制来决定测量逻辑,LLM是支持自动测量机制的,因此只分析这种情况的测量。
究竟什么是自动测量机制,大家可以仔细研读源码的注释以及测量逻辑,我这里只做简单分析。
使用自动测量机制,首先会调用LM的
onMeasure()
进行测量。这里你可能会有疑问,既然叫做自动测量机制,为何还会用LM来测量呢。其实这是为了兼容处理,它实际是调用了RV的
defaultOnMeasure()
方法
void defaultOnMeasure(int widthSpec, int heightSpec) {
final int width = LayoutManager.chooseSize(widthSpec,
getPaddingLeft() + getPaddingRight(),
ViewCompat.getMinimumWidth(this));
final int height = LayoutManager.chooseSize(heightSpec,
getPaddingTop() + getPaddingBottom(),
ViewCompat.getMinimumHeight(this));
setMeasuredDimension(width, height);
}
复制代码
我们可以发现,RV作为一个ViewGroup,这里居然没有考虑子View就保存了测量的结果。很显然,这是一个粗糙的测量。
但是这个粗糙的测量其实是为了满足一种特殊情况,那就是父View给出的宽高限制模式都是
MeasureSpec.EXACTLY
。从代码中可以发现,在经历这一步粗糙测量后,就处理了这种特殊情况。
为了简化分析,目前只考虑这种特殊情况。 被省略的代码其实就是考虑子View测量的代码,而这段代码在
onLayout()
中也有,因为放到后面讲解。
布局
onLayout
是由
dispatchLayout()
实现的
void dispatchLayout() {
// ...
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
// RV已经测量完毕,因此LM保存RV的测量结果
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
// ...
} else {
// ...
}
dispatchLayoutStep3();
}
复制代码
布局的过程,无论如何,都是经过
dispatchLayoutStep1()
,
dispatchLayoutStep2()
,
dispatchLayoutStep3()
完成。而与本文相关的只有
dispatchLayoutStep2()
,它是完成子View的实际布局操作,它是由LM的
onLayoutChildren()
实现。
LM实现子View的布局
从ww前面的分析可知,RV对子View的布局是交给LM来处理的。例子中使用的是LLM,因此这里分析它的
onLayoutChildren()
方法。由于这个方法代码量比较大,因此将分步解析。
初始化信息
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// 确保mLayoutState被创建
ensureLayoutState();
mLayoutState.mRecycle = false;
// 解析是否使用反向布局
if (mOrientation == VERTICAL || !isLayoutRTL()) {
mShouldReverseLayout = mReverseLayout;
} else {
mShouldReverseLayout = !mReverseLayout;
}
}
复制代码
首先确保了
LayoutState mLayoutState
的创建,
LayoutState
用来保存布局的状态。
然后解析是否使用反向布局,例子中的LLM使用的是垂直布局,并且布局使用默认的不支持
RTL
,因此
mShouldReverseLayout
值为
false
,表示不是反向布局。
大家需要知道LLM的返回布局的情况。
更新锚点信息
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// 1. 初始化信息
// 2. 更新锚点信息
if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
|| mPendingSavedState != null) {
mAnchorInfo.reset();
// 锚点信息保存是否是反向布局
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// 计算锚点的位置和坐标
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
// 表示锚点信息有效
mAnchorInfo.mValid = true;
}
// ...
final int firstLayoutDirection;
if (mAnchorInfo.mLayoutFromEnd) {
firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
: LayoutState.ITEM_DIRECTION_HEAD;
} else {
firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
: LayoutState.ITEM_DIRECTION_TAIL;
}
// 通知锚点信息准备就绪
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
}
复制代码
AnchorInfo mAnchorInfo
是用来保存锚点信息,锚点位置和坐标来表示布局从哪里开始,这个将会在后面看到。
锚点信息保存了是否反向布局的信息,这里又冒出来一个
mStackFromEnd
,这个是为了兼容支持
AbsListView#setStackFromBottom(boolean)
特性,说白了就是为了提供给开发者一致的操作方法。个人觉得这真是一个垃圾操作。
之后用
updateAnchorInfoForLayout()
方法计算出了锚点的位置和坐标
private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
AnchorInfo anchorInfo) {
// ...
// 根据padding值决定锚点坐标
anchorInfo.assignCoordinateFromPadding();
// 如果不是反向布局,锚点位置为0
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
}
// AnchorInfo#assignCoordinateFromPadding()
void assignCoordinateFromPadding() {
// 如果不是反向布局,坐标就是RV的paddingTop值
mCoordinate = mLayoutFromEnd
? mOrientationHelper.getEndAfterPadding()
: mOrientationHelper.getStartAfterPadding();
}
复制代码
根据例子中的情况,锚点坐标是RV的
paddingTop
,位置是0。
计算布局的额外空间
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// 1. 初始化信息
// 2. 更新锚点信息
// 3. 计算布局的额外空间
// 保存布局方向,无滚动的情况下,值为LayoutState.LAYOUT_END
mLayoutState.mLayoutDirection = mLayoutState.mLastScrollDelta >= 0
? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
// 计算布局需要的额外空间,结果保存到mReusableIntPair
calculateExtraLayoutSpace(state, mReusableIntPair);
// 额外究竟还要考虑padding
int extraForStart = Math.max(0, mReusableIntPair[0])
+ mOrientationHelper.getStartAfterPadding();
int extraForEnd = Math.max(0, mReusableIntPair[1])
+ mOrientationHelper.getEndPadding();
if (state.isPreLayout() && mPendingScrollPosition != RecyclerView.NO_POSITION
&& mPendingScrollPositionOffset != INVALID_OFFSET) {
// ...
}
}
复制代码
在RV滑动的时候,
calculateExtraLayoutSpace()
会分配一个页面的额外空间,其它的情况下是不会分配额外空间的。
对于例子中的情况,
calculateExtraLayoutSpace()
分配的额外空间就是0。但是对于布局,额外空间还需要考虑RV的padding值。
如果自定义一个继承自LLM的LM,可以复写
calculateExtraLayoutSpace()
定义额外空间的分配策略。
为子View布局
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// 1. 初始化信息
// 2. 更新锚点信息
// 3. 计算布局的额外空间
// 4. 为子View布局
// 首先分离并且回收子View
detachAndScrapAttachedViews(recycler);
// RV的高度为0,并且模式为UNSPECIFIED
mLayoutState.mInfinite = resolveIsInfinite();
mLayoutState.mIsPreLayout = state.isPreLayout();
mLayoutState.mNoRecycleSpace = 0;
if (mAnchorInfo.mLayoutFromEnd) {
// ...
} else {
// 从锚点位置向后填充
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;
}
// 从锚点位置向前填充
// ...
// 如果还有额外空间,就向后填充更多的子View
if (mLayoutState.mAvailable > 0) {
// ...
}
}
复制代码
为子View布局之前,首先从RV分离子View,并回收。然后,通过
fill()
分别从锚点位置,向后以及向前填充子View,最后如果还有剩余空间,就尝试尝试继续向后填充子View(如果还有子View的话)。
根据例子计算出来的锚点位置是0,坐标是
paddongTop
,因此这里只分析从锚点位置向后填充的过程。
首先调用
updateLayoutStateToFillEnd()
方法,根据锚点信息来更新
mLayoutState
private void updateLayoutStateToFillEnd(AnchorInfo anchorInfo) {
// 参数传入的是锚点的位置和坐标
updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate);
}
private void updateLayoutStateToFillEnd(int itemPosition, int offset) {
// 可用空间就是去掉padding后的可用大小
mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;
// 表示Adapter的数据遍历的方向
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;
}
复制代码
更新完
mLayoutState
信息后,就调用
fill()
填充子View
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
final int start = layoutState.mAvailable;
// ...
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
// 有可以空间,并且还有子View没有填充
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
layoutChunk(recycler, state, layoutState, layoutChunkResult);
// 这里代表所有子View已经layout完毕
if (layoutChunkResult.mFinished) {
break;
}
// 更新布局偏移量
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
// 重新计算可用空间
if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
remainingSpace -= layoutChunkResult.mConsumed;
}
// ...
}
// 返回此次布局使用了多少空间
return start - layoutState.mAvailable;
}
复制代码
根据例子来分析,只有还存在可用空间,并且还有子View没有填充,那么就会一直调用
layoutChunk()
方法进行填充子View,直到可用空间消耗完,或者没有了子View。
LLM#layoutChunk()分析
获取子View
layoutChunk()
是LLM为子View布局的核心方法,我们需要重点关注这个方法的实现。由于这个方法也比较长,因此我也打算分段讲解
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
// 1. 获取一个子View,并更新mLayoutState.mCurrentPosition
View view = layoutState.next(recycler);
}
复制代码
根据例子的情况,这里是从
RecyclerView.Recycler
中创建一个子View,下面的代码为创建新View的代码
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
// ...
if (holder == null) {
if (holder == null) {
// 1. 回调Adapter.onCreateViewHoler()创建ViewHolder,并设置ViewHolder类型
holder = mAdapter.createViewHolder(RecyclerView.this, type);
// ...
}
}
// ...
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// ...
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
// 2. 回调Adapter.bindViewHolder()绑定ViewHolder,并更新ViewHolder的一些信息
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
// 3. 确保创建View的布局参数的正确性并更新信息
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
final LayoutParams rvLayoutParams;
if (lp == null) {
rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
holder.itemView.setLayoutParams(rvLayoutParams);
} else if (!checkLayoutParams(lp)) {
rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
holder.itemView.setLayoutParams(rvLayoutParams);
} else