刚才有个朋友问我,博主发生什么事了,给我发了几张截图,我一看,哦,原来是有个大帅哔看了文章,说是,博主,我能白嫖你的文章,我说年轻人,点个赞再走,他说不点,我说点一个,他说不点,我说点一个,他说不点,我说我这文章对你有用,他不服气,说要先看看。我说可以,很快啊,看完后,就是一个复制,一个粘贴,一个网页关闭,我大意了啊,没有删除文章。按传统博客的有用为止,他说已经输了啊。 后来他说他是乱点的,这可不是乱点的啊,训练有素。我劝年轻人好好点赞,耗子尾汁,谢谢朋友们
前言
整体流程、measure、layout 详解 ——深入分析RecyclerView源码(一)
缓存 ——深入分析RecyclerView源码(二)
滑动和动画 ——深入分析RecyclerView源码(二)
本篇文章分析主体流程,先来整体看一下RecycleView的 结构
- RecycleView 是一个ViewGroup,想要显示数据集Datas,需要通过适配器Adapter,把数据转为对应的View,这样就可以添加到RecycleView中了。(适配器模式)
- 由于屏幕能显示View的个数,往往是小于所有数据的个数。如果为每一个数据都创建一个View,效率肯定不够,所有使用Recycler来管理这些View,实现对View的创建,缓存,数据绑定(在代码实现中,缓存的是ViewHolder)
- 不同的业务场景,需要不同的布局样式,有线性、网格、瀑布等,这里抽象出LayoutManager 来方便拓展,例如LinearLayoutManager等(策略模式)
这篇文章认为此处是桥接模式,桥接模式的核心是为了解决 如果有多个变化维度,每个维度有多种实现,若抽象类使用继承实现每个维度的每种实现,会导致的类爆炸。应该使用组合,抽象类和维度接口的组合(抽象化和实现化之间使用组合/聚合关系而不是继承关系,从而使两者可以独立的变化)。桥接模式是一种结构型模式,不会在运行时改变。而策略模式,是一种行为模式,主要目的就是在运行时,可动态改变。这两种模式的类图很相似,但是要解决的问题完全不同。
- 每当数据集发生变化的时候,需要调用notifyDataSetChanged 等函数,通知RecycleView更新界面。所以需要在Adapter(被观察者)和RecycleView(观察者) 建立监听关系(观察者模式)
源码分析
文章的源码版本是androidx recycleView 1.0.0
先来看看RecyclerView的构造函数
代码一:
RecyclerView.java
public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
//获取自定义属性值
if (attrs != null) {
TypedArray a = context.obtainStyledAttributes(attrs, CLIP_TO_PADDING_ATTR, defStyle, 0);
mClipToPadding = a.getBoolean(0, true);
a.recycle();
} else {
mClipToPadding = true;
}
//设置当前View 是否可以滑动
setScrollContainer(true);
setFocusableInTouchMode(true);
final ViewConfiguration vc = ViewConfiguration.get(context);
//获取最小滑动的阈值
mTouchSlop = vc.getScaledTouchSlop();
//获取横竖方向的 滑动缩放因子,用在手势操作中
mScaledHorizontalScrollFactor =
ViewConfigurationCompat.getScaledHorizontalScrollFactor(vc, context);
mScaledVerticalScrollFactor =
ViewConfigurationCompat.getScaledVerticalScrollFactor(vc, context);
//获取最大最小 的滑动速率
mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
//如果不可滑动,就不需要绘制自己了。如果设置了Item Decoration,就会设置为false
setWillNotDraw(getOverScrollMode() == View.OVER_SCROLL_NEVER);
mItemAnimator.setListener(mItemAnimatorListener);
//初始化AdapterManager,用于处理Adapter的数据发生变化,例如新增、删除item等
//之所以需要它,如果有多个连续这样的操作,把这些操作放入队列,逐一处理(类似于fragment的处理方式)
initAdapterManager();
//初始化ChildHelper,用于管理子View,添加、删除item的View等
initChildrenHelper();
initAutofill();
// If not explicitly specified this view is important for accessibility.
if (ViewCompat.getImportantForAccessibility(this)
== ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
ViewCompat.setImportantForAccessibility(this,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
}
mAccessibilityManager = (AccessibilityManager) getContext()
.getSystemService(Context.ACCESSIBILITY_SERVICE);
setAccessibilityDelegateCompat(new RecyclerViewAccessibilityDelegate(this));
// Create the layoutManager if specified.
//默认是支持嵌套滑动的
boolean nestedScrollingEnabled = true;
//从xml中 获取属性值
if (attrs != null) {
int defStyleRes = 0;
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView,
defStyle, defStyleRes);
String layoutManagerName = a.getString(R.styleable.RecyclerView_layoutManager);
int descendantFocusability = a.getInt(
R.styleable.RecyclerView_android_descendantFocusability, -1);
if (descendantFocusability == -1) {
setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
}
mEnableFastScroller = a.getBoolean(R.styleable.RecyclerView_fastScrollEnabled, false);
if (mEnableFastScroller) {
StateListDrawable verticalThumbDrawable = (StateListDrawable) a
.getDrawable(R.styleable.RecyclerView_fastScrollVerticalThumbDrawable);
Drawable verticalTrackDrawable = a
.getDrawable(R.styleable.RecyclerView_fastScrollVerticalTrackDrawable);
StateListDrawable horizontalThumbDrawable = (StateListDrawable) a
.getDrawable(R.styleable.RecyclerView_fastScrollHorizontalThumbDrawable);
Drawable horizontalTrackDrawable = a
.getDrawable(R.styleable.RecyclerView_fastScrollHorizontalTrackDrawable);
initFastScroller(verticalThumbDrawable, verticalTrackDrawable,
horizontalThumbDrawable, horizontalTrackDrawable);
}
a.recycle();
//创建LayoutManager,如果在xml中设置了LayoutManager ,这里会通过反射成功创建对应的LayoutManager
createLayoutManager(context, layoutManagerName, attrs, defStyle, defStyleRes);
if (Build.VERSION.SDK_INT >= 21) {
a = context.obtainStyledAttributes(attrs, NESTED_SCROLLING_ATTRS,
defStyle, defStyleRes);
nestedScrollingEnabled = a.getBoolean(0, true);
a.recycle();
}
} else {
setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
}
//设置是否支持嵌套滑动
// Re-set whether nested scrolling is enabled so that it is set on all API levels
setNestedScrollingEnabled(nestedScrollingEnabled);
}
下面我们来到View绘制流程的,RecyclerView把子View的measure和layout的交给LayoutManager,这样就可以方便定制布局。
在measure阶段,可能会调用LayoutManager的dispatchLayoutStep1() dispatchLayoutStep2()
,这事为了计算出wrap_content的recycleView的大小
在layout阶段 ,分三个步骤 dispatchLayoutStep1() dispatchLayoutStep2() dispatchLayoutStep3()
对应的状态 STEP_START、STEP_LAYOUT 、 STEP_ANIMATIONS
- STEP_START 即将开始布局,此时会调用dispatchLayoutStep1,完成后 ,布局状态变为STEP_LAYOUT
- STEP_LAYOUT 真正开始布局,此时会调用dispatchLayoutStep2,会调用到layoutManager 中的layout,完成后,状态变为STEP_ANIMATIONS
- STEP_ANIMATIONS 执行动画,此时会调用dispatchLayoutStep3,
这三个函数会在onLayout 的时候分析,下面先来看看onMeasure
onMeasure
代码二
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
//是否开启自动测量,如果true,就交给RecyclerView 去测量,如果false ,就由LayoutManager去测量。
//现在自带的LayoutManager,都是返回true,例如 LinearLayoutManager
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.
*/
//调用RecycleView 中的defaultOnMeasure,此处主要是设置measure的宽高模式,代码三分析
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
//宽高的测量模式是否都是精确的
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {
//如果宽高的测量模式都是精确的,宽高不受子View 的影响,所以就直接返回
return;
}
//代码执行到这里说明RecycleView的宽高 至少一个是wrap_content,
//那么就需要measure、layout 子View后才能确定RecycleView的MeasureSpec
//
// 调用第一步layout,准备测量子View
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
// set dimensions in 2nd step. Pre-layout should happen with old dimensions for
// consistency
//子View的measure,layout 是在layoutManager中进行的,所以需要把RecycleView的MeasureSpec 传递过去
//在最终完成测量布局后,layoutManager中保存的RecycleView 的测量规则值可能会失效(详见下面疑问1)
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
//调用第二步layout,测量子View
dispatchLayoutStep2();
//此时子View measure,layout完成,把可容纳这些View的宽高值 设置到RecycleView。代码四分析
//这里有一个疑问,见下面的疑问2
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
// if RecyclerView has non-exact width and height and if there is at least one child
// which also has non-exact width & height, we have to re-measure.
if (mLayout.shouldMeasureTwice()) {
mLayout.setMeasureSpecs(
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
} else {
}
}
看下代码 mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
做了些什么事情
代码三
// 该函数是在LayoutManager 中
//widthSpec、heightSpec 是RecyclerView的测量规格
public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec,
int heightSpec) {
mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
}
// 该函数是在RecyclerView 中
//根据测量规则计算 宽高值,并设置到RecyclerView的宽高中
void defaultOnMeasure(int widthSpec, int heightSpec) {
// calling LayoutManager here is not pretty but that API is already public and it is better
// than creating another method since this is internal.
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 中
//由上面的调用可知,desired 是padding值
public static int chooseSize(int spec, int desired, int min) {
final int mode = View.MeasureSpec.getMode(spec);
final int size = View.MeasureSpec.getSize(spec);
switch (mode) {
case View.MeasureSpec.EXACTLY:
return size;
case View.MeasureSpec.AT_MOST:
//注意,如果是AT_MOST,RecyclerView的还需要测量布局其子View,才能最终确定
//所以这里返回最小值,,并不能决定最终的RecyclerView的尺寸
return Math.min(size, Math.max(desired, min));
case View.MeasureSpec.UNSPECIFIED:
default:
return Math.max(desired, min);
}
}
继续来看一下onMeasure中调用的setMeasuredDimensionFromChildren
代码四
void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) {
final int count = getChildCount();
if (count == 0) {
//如果子View 测量完了,数量是0,设置RecyclerView的测量规格,就不用考虑子View的位置,使用测量规格
mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
return;
}
//top,取最小值,bottom 取最大值
//left 取最小值,right 取最大值
//这样才能最大限度把所有子View都显示出来
int minX = Integer.MAX_VALUE;
int minY = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int maxY = Integer.MIN_VALUE;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
final Rect bounds = mRecyclerView.mTempRect;
//需要考虑item 直接的装饰线
getDecoratedBoundsWithMargins(child, bounds);
if (bounds.left < minX) {
minX = bounds.left;
}
if (bounds.right > maxX) {
maxX = bounds.right;
}
if (bounds.top < minY) {
minY = bounds.top;
}
if (bounds.bottom > maxY) {
maxY = bounds.bottom;
}
}
//得到容纳当前所有子View的尺寸,也就是RecyclerView的最大尺寸
mRecyclerView.mTempRect.set(minX, minY, maxX, maxY);
setMeasuredDimension(mRecyclerView.mTempRect, widthSpec, heightSpec);
}
public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) {
int usedWidth = childrenBounds.width() + getPaddingLeft() + getPaddingRight();
int usedHeight = childrenBounds.height() + getPaddingTop() + getPaddingBottom();
//根据最大的尺寸和测量模式计算出 最终的测量规格,
int width = chooseSize(wSpec, usedWidth, getMinimumWidth());
int height = chooseSize(hSpec, usedHeight, getMinimumHeight());
setMeasuredDimension(width, height);
}
疑问1:在dispatchLayoutStep2() 中measure,layout子View 的时候,会使用到RecycleView的MeasureSpec,而该值是在dispatchLayoutStep2()之前就已经传入layoutManager了,dispatchLayoutStep2() 之后又要重新设置RecycleView的MeasureSpec, 那么是不是对子View UI的确定 就会有问题呢?因为确定子View 的时候,可能使用了不正确的RecycleView的MeasureSpec
要理清这个问题,需要先知道,在此时的代码RecycleView的宽高至少有一个是wrap_content,也就是不确定的。假设这样的情况,屏幕上的布局RecycleView 最大可容纳10个item,此时只有2个item,RecycleView的高度设置wrap_content
分4步简单介绍一下
1、在执行到RecycleView的onMeasure(), 父View会给出尽可能大的size,
2、RecycleView 把子View measure,layout完成后,再去设置自己具体的宽高值,
3、父View(这里假设为RelativeLayout) 在根据具体的RecycleView宽高值,设置RecycleView的LayoutParams
4、到layout阶段,就根据RecycleView的LayoutParams,去设置top,bottom,left,right 的值
这个问题大概分析到这里,下面贴一下主要的代码
-
对应步骤1
下面是RelativeLayout的onMeasure 函数,其中调用了measureChild 会调用到RecycleView的onMeasure ()
-
对应步骤3
进入到RelativeLayout的positionChildVertical 函数,把RecycleView的LayoutParams 进行调整,注意图中断点处的child.getMeasureHeight 获取到的是最新的RecycleView的尺寸(就是步骤2 执行完后,能容纳子View的宽高)
- 对应步骤4
这里执行到RelativeLayout的onLayout 函数,看到在设置RecycleView的top,bottom,left,right 的值,使用的是LayoutParams的值,也就是新得到的RecycleView尺寸
疑问2:为什么需要子View measure,layout都完成后,才能确定RecycleView的宽高呢?
想想网格布局和瀑布布局
注意,此时LayoutManager中的保存RecycleView的宽高值 和 当前RecycleView 的宽高值 ,就不一样了。这个很关键,正因为这两个值不一样,所以下面的dispatchLayout 阶段,需要重新执行一次dispatchLayoutStep2() 。下面来分析绘制流程的onLayout
onLayout
代码五
@Override
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) {
//STEP_START,需要从第一步layout 执行
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.
//如果是wrap_content 会进入到这个分支,
//此时把LayoutManager中的保存RecycleView的测量模式设为精确,因为在LayoutManager布局的时候,无法改变RecycleView的大小
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
在上面介绍过,Layout被分为三个阶段, dispatchLayoutStep1() dispatchLayoutStep2() dispatchLayoutStep3()
,下面来逐一分析这几个函数
dispatchLayoutStep1()
代码六
private void dispatchLayoutStep1() {
mState.assertLayoutStep(State.STEP_START);
fillRemainingScrollValues(mState);
mState.mIsMeasuring = false;
//开启拦截 RequestLayout,避免多余的RequestLayout
startInterceptRequestLayout();
//记录View的布局前后状态,第三阶段根据状态变化,来最后执行动画。这里先清除
mViewInfoStore.clear();
onEnterLayoutOrScroll();
//执行队列任务,notifyItemXXXXX 这系列函数,产生的任务。例如:增加了一个item等
//mRunSimpleAnimations、mRunPredictiveAnimations 这两个值 也是由此计算出来的
processAdapterUpdatesAndSetAnimationFlags();
// 查找焦点view并设置mState焦点相关变量
saveFocusInfo();
mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
mItemsAddedOrRemoved = mItemsChanged = false;
mState.mInPreLayout = mState.mRunPredictiveAnimations;
//获取Adapter 中item的数量
mState.mItemCount = mAdapter.getItemCount();
//获取View最小和最大的布局索引位置 ,保存在mMinMaxLayoutPositions 数组中
findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);
if (mState.mRunSimpleAnimations) {
//第一次layout 肯定不会到这个分支,在完整布局过一次后,该变量才可能为true。
//该分支主要是记录view 布局前的状态,为后续动画做准备
// 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));
....省略 代码...
//使用mItemAnimator 获取一个ItemHolderInfo 变量,该变量记录了Item在layout之前的边界
final ItemHolderInfo animationInfo = mItemAnimator
.recordPreLayoutInformation(mState, holder,
ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
holder.getUnmodifiedPayloads());
//以holder为键,把animationInfo 保存起来
//mViewInfoStore 主要是保存动画View 的前后状态
//在dispatchLayoutStep3(),根据最终的状态,来执行动画。
mViewInfoStore.addToPreLayout(holder, animationInfo);
....省略 代码...
}
}
if (mState.mRunPredictiveAnimations) {
....省略 代码...
} else {
// 第一次布局(还没有一次完整的布局) ,会执行到这里
clearOldPositions();
}
onExitLayoutOrScroll();
//停止 拦截RequestLayout
stopInterceptRequestLayout(false);
mState.mLayoutStep = State.STEP_LAYOUT;
}
dispatchLayoutStep2()
这里是真正的布局,会调用到LayoutManager中
代码七
private void dispatchLayoutStep2() {
startInterceptRequestLayout();
onEnterLayoutOrScroll();
mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
mAdapterHelper.consumeUpdatesInOnePass();
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
// Step 2: Run layout
mState.mInPreLayout = false;
//调用到layoutManager中,实现真正的布局
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;
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
}
关于这部分的源码分析,网上虽然也有,不仅没有分析透彻,而且还是错误的(这篇文章 关于锚点的不够全面,但我相信该作者理解原理,可能是大意了。这篇文章,关于锚点的描述,是直接把英文文档翻译了一下,fill towards end 翻译成 结束填充,不知道要误导多少人了。)
下面以LinearLayoutManager 分析一下onLayoutChildren 函数的作用,首先找到一个锚点(数据集的索引mPosition,屏幕的像素位置mCoordinate),以这个点为基础,按照指定方向(start 或end)来填充item
那么这个点是如何找到的呢?分为三种情况:
- 初始的时候,这个点mPosition = 0,mCoordinate = 0,也就是说初始就是按照一个方向,LinearLayoutManager 默认为end,从上到下填充item。
- 滚动到指定索引位置(例如,调用scrollToPosition),这里又分为两种情况
- 2.1. 目标位置mPosition 已经显示在当前屏幕 ,那么计算出该位置的屏幕位置mCoordinate,因为该位置已经在屏幕上显示了,所以需要分别填充它的上面和下面的item,也就是每个分支中 fill 会被调用两次的原因
- 2.2. 目标位置mPosition 没有显示在当前屏幕,那么计算出该位置的屏幕位置mCoordinate,因为没有显示到屏幕,所以就设置它的mCoordinate为最上(需要向下滚动)或者最下(需要向上滚动),此时只要一次fill填充,就能完成。在执行下一个fill的时候,不符合条件 会直接在退出
- 在已有的item中选择,如果有item 正在获得焦点,就优先考虑它是否可以成为锚点,否则根据mLayoutFromEnd, 选择一个最靠近start 或end的item,
mLayoutFromEnd 的意识就是 布局是否从End方向开始。在选择锚点的时候,该变量是这样赋值的mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
mShouldReverseLayout
表示 列表是否翻转,例如 原来是顺序 ,改为倒序
mStackFromEnd
表示 item的堆叠方向是否从End->Start,改变该值不影响列表的顺序,原来item 在列表中是第几个,该值改变后还是第几个。该值决定了,在屏幕上最终看到的是前几个item ,还是后几个item。
这两个值互相组合,有四种情况,上图,图中粉色的线,为锚点 所在的值。锚点不应该是一个点吗,因为本例是垂直方向的LinearLayoutManger,锚点是Y轴上的值,所以图中使用了横向的线来表示
下面就在代码中来一探究竟
代码八
@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.
// 2) fill towards start, stacking from bottom
// 3) fill towards end, stacking from top
// 4) scroll to fulfill requirements like stack from bottom.
// create layout state
... 省略代码...
//从保存的待定状态中 设置待定的滚动位置 mPendingScrollPosition。如果有值,那么在获取锚点的时候,就会以此为准
if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
}
... 省略代码...
final View focused = getFocusedChild();
//mValid 在初次或者一次完整的绘制完成,该值等于false
if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
|| mPendingSavedState != null) {
mAnchorInfo.reset();
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// calculate anchor position and coordinate
//上面分析的三点,就是个函数里
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
} else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
>= mOrientationHelper.getEndAfterPadding()
|| mOrientationHelper.getDecoratedEnd(focused)
<= mOrientationHelper.getStartAfterPadding())) {
... 省略代码...
}
... 省略代码...
//这两个extra 的值,代表了可是额外使用的空间,虽然可以绘制,但是用户是看不到的。 这部分看不到的子View,为什么还需要绘制呢? 例如,为了即将到来的平滑滚动
int extraForStart;
int extraForEnd;
//getExtraLayoutSpace这个函数会判断是否有平滑滚动,如果有,那么额外空间,就给当前RecycleView的可绘制空间。否则为0
//这个额外空间是否给大了?下面可能还会根据即将滚动的位置进行调整
final int extra = getExtraLayoutSpace(state);
// If the previous scroll delta was less than zero, the extra space should be laid out
// at the start. Otherwise, it should be at the end.
if (mLayoutState.mLastScrollDelta >= 0) {
extraForEnd = extra;
extraForStart = 0;
} else {
extraForStart = extra;
extraForEnd = 0;
}
extraForStart += mOrientationHelper.getStartAfterPadding();
extraForEnd += mOrientationHelper.getEndPadding();
//下面这部分代码,根据是否预布局和即将要滚动到的位置,来调整,这个需要绘制的额外空间(也就是屏幕可用绘制以外的空间)
if (state.isPreLayout() && mPendingScrollPosition != RecyclerView.NO_POSITION
&& mPendingScrollPositionOffset != INVALID_OFFSET) {
// if the child is visible and we are going to move it around, we should layout
// extra items in the opposite direction to make sure new items animate nicely
// instead of just fading in
final View existing = findViewByPosition(mPendingScrollPosition);
if (existing != null) {
final int current;
final int upcomingOffset;
if (mShouldReverseLayout) {
current = mOrientationHelper.getEndAfterPadding()
- mOrientationHelper.getDecoratedEnd(existing);
upcomingOffset = current - mPendingScrollPositionOffset;
} else {
current = mOrientationHelper.getDecoratedStart(existing)
- mOrientationHelper.getStartAfterPadding();
upcomingOffset = mPendingScrollPositionOffset - current;
}
if (upcomingOffset > 0) {
extraForStart += upcomingOffset;
} else {
extraForEnd -= upcomingOffset;
}
}
}
... 省略代码...
//回收当前可见的view到mAttachedScrap中,这样等下就可以直接复用,不需要重建了
detachAndScrapAttachedViews(recycler);
if (mAnchorInfo.mLayoutFromEnd) {
//这里的逻辑和else分支的逻辑大体相同,只是调用顺序有所调整,因为一个 是从底向顶,一个是从顶向底
... 省略代码...
} else {
// fill towards end
//设置布局的一些信息,例如 方向,可用空间等。下面会详细分析
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
//获取或创建 子View,然后进行addview,就添加到RecycleView中了
fill(recycler, mLayoutState, state, false);
//这个偏移量,就是锚点的值(例如:垂直方向的LinearLayoutManager ,就是Y 轴方向的值)
endOffset = mLayoutState.mOffset;
//锚点 的 数据集对应的索引
final int lastElement = mLayoutState.mCurrentPosition;
//一次填充后,屏幕的可用空间 还有剩余。那就加入到extraForStart,
// 见下面 疑惑1
if (mLayoutState.mAvailable > 0) {
extraForStart += mLayoutState.mAvailable;
}
// fill towards start
//比较常见的调用到这里的,就是上面关于锚点寻找2.1 的情况
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
//这里的mItemDirection 会根据绘制方向,为 1 或 -1
//绘制完 第n个及以下的view,如果需要向上绘制,就需要从第n-1开始。此时mItemDirection 就应该是-1 。
//该值是在updateLayoutStateToFillxxx中 赋值的
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
extraForEnd = mLayoutState.mAvailable;
// start could not consume all it should. add more items towards end
updateLayoutStateToFillEnd(lastElement, endOffset);
mLayoutState.mExtra = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
}
}
// changes may cause gaps on the UI, try to fix them.
// TODO we can probably avoid this if neither stackFromEnd/reverseLayout/RTL values have
// changed
if (getChildCount() > 0) {
// because layout from end may be changed by scroll to position
// we re-calculate it.
// find which side we should check for gaps.
if (mShouldReverseLayout ^ mStackFromEnd) {
int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true);
startOffset += fixOffset;
endOffset += fixOffset;
fixOffset = fixLayoutStartGap(startOffset, recycler, state, false);
startOffset += fixOffset;
endOffset += fixOffset;
} else {
int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);
startOffset += fixOffset;
endOffset += fixOffset;
fixOffset = fixLayoutEndGap(endOffset, recycler, state, false);
startOffset += fixOffset;
endOffset += fixOffset;
}
}
layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
if (!state.isPreLayout()) {
mOrientationHelper.onLayoutComplete();
} else {
mAnchorInfo.reset();
}
mLastStackFromEnd = mStackFromEnd;
}
updateLayoutStateToFillEnd 和 updateLayoutStateToFillStart 内部的逻辑基本是对称的
代码九
private void updateLayoutStateToFillEnd(AnchorInfo anchorInfo) {
updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate);
}
private void updateLayoutStateToFillEnd(int itemPosition, int offset) {
//获取可用的空间,例如垂直方向的linearLayoutManager,就是y轴上的可用像素大小
mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;
//根据是否翻转列表,来设置该值 1,或-1,这样在获取下一个item的时候,都只要 加上该值就可以了。
mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD :
LayoutState.ITEM_DIRECTION_TAIL;
mLayoutState.mCurrentPosition = itemPosition;
mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END;
//当前item的像素位置,例如垂直方向的linearLayoutManager,就是y轴上像素的值
mLayoutState.mOffset = offset;
mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
}
fill 函数是实现view的填充的,根据屏幕的大小 ,来判断填充多少,它的返回是填充了多少像素(在LinearLayoutManager中指在垂直或横向 填充了多少像素),该数值可能会大于屏幕的像素。例如最后一个item,只显示了一部分
代码十
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// max offset we should set is mFastScroll + available
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// TODO ugly bug fix. should not happen
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
//回收view,那些滚动后 出界的子view
recycleByLayoutState(recycler, layoutState);
}
//得到可用空间
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
//循环填充子View,三个判断条件,每添加一个子View,remainingSpace会减去相应的像素值。
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
//这里是真正获取子View,Measure,layout的地方,下面会分析
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (layoutChunkResult.mFinished) {
break;
}
//累计偏移量,在指定的方向的
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
/**
* Consume the available space if:
* * layoutChunk did not request to be ignored
* * OR we are laying out scrap children
* * OR we are not doing pre-layout
*/
if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
remainingSpace -= layoutChunkResult.mConsumed;
}
... 省略代码...
}
//返回总共填充了多少像素,在期望的方向上
return start - layoutState.mAvailable;
}
接着看一下,添加子View的函数 layoutChunk 中看看,每执行一次就添加了一个子View
代码十一
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
// 从recycler 中获取一个View,这个view 可以是新建的,也可能是从CacheView或 RecycleViewPool中获取的,
//其中会执行createViewHolder,binderViewHolder
//关于View 缓存的源码,会在下一篇分析
View view = layoutState.next(recycler);
... 省略代码...
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
//添加子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);
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
int left, top, right, bottom;
//根据绘制的方向,来确定子view的位置
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;
}
}
//最终进行layout,
// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
layoutDecoratedWithMargins(view, left, top, right, bottom);
// Consume the available space if the view is not removed OR changed
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable = view.hasFocusable();
}
dispatchLayoutStep2() 就算分析完了,下面来看看dispatchLayoutStep3()
dispatchLayoutStep3()
代码十二
private void dispatchLayoutStep3() {
mState.assertLayoutStep(State.STEP_ANIMATIONS);
startInterceptRequestLayout();
onEnterLayoutOrScroll();
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);
//还记得,在dispatchLayoutStep1()中调用类似的函数,来获得ItemHolderInfo
// 获取holder的边界信息,这里是布局完成后的
final ItemHolderInfo animationInfo = mItemAnimator
.recordPostLayoutInformation(mState, holder);
ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
//以holder为键,把animationInfo 保存到 mLayoutHolderMap,这样就有了该View布局前和布局后的状态
if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
// run a change animation
... 省略代码...
mViewInfoStore.addToPostLayout(holder, animationInfo);
... 省略代码...
} else {
mViewInfoStore.addToPostLayout(holder, animationInfo);
}
}
//有了View的前后状态,就开始执行动画
// Step 4: Process view info lists and trigger animations
mViewInfoStore.process(mViewInfoProcessCallback);
}
//还记得在onLayoutChildren中把可见的view 都缓存到mAttachedScrap
//现在布局完成了,该复用的都复用了,就不需要这一级的缓存了,这里清除掉
mLayout.removeAndRecycleScrapInt(mRecycler);
mState.mPreviousLayoutItemCount = mState.mItemCount;
mDataSetHasChangedAfterLayout = false;
mDispatchItemsChangedEvent = false;
mState.mRunSimpleAnimations = false;
mState.mRunPredictiveAnimations = false;
mLayout.mRequestedSimpleAnimations = false;
if (mRecycler.mChangedScrap != null) {
mRecycler.mChangedScrap.clear();
}
//在滑动屏幕的时候,触发GapWorker 任务,其中会设置到这个值。在那里把缓存的数量了提升了,
//这里就根据该值,把缓存数量降下来
if (mLayout.mPrefetchMaxObservedInInitialPrefetch) {
// Initial prefetch has expanded cache, so reset until next prefetch.
// This prevents initial prefetches from expanding the cache permanently.
mLayout.mPrefetchMaxCountObserved = 0;
mLayout.mPrefetchMaxObservedInInitialPrefetch = false;
mRecycler.updateViewCacheSize();
}
//一次完整layout 执行完成
mLayout.onLayoutCompleted(mState);
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
mViewInfoStore.clear();
if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1])) {
dispatchOnScrolled(0, 0);
}
recoverFocusFromState();
resetFocusInfo();
}
上面涉及到的几个函数,就不展开了,内容都很简单,对照源码 看一下,就明白了
只能说分析了大体流程,虽然我觉得比网上的文章要深入一些,但是距离100%解析,还是有点距离,不过整体轮廓已经出来了,有兴趣的朋友可以继续把细节钻一钻。
setLayoutManager
如果没有在xml中指定LayoutManager,那么需要在代码中设置recyclerView.setLayoutManager(new LinearLayoutManager(this));
下面来看一下 setLayoutManager
代码十三
RecyclerView.java
public void setLayoutManager(@Nullable LayoutManager layout) {
if (layout == mLayout) {
return;
}
//停止滑动
stopScroll();
// TODO We should do this switch a dispatchLayout pass and animate children. There is a good
// chance that LayoutManagers will re-use views.
//之前是否已经设置过LayoutManager
if (mLayout != null) {
// end all running animations
if (mItemAnimator != null) {
mItemAnimator.endAnimations();
}
//mRecycler 是用来管理子View的,缓存、删除、获取都会调用它
//删除所有的view
mLayout.removeAndRecycleAllViews(mRecycler);
//删除缓存的Scrap view
mLayout.removeAndRecycleScrapInt(mRecycler);
mRecycler.clear();
if (mIsAttached) {
mLayout.dispatchDetachedFromWindow(this, mRecycler);
}
mLayout.setRecyclerView(null);
mLayout = null;
} else {
//删除缓存AttachedSrap、CachedView 缓存,把CachedView 的缓存加入到RecycledViewPool中
//关于缓存的会在下一篇文章详细分析
mRecycler.clear();
}
// this is just a defensive measure for faulty item animators.
mChildHelper.removeAllViewsUnfiltered();
mLayout = layout;
if (layout != null) {
if (layout.mRecyclerView != null) {
throw new IllegalArgumentException("LayoutManager " + layout
+ " is already attached to a RecyclerView:"
+ layout.mRecyclerView.exceptionLabel());
}
//把RecyclerView、及宽高、mChildHelper 设置到mLayout 中
mLayout.setRecyclerView(this);
if (mIsAttached) {
mLayout.dispatchAttachedToWindow(this);
}
}
//设置CacheView 缓存的数量,默认为2。可调用setViewCacheSize来设置
mRecycler.updateViewCacheSize();
//重新布局,就会走onMeasure,onLayout流程
requestLayout();
}
setAdapter
代码十四
public void setAdapter(@Nullable Adapter adapter) {
// bail out if layout is frozen
setLayoutFrozen(false);
//设置adapter
setAdapterInternal(adapter, false, true);
//adapter 发生变化,需要通过Recycler 把所有的ViewHolder回收。并给这些ViewHolder设置无效标志位
processDataSetCompletelyChanged(false);
requestLayout();
}
下面来分析一个setAdapterInternal的逻辑
代码十五
private void setAdapterInternal(@Nullable Adapter adapter, boolean compatibleWithPrevious,
boolean removeAndRecycleViews) {
if (mAdapter != null) {
//mAdapter 不为null ,说明之前已经设置过了,需要取消注册的观察者
mAdapter.unregisterAdapterDataObserver(mObserver);
//Adapter 和 Recycleview分离,这是一个生命周期性的函数,看自定义覆盖,源码中该函数为空
mAdapter.onDetachedFromRecyclerView(this);
}
if (!compatibleWithPrevious || removeAndRecycleViews) {
//根据上面传入的参数,这里一定会执行,删除和回收ViewHolder
removeAndRecycleViews();
}
//情况任务队列,这里任务,例如 增加,删除View 等
mAdapterHelper.reset();
final Adapter oldAdapter = mAdapter;
mAdapter = adapter;
if (adapter != null) {
//代码十六会分析
adapter.registerAdapterDataObserver(mObserver);
//这是一个生命周期性的函数,看自定义覆盖,源码中该函数为空
adapter.onAttachedToRecyclerView(this);
}
if (mLayout != null) {
mLayout.onAdapterChanged(oldAdapter, mAdapter);
}
mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
mState.mStructureChanged = true;
}
代码十六
在Adapter 类中
//每个adapter中都有的变量,可被观察者,把他注册到观察者
private final AdapterDataObservable mObservable = new AdapterDataObservable();
//observer 是AdapterDataObserver类型,它的实现类是RecyclerViewDataObserver
public void registerAdapterDataObserver(@NonNull AdapterDataObserver observer) {
//把观察者observe 注册到 被观察者mObservable中的mObservers列表中,
//也就是说一个被观察者,可以有多个观察者,这样用户就可以自定义这些事件的处理方式
mObservable.registerObserver(observer)
}
下面来看一下这个观察者RecyclerViewDataObserver 和 被观察者 AdapterDataObservable
代码十七
static class AdapterDataObservable extends Observable<AdapterDataObserver> {
public boolean hasObservers() {
return !mObservers.isEmpty();
}
... 省略代码...
public void notifyChanged() {
// since onChanged() is implemented by the app, it could do anything, including
// removing itself from {@link mObservers} - and that could cause problems if
// an iterator is used on the ArrayList {@link mObservers}.
// to avoid such problems, just march thru the list in the reverse order.
//可以看到遍历所有的观察者,调用它们的函数
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onChanged();
}
}
... 省略代码...
}
代码十八
private class RecyclerViewDataObserver extends AdapterDataObserver {
RecyclerViewDataObserver() {
}
... 省略代码...
//触发观察者的事件
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
assertNotInLayoutOrScroll(null);
// mAdapterHelper 中的Pending Updates 队列 == 1,就立即执行,
// 如果大于1,说明有任务正在执行,直接加入到队列
if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) {
triggerUpdateProcessor();
}
}
... 省略代码...
//触发布局,执行动画
//下面的详细步骤,会在后面的文章分析
void triggerUpdateProcessor() {
if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
} else {
mAdapterUpdateDuringMeasure = true;
requestLayout();
}
}
}
代码十九
在AdapterHelper.java 中
boolean onItemRangeInserted(int positionStart, int itemCount) {
if (itemCount < 1) {
return false;
}
//在待更新任务列表中 ,加入该类型的操作
mPendingUpdates.add(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount, null));
mExistingUpdateTypes |= UpdateOp.ADD;
//如果任务列表数量 为1 ,返回true。需要立即执行的,该判断为了避免重复触发处理操作。
return mPendingUpdates.size() == 1;
}
熟悉并发编程的朋友,一定能马上意识到,这里mPendingUpdates.size() == 1;
是否有问题,如果mPendingUpdates 里的任务已经处理完成,在要清理的时候,切换了线程,而此时加入一个新的任务后,改语句返回false,也就是无法触发执行操作。以后再继续增加,岂不是一直不能触发执行了。 我翻了半天源码没有找到线程同步的逻辑,后来看到triggerUpdateProcessor()
的else分支的调用了requestLayout();
,说明mPendingUpdates的增加,是在主线程。而是mPendingUpdates的任务执行 ,也是在主线程。所以这里不会有线程安全问题