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的布局
从前面的分析可知,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();
}
Anchor#mPosition
代表需要从Adapter
中获取数据的位置。Anchor#mCoordinate
代表子View需要从哪个坐标点开始填充子View。
根据例子中的情况,锚点坐标是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;
// 保存需要从Adapter获取数据的位置
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 {
rvLayoutParams = (LayoutParams) lp;
}
// 布局参数保存ViewHolder
rvLayoutParams.mViewHolder = holder;
// 如果View不是新创建,并且已经绑定,那么mPendingInvalidate为true,表示需要刷新
rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
return holder;
}
第一步,调用ViewHolder#createViewHolder()
方法创建ViewHolder
对象
public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
final VH holder = onCreateViewHolder(parent, viewType);
holder.mItemViewType = viewType;
return holder;
}
通过ViewHolder#onCreateViewHolder()
创建ViewHolder
对象,并给ViewHolder
对象设置了mItemViewType
的值。
第二步,创建ViewHolder
对象后,通过ViewHolder#tryBindViewHolderByDeadline()
方法绑定ViewHolder
对象
private boolean tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPosition,
int position, long deadlineNs) {
// 设置ViewHolder的mOwnerRecyclerView值,表明ViewHolder绑定到RV上
holder.mOwnerRecyclerView = RecyclerView.this;
mAdapter.bindViewHolder(holder, offsetPosition);
// 如果在pre-layout过程,用ViewHolder.mPreLayoutPosition保存ViewHolder在屏幕上显示的位置
if (mState.isPreLayout()) {
holder.mPreLayoutPosition = position;
}
return true;
}
public final void bindViewHolder(@NonNull VH holder, int position) {
// ViewHolder保存了数据在Adapter中的位置
holder.mPosition = position;
// 如果每个Item有固定的ID,那么ViewHolder保存这个ID
if (hasStableIds()) {
holder.mItemId = getItemId(position);
}
// ViewHolder设置标志FLAG_BOUND
holder.setFlags(ViewHolder.FLAG_BOUND,
ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
// 绑定ViewHolder
onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
holder.clearPayload();
final ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
if (layoutParams instanceof RecyclerView.LayoutParams) {
// 绑定后,mInsetsDirty设置为true,表明需要更新它的ItemDecoration
((LayoutParams) layoutParams).mInsetsDirty = true;
}
}
最主要就是通过ViewHolder#onBindViewHolder()
方法绑定ViewHolder
对象。另外,我们还需要注意给ViewHolder
设置的一些属性,我们在分析其他过程的时候,可能就会用到这里的某些参数。
大家需要知道基本的
Adapter
是如何写。
第三步,绑定ViewHolder
对象后,就需要确定创建出来的View,也就是ViewHolder#itemView
,它的布局参数的正确性,以及更新一些属性,例如用布局参数保存绑定的ViewHolder
对象。
如果你不清楚布局参数,可以参考我写的ViewGroup实现LayoutParams。
把子View添加到RV中
获取子View后,就需要把这个子View添加到RV中
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
// 1. 获取子View
// 2. 把子View添加到RV中
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
// 把获取的子View添加到RV末尾
addView(view);
} else {
}
}
}
addView()
方法是利用基类LM的addViewInt()
实现,而最终是通过mChildHelper.addView()
实现。
// ChildHelper#addView()
void addView(View child, int index, boolean hidden) {
final int offset;
if (index < 0) {
// index为-1,就获取RV已经有了多少个子View
offset = mCallback.getChildCount();
} else {
offset = getOffset(index);
}
// ...
// 根据offset,把子View添加到RV中
mCallback.addView(child, offset);
}
由于index
值为-1,因此是把这个View添加到RV的子View末尾。
测量子View
把子View添加到RV后,就需要测量这个子View
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
// 1. 获取子View
// 2. 把子View添加到RV中
// 3. 测量子View
measureChildWithMargins(view, 0, 0);
// 保存LLM相应方向上消耗的大小
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
}
public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 获取ItemDecoration的Rect
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
// 已经使用的宽和高要把ItemDecoration的Rect计算在内
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
// 这是一个考虑了padding, margin, ItemDecoration Rect的测量
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight()
+ lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom()
+ lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
对子View的测量,考虑了padding
, margin
, 以及ItemDecoration
的Rect
。 具体如何测量就不在本文的分析范围内。
给子View进行布局
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
// 1. 获取子View
// 2. 把子View添加到RV中
// 3. 测量子View
// 4. 子View布局
// 计算布局需要的坐标值
int left, top, right, bottom;
if (mOrientation == VERTICAL) {
if (isLayoutRTL()) {
} else {
left = getPaddingLeft();
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
} else {
top = layoutState.mOffset;
bottom = layoutState.mOffset + result.mConsumed;
}
} else {
// ...
}
// 进行布局
layoutDecoratedWithMargins(view, left, top, right, bottom);
}
这个布局过程很简单,这里是一笔代过了。
大家一定要了解自定义View如何测量和布局,这是你分析本文的基础。
绘制
测量和布局过程都已经分析完毕,剩下的就是绘制过程
public void draw(Canvas c) {
// 调用onDraw()方法
super.draw(c);
// 用ItemDecoration.onDrawOver()进行绘制
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
// 绘制边界效果
// ...
}
public void onDraw(Canvas c) {
super.onDraw(c);
// 用ItemDecoration.onDraw()进行绘制
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
对于RV来说,绘制过程主要就是绘制ItemDecoration
,首先是用ItemDecoration.onDraw()
方法进行绘制,然后再用ItemDecoration.onDrawOver()
进行绘制。
本系列的文章中,我会用单独一篇文章讲解
ItemDecoration
的原理,以及如何使用。
感想
RV的源码分析真不能一蹴而就,需要极大的耐心和毅力。本文以极简的方式(文章仍然很长)分析了RV从创建到显示界面的过程,初始窥探了RV的原理,但是这还远远不能满足我的好奇心,我将以这篇文章为基石继续前行。