在Android中如果想要实现一个可滑动的列表,你会怎么做呢?对于我来说,我接触到的第一个列表类的控件就是ListView,其次就是RecylerView。目前来说,ListView已经很少用了,作为JetPack库中一部分,RecylerView显然是主流。可是对于我来说,我只知道实现RecyclerView的最简单的使用,而不了解其原理,在想要实现一些进阶效果时就有心而无力。
本文是源码解析RecyclerView的第一篇文章,主要介绍的是一些前置概念和RecyclerView的视图的工作流程,即onMeasure,onLayout,onDraw三个过程。
首先我们来看看源码中最开始提供的注释,主要是一张术语表:
这些术语在一定程度上说明了RecycleView的特点,主要是通过一层抽象把视图项和数据项进行分离,以此来达到高效显示和动态显示的效果。
除了介绍了RecyclerView的基本信息之外,注释中还介绍了一些和RecyclerView一起使用的一些API,他们分别适用于不同的使用场景。
使用RecyclerView中永恒的主题应该就是关于列表项的更新了,大部分的初级使用者可能只会使用notifyDataSetChanged方法来更新整个RecyclerView。但是实际上,Google官方提供了许多工具来帮助我们正确地更新列表项。关于更新列表项主要提供了四个工具
分页加载主要就是一个Paging库,使用这个库可以帮助我们实现分页加载的效果。
首先我们来看源码中的一些成员变量的类型:
onDraw()
中)和在项目之上(在 onDrawOver(Canvas, RecyclerView, RecyclerView.State)
中)进行绘制。ItemAnimator
的子类可以用于为 ViewHolder 项的操作实现自定义动画。接下来就需要分析的就是RecyclerView视图从测量,放置到绘制的这三大流程了,首先查看它的测量流程:
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
......
}
当我们没有设置LayoutManager时,就会调用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);
}
而当我们设置了LayoutManager后就且允许自动测量标志位为true时就会调用该LayoutManager的onMeasure方法来测量尺寸:
protected void onMeasure(int widthSpec, int heightSpec) {
......
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
/**
* This specific call should be considered deprecated and replaced with
* {@link #defaultOnMeasure(int, int)}. It can't actually be replaced as it could
* break existing third party code but all documentation directs developers to not
* override {@link LayoutManager#onMeasure(int, int)} when
* {@link LayoutManager#isAutoMeasureEnabled()} returns true.
*/
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
......
}
}
这个自动测量模式是RecyclerView中的一种测量机制,它简化了布局的测量过程,包括根据子项的尺寸和位置计算所需的尺寸。它还支持 RecyclerView 的所有现有动画功能。我们常用的LinearLayoutManager就启用了这个自动测量模式。
可以看到在这里有一段注释,大概内容是:
onMeasure(int, int)
方法在特定情况下应被视为已弃用。虽然该方法仍然可用,但它被认为是过时的。defaultOnMeasure(int, int)
方法来代替 onMeasure(int, int)
方法。这是因为布局管理器的自动测量(AutoMeasure)被启用时,onMeasure(int, int)
方法的行为可能不稳定,因此建议使用默认方法。onMeasure(int, int)
方法,但也提到了一些现有的第三方代码可能仍然在使用该方法。因此,即使建议使用新方法,但不建议删除或更改现有代码中的 onMeasure(int, int)
方法,以兼容旧版本的代码。所以说,如果启用了这个自动测量模式的话,就不应该再实现LayoutManager的onMeasure方法了。
继续往下走,就会触发mLayoutManager的onMeasure,但是由于启动了自动测量就不应该再重写mLayoutManager的onMeasure方法,所以就会触发RecyclerView的defaultOnMeasure方法,和一开始没有设置mLayoutManager时是一样地测量。如果宽和高的测量模式都是EXACTLY模式或者没有设置适配器的话就会退出测量。
protected void onMeasure(int widthSpec, int heightSpec) {
.......
}
if (mLayout.isAutoMeasureEnabled()) {
......
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
.....
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
} else {
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
........
}
}
如果其中有一个测量模式不是EXACTLY模式的话,接下来就会开始进一步的测量。首先将自身状态设置为START状态,然后开始进行分发Layout过程,进入分发的第一步进入dispatchLayoutStep1方法中。完成dispatchLayoutStep1之后就会接着进行dispatchLayoutStep2的方法中。接着step2也被完成后就会调用setMeasureDimensionFromChildren方法,这个方法的作用是计算子项的边界框,然后根据这个边界框来设置 RecyclerView 的测量尺寸。如果不启动自动测量的话就会进入到else块中进行自定义测量。不过官方的LayoutManager都是启用的自动测量,所以我们就不关注else块中了。
首先这个方法有一段简介,大致内容就是这个方法主要进行了这几个步骤:处理适配器的更新,决定需要运行哪些动画,存储当前视图的信息,如果有必要的话将会进行布局预测。来看这个方法:
private void dispatchLayoutStep1() {
.......
if (mState.mRunSimpleAnimations) {
// Step 0: Find out where all non-removed items are, pre-layout
int count = mChildHelper.getChildCount();
for (int i = 0; i < count; ++i) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) {
continue;
}
final ItemHolderInfo animationInfo = mItemAnimator
.recordPreLayoutInformation(mState, holder,
ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
holder.getUnmodifiedPayloads());
mViewInfoStore.addToPreLayout(holder, animationInfo);
if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
&& !holder.shouldIgnore() && !holder.isInvalid()) {
long key = getChangedHolderKey(holder);
// This is NOT the only place where a ViewHolder is added to old change holders
// list. There is another case where:
// * A VH is currently hidden but not deleted
// * The hidden item is changed in the adapter
// * Layout manager decides to layout the item in the pre-Layout pass (step1)
// When this case is detected, RV will un-hide that view and add to the old
// change holders list.
mViewInfoStore.addToOldChangeHolders(key, holder);
}
}
}
.........
}
首先来看这个方法的第一个分支,如果需要进行简单动画的话,那么就会进入到这个分支里去。首先获取当前列表中的列表项数目,然后依次获取对应的ViewHolder。如果这个ViewHolder被设置为可忽略的 或者 holder失效且其没有稳定的Id的话 就会跳过这次循环。这里出现了StableIds(稳定的Id),这个Id主要用于支持有关项目的动画效果以及在数据集更改时正确更新项目的位置。如果适配器的数据集中的项目具有稳定的 ID,RecyclerView 将能够更好地管理动画和视图项的位置。
如果还没有退出本次循环的话就会进行到下一步,也就是记录预布局的相关信息(recordPreLayoutInformation)
,然后将其视图信息的存储库中mViewInfoStore.addToPreLayout(holder, animationInfo)
。接下来如果目前的这个ViewHolder被更新了且仍然有效且需要追踪旧的ViewHolder(mState.mTrackOldChangeHolders为true
)的话那么就会将老的ViewHolder的信息也加入到ViewInfoStore中。这样第一个分支就结束了,接下来再看第二个分支:
private void dispatchLayoutStep1() {
......
if (mState.mRunPredictiveAnimations) {
// Step 1: run prelayout: This will use the old positions of items. The layout manager
// is expected to layout everything, even removed items (though not to add removed
// items back to the container). This gives the pre-layout position of APPEARING views
// which come into existence as part of the real layout.
// Save old positions so that LayoutManager can run its mapping logic.
saveOldPositions();
final boolean didStructureChange = mState.mStructureChanged;
mState.mStructureChanged = false;
// temporarily disable flag because we are asking for previous layout
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = didStructureChange;
.......
clearOldPositions();
} else {
clearOldPositions();
}
......
}
如果需要运行预加载的动画的话就会进入到这个分支,首先会存储旧的Position信息。然后存储旧的结构是否改变的标志位,然后暂时将这个标志位置为false。然后会调用到mLayoutManager的onLayoutChildren方法来布局各个子项。布局完毕之后再讲结构改变标志位置为原来的状态。
private void dispatchLayoutStep1() {
.....
if (mState.mRunPredictiveAnimations) {
.....
for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
final View child = mChildHelper.getChildAt(i);
final ViewHolder viewHolder = getChildViewHolderInt(child);
if (viewHolder.shouldIgnore()) {
continue;
}
if (!mViewInfoStore.isInPreLayout(viewHolder)) {
int flags = ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder);
boolean wasHidden = viewHolder
.hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
if (!wasHidden) {
flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
}
final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(
mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
if (wasHidden) {
recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
} else {
mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
}
}
}
// we don't process disappearing list because they may re-appear in post layout pass.
clearOldPositions();
} else {
clearOldPositions();
}
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
mState.mLayoutStep = State.STEP_LAYOUT;
}
接下来仍然会获取到列表项,然后获得其ViewHolder,当当前ViewHolder不是正在预布局列表中的话就会进入到if块中。这个预布局列表在之前的分支中提到过(mViewInfoStore.addToPreLayout(holder, animationInfo)
)。在这个语句块中首先会通过ItemAnimator来为当前ViewHolder构建出动画标志位,以确定应该如何处理该项目的动画。然后调用 mItemAnimator.recordPreLayoutInformation()
方法来记录列表项的预布局信息。这个方法将 ViewHolder、动画标志和列表项的未修改负载(payloads)作为参数传递,并返回一个包含动画信息的 ItemHolderInfo
对象。最后,根据列表项是否曾经隐藏过,将动画信息添加到 mViewInfoStore
中的适当位置。如果曾经隐藏过,使用 recordAnimationInfoIfBouncedHiddenView()
方法进行记录,否则,添加到 mViewInfoStore
的 “AppearedInPreLayoutHolders” 中,以便稍后的动画处理。
最后会通过clearOldPosition方法来清除所有ViewHolder项的旧位置信息。我们也可以稍微看一看这个方法:
void clearOldPositions() {
final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
if (!holder.shouldIgnore()) {
holder.clearOldPosition();
}
}
mRecycler.clearOldPositions();
}
可以看到这个方法就是对RecyclerView中的所有未被忽略的holder进行旧数据的清除,最后再调用回收池的clearOldPositions方法来清除回收池中的旧数据。关于RecyclerView中的回收池等缓存机制我们将在下篇文章介绍。
接着回到dispatchLayoutStep1方法中,最后的三个方法:
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
mState.mLayoutStep = State.STEP_LAYOUT;
onExitLayoutOrScroll()
方法确保在布局或滚动操作完成后执行一些必要的清理和事件分发操作,以维护 RecyclerView 的一致性和正确性。然后调用stopInterceptRequestLayout()
方法停止拦截layout过程。然后把当前状态位置为State.STEP_LAYOUT。到这里就完成了dispatchLayoutStep1。
紧接着回到onMeasure方法中我们就可以发现完成了dispatchLayoutStep1之后就是dispatchLayoutStep2方法的执行了。
这个方法也有一段摘要:
The second layout step where we do the actual layout of the views for the final state. This step might be run multiple times if necessary (e.g. measure).
主要就是说这个步骤中RecyclerView 将根据最终的状态,安排和布局所有的视图。这可能包括视图的测量、定位、分配大小和排列等操作,以确保它们正确地显示在 RecyclerView 中。在必要的情况下这个步骤将会多次被执行。
接下来看这个方法:
private void dispatchLayoutStep2() {
....
mAdapterHelper.consumeUpdatesInOnePass();
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = false;
mPendingSavedState = null;
// onLayoutChildren may have caused client code to disable item animations; re-check
mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
mState.mLayoutStep = State.STEP_ANIMATIONS;
......
}
第一个重要方法是mAdapterHelper.consumeUpdatesInOnePass()
,在这个方法中将消费掉,RecyclerView 通过 mAdapterHelper 对象一次性处理所有的更新操作,以确保布局过程中不会受到更新的干扰。消费完之后再更新目前列表项的数目。接着把自身的的InPreLayout标志位设置为false,这个标志位和RecyclerView的布局生命周期有关:
在 RecyclerView 的生命周期中,有两个主要的布局步骤:预布局(pre-layout)和主布局(main layout) 。
所以说将这个标志位置为false代表这个RecyclerView不运行在了预布局,而是在进行主布局。之后会调用mLayoutManager的onLayoutChildren来布局列表项。最后标记结构未改变,清除保存状态然后标记当前状态为State.STEP_ANIMATIONS。
你可能会好奇dispatchLayoutStep3方法在哪里,它实际上是在dispatchLayout方法中,而dispatchLayout方法是在onLayout方法中:
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
void dispatchLayout() {
......
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
// First 2 steps are done in onMeasure but looks like we have to run again due to
// changed size.
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
dispatchLayout
方法是 RecyclerView 布局过程的入口点,它负责执行布局的三个主要步骤,并根据需要重新运行布局以确保布局的正确性。这个方法来说:
mState.mIsMeasuring
设置为 false
,表示不处于测量状态。接着,根据当前布局步骤(mState.mLayoutStep
)的状态,执行以下两种情况之一:
State.STEP_START
,表示布局的起始阶段,那么它将执行 dispatchLayoutStep1()
,这是布局的第一步。接着,它会通过 mLayout.setExactMeasureSpecsFrom(this)
设置布局管理器的测量规格(MeasureSpecs),然后执行 dispatchLayoutStep2()
,这是布局的第二步。mAdapterHelper.hasUpdates()
)或者 RecyclerView 的尺寸是否发生了变化来完成的。如果有更新或尺寸变化,它会再次通过 mLayout.setExactMeasureSpecsFrom(this)
设置布局管理器的测量规格,然后执行 dispatchLayoutStep2()
。dispatchLayoutStep3()
,这是布局的第三步。这一步用于完成布局过程,包括执行可能的动画和其他必要的布局操作。所以我们接下来看最后一步方法dispatchLayoutStep3:
private void dispatchLayoutStep3() {
.......
mState.mLayoutStep = State.STEP_START;
if (mState.mRunSimpleAnimations) {
// Step 3: Find out where things are now, and process change animations.
// traverse list in reverse because we may call animateChange in the loop which may
// remove the target view holder.
for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
if (holder.shouldIgnore()) {
continue;
}
long key = getChangedHolderKey(holder);
final ItemHolderInfo animationInfo = mItemAnimator
.recordPostLayoutInformation(mState, holder);
ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
// run a change animation
// If an Item is CHANGED but the updated version is disappearing, it creates
// a conflicting case.
// Since a view that is marked as disappearing is likely to be going out of
// bounds, we run a change animation. Both views will be cleaned automatically
// once their animations finish.
// On the other hand, if it is the same view holder instance, we run a
// disappearing animation instead because we are not going to rebind the updated
// VH unless it is enforced by the layout manager.
final boolean oldDisappearing = mViewInfoStore.isDisappearing(
oldChangeViewHolder);
final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
if (oldDisappearing && oldChangeViewHolder == holder) {
// run disappear animation instead of change
mViewInfoStore.addToPostLayout(holder, animationInfo);
} else {
final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(
oldChangeViewHolder);
// we add and remove so that any post info is merged.
mViewInfoStore.addToPostLayout(holder, animationInfo);
ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
if (preInfo == null) {
handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder);
} else {
animateChange(oldChangeViewHolder, holder, preInfo, postInfo,
oldDisappearing, newDisappearing);
}
}
} else {
mViewInfoStore.addToPostLayout(holder, animationInfo);
}
}
// Step 4: Process view info lists and trigger animations
mViewInfoStore.process(mViewInfoProcessCallback);
}
.........
}
这一步方法也有摘要,主要就是说这个方法是最后的一步方法,在这个方法里我们将会将视图的动画信息存储下来然后触发这些动画,在必要的情况下还会做一些清理工作。
在这层循环一开始是和dispatchLayoutStep1
方法很像的。最开始会把自身的状态设置为start,这样就可以保证接下来再次进行layout时也会触发dispatchLayoutStep1
和dispatchLayoutStep2
方法。然后就是获取到相关联的ViewHolder。接着获取到它的更改Key,再用ItemAnimator将当前信息记录下来。接下来还会额外获取到与当前ViewHolder对应的旧版本的ViewHolder,这主要是为了判断ViewHolder是否发生了变化,根据这个ViewHolder的变化情况接下来会进行不同的处理。实际上上面的大段注释也告诉了我们它是如何处理不同情况的, 根据不同情况处理变化。具体情况如下:
mViewInfoStore
中弹出前布局信息(preInfo),然后将当前 ViewHolder 添加到后布局信息中。接着,再次从 mViewInfoStore
中弹出后布局信息(postInfo)。然后,通过调用 animateChange
方法处理变化,包括旧版本和新版本的 ViewHolder、前布局信息和后布局信息以及是否被标记为消失的状态。进行完这一系列处理之后,调用mViewInfoStore.process(mViewInfoProcessCallback)
来处理视图信息列表并且触发动画。
在做完这一大段操作之后实际上这个方法的核心逻辑就已经结束了,之后这个方法还会进行一些后续处理,主要是更新各种标志位,表明dispatchLayoutStep过程已经结束了,最后调用mLayoutManager的onLayoutCompleted方法做一些清理工作。
最后就是Draw流程了,来到onDraw流程也是最简单的一个流程:
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
ess(mViewInfoProcessCallback)`来处理视图信息列表并且触发动画。
在做完这一大段操作之后实际上这个方法的核心逻辑就已经结束了,之后这个方法还会进行一些后续处理,主要是更新各种标志位,表明dispatchLayoutStep过程已经结束了,最后调用mLayoutManager的onLayoutCompleted方法做一些清理工作。
最后就是Draw流程了,来到onDraw流程也是最简单的一个流程:
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
很简单,调用到了超类的onDraw,也就是ViewGroup的onDraw方法,它会将onDraw来分发给各个子View进行绘制。然后会根据ItemDecorations中的信息来绘制一些列表项的额外装饰,比如说分割线等。