深入分析RecyclerView源码——布局流程(上)

开篇简述

RecyclerView的源码数量真的是太多啦,第一次解析打算按照分析view的标准方法,先从measure、layout和draw三大流程入手。本篇文章主要着眼于最核心的measure和layout过程。分析后会发现RecyclerView实际上是把两个过程分由三个函数dispatchLayoutStep1,dispatchLayoutStep2和dispatchLayoutStep3去做了,onMeasure和onLayout的区别并不明显。d1、d2、d3三函数主要负责处理预布局、布局和动画,其中的布局就是一般情况下ViewGroup的测量和布局过程合二为一,这部分RecyclerView交给了布局管理器LayoutManager的onLayoutChildren方法完成,源码中是mLayout,以下的分析均基LinearLayoutManager,它是LayoutManager的子类。除此之外,本篇还将努力理清预布局和动画的流程,还原RecyclerView布局流程的全过程。

onMeasure

onMeasure函数很简单,如果RecyclerView的width和height不都是EXACTLY,就在onMeasue里执行布局,并且有可能需要执行两遍。如果width和height都是EXACTLY,跳过onMeasure直接进入onLayout。所以onMeasure和onLayout的仅仅是对于非EXACTLY和EXACTLY的不同策略,实质上都同时承担了测量和布局任务,对于ViewGroup来说测量和布局往往是杂糅在一起的,之前分析过线性布局LinearLayout的源码对此深有体会。

@Override
protected void onMeasure(int widthSpec, int heightSpec) {
    //mLayout就是我们设置的布局管理器,如果没有设置的话就走defaultOnMeasure
    //defaultOnMeasure只是简单应用widthSpec和heightSpec,其他什么都不做,等于放弃布局
    if (mLayout == null) {
        defaultOnMeasure(widthSpec, heightSpec);
        return;
    }
    //如果设置了布局管理器,返回值必为true
    if (mLayout.isAutoMeasureEnabled()) {
        final int widthMode = MeasureSpec.getMode(widthSpec);
        final int heightMode = MeasureSpec.getMode(heightSpec);
        //布局管理器并不实现该方法,走的还是defaultOnMeasure,所以没什么用
        mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            
        final boolean measureSpecModeIsExactly =
                widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
        //如果width和height都是绝对值直接结束onMeasure函数
        //初看可能比较懵,我开头说过了RecyclerView的measure和layout区别不明显
        //实际的过程是dispatchLayoutStep三兄弟实现的,所以延后到onLayout再调用也一样
        if (measureSpecModeIsExactly || mAdapter == null) {
            return;
        }
        //刚开始状态必为State.STEP_START,和dispatchLayoutStep三兄弟有关
        //有三个取值,分别代表开始、布局、动画
        //dispatchLayoutStep1执行完后变成STEP_LAYOUT,
        //dispatchLayoutStep2执行完后变成STEP_ANIMATIONS,
        //dispatchLayoutStep3执行完后变回STEP_START
        if (mState.mLayoutStep == State.STEP_START) {
            //三兄弟第一个出现了,d1是用来处理预布局的,后面会详细分析
            dispatchLayoutStep1();
        }
        //这句不是很懂,意思是SDK<23时不支持MeasureSpec.UNSPECIFIED
        mLayout.setMeasureSpecs(widthSpec, heightSpec);
        mState.mIsMeasuring = true;
        //三兄弟第二个出现了,d2是用来更新布局的,d2完成后
        //布局measure和layout过程就算完成了,后面会详细分析
        dispatchLayoutStep2();
        //根据子view的大小设置自身大小,因为自身的width或者height是不确定的
        //所以必须测完子view才能确定,当wrap_content的时候必须先知道content
        //多大才能wrap
        mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
        //如果需要测量两次,拿LinearLayoutManager举例,当父view是wrap_content
        //子view是match_parent时,必须先测一遍子view,然后设定父view,最后设定
        //match_parent的子view
        if (mLayout.shouldMeasureTwice()) {
            mLayout.setMeasureSpecs(
                    MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
            mState.mIsMeasuring = true;
            dispatchLayoutStep2();
            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
        }
    } else {
        //如果设置了布局管理器,是不可能走到这里的,以下省略一大段代码
        ......
    }
}

onLayout

onLayout只简单地调用dispatchLayout,直接看dispatchLayout。

void dispatchLayout() {
    //没有适配器和布局管理器就不用往下走了
    if (mAdapter == null) {
        Log.e(TAG, "No adapter attached; skipping layout");
        return;
    }
    if (mLayout == null) {
        Log.e(TAG, "No layout manager attached; skipping layout");
        return;
    }
    mState.mIsMeasuring = false;
    //这里判断了mState.mLayoutStep,如果在onMeasure中因为宽高是EXACTLY直接结束的话
    //就还是State.STEP_START,否则应该是State.STEP_ANIMATIONS
    if (mState.mLayoutStep == State.STEP_START) {
        //三兄弟之一,执行预布局
        dispatchLayoutStep1();
        //此处不需要测量两遍是因为只有宽高固定才会进来这
        mLayout.setExactMeasureSpecsFrom(this);
        //三兄弟之二,执行布局
        dispatchLayoutStep2();
    } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
            || mLayout.getHeight() != getHeight()) {
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else {
        mLayout.setExactMeasureSpecsFrom(this);
    }
    //三兄弟之三,处理动画
    dispatchLayoutStep3();
}

dispatchLayoutStep1

以下dispatchLayoutStep1为删减版,挑选出比较关键的步骤分析。d1的任务是是执行PreLayout预布局过程。设想一个场景,对于线性布局的RecyclerView,如果删除了其中的某个子view,将导致后面的所有子view向上平移,并且会新加入一个子view在屏幕最后,默认情况下这个新加入的子view也是通过平移动画进入,所以在进行动画之前必须知道它的初始位置即从哪里平移,但是此时该view并没有初始位置,于是d1通过预布局解决这个问题。当RecyclerView有n个子view发生变化或删除时,会在d1额外布局n个子view在屏幕外为动画做准备。详细过程见源码和注释。

private void dispatchLayoutStep1() {
	startInterceptRequestLayout();
	//mViewInfoStore是存储屏幕上view的动画的,先清空
	mViewInfoStore.clear();
	//adapter执行notifyItemXXX方法时会给需要变化的view附加更新信息,
	//此时进行处理,具体见adapter和recyclerView的关系部分
	processAdapterUpdatesAndSetAnimationFlags();
	//此时mState.mInPreLayout变为true,代表预布局(PreLayout)正式开始
	mState.mInPreLayout = mState.mRunPredictiveAnimations;
	//第一个if是为屏幕上显示的view附加动画信息
	if (mState.mRunSimpleAnimations) {
		int count = mChildHelper.getChildCount();
		for (int i = 0; i < count; ++i) {
			final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
			//只是附加了位置信息,真正添加动画应该是在别的地方,
			//此处的位置是原始位置,即界面更新前的老位置,因为目前还未执行布局操作
			final ItemHolderInfo animationInfo = mItemAnimator
				.recordPreLayoutInformation(mState, holder,
				ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
				holder.getUnmodifiedPayloads());
			//开始布局前的所有屏幕上的view添加旧位置信息到mViewInfoStore的mLayoutHolderMap容器中,flag为FLAG_PRE
			mViewInfoStore.addToPreLayout(holder, animationInfo);
			if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
                        && !holder.shouldIgnore() && !holder.isInvalid()) {
                long key = getChangedHolderKey(holder);
				//更新却未删除的view会把位置信息加入到mOldChangedHolders容器中
				mViewInfoStore.addToOldChangeHolders(key, holder);
            }
		}
	}
	//第二个if是为在屏幕外的PreLayout的view附加动画信息
	if (mState.mRunPredictiveAnimations) {
		//recyclerView一共会执行两次onLayoutChildren,这是第一次,发生在PreLayout阶段
		//主要任务是如果有n个view发生变化,则多layout n个view在屏幕外面,为后面的进入动画做准备
		mLayout.onLayoutChildren(mRecycler, mState);
		for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
			final View child = mChildHelper.getChildAt(i);
            final ViewHolder viewHolder = getChildViewHolderInt(child);
			if (!mViewInfoStore.isInPreLayout(viewHolder)) {
				//只有在屏幕外的PreLayout的view会进来,animationInfo和前一个if中的是一样的,
				//也是只附加位置信息,此处位置是在屏幕外的
                final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(
                    mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
                if (wasHidden) {
                    recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
                } else {
					//添加位置信息(屏幕外)到mViewInfoStore的mLayoutHolderMap容器中,flag为FLAG_APPEAR
                    mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
					//至此第一次onLayoutChildren的view全部加进了mLayoutHolderMap容器中
                }
            }
		}
	}
	stopInterceptRequestLayout(false);
	//dispatchLayoutStep1执行完代表State.STEP_START阶段结束,进入State.STEP_LAYOUT阶段
    mState.mLayoutStep = State.STEP_LAYOUT;
}

可以看到d1主要做了三件事:

一、调用processAdapterUpdatesAndSetAnimationFlags,这个涉及到Adapter和RecyclerView的缓存策略,后面会单独开篇解析。

二、调用mLayout.onLayoutChildren进行预布局(d1、d2各会调用该函数一次但目的不同),保存子view位置信息,为动画做准备。其中之所以经过mLayout.onLayoutChildren后子view会变多,正是因为对于变化或删除的子view不考虑他们占的空间,从而能多列出n个子view到屏幕外。

三、d1还保存了所有子view的的原始位置信息,这些信息在d3中会再此用到,d3才是真正确定动画的地方。信息被保存到了mViewInfoStore的两个容器中,更新却不删除的view的被保存到mOldChangedHolders,其他被保存到mLayoutHolderMap。之所以这样区分是因为更新却不删除的view有可能需要执行缩放动画,其他view不需要。

dispatchLayoutStep2

由于dispatchLayoutStep2的源码比较少,所以就全部放出来了。抛开mLayout.onLayoutChildren,d2应该是三兄弟中最简单的,d2执行的就是ViewGroup常规的测量和布局操作,并且是委托给布局管理器实现的。经过d2整个界面就已经变成更新后的样子了,但是因为没有执行draw操作,所以现实屏幕上暂时还看不到。onLayoutChildren是一个非常重点的方法,但是本篇着重从全局把握布局流程,所以会单独再开一篇解析onLayoutChildren和RecyclerView的缓存策略。

private void dispatchLayoutStep2() {
    startInterceptRequestLayout();
    onEnterLayoutOrScroll();
    mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
	//走到最后都是空方法,官方并未实现
    mAdapterHelper.consumeUpdatesInOnePass();
    mState.mItemCount = mAdapter.getItemCount();
    mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

	//PreLayout预布局阶段结束,下面开始正式布局
    mState.mInPreLayout = false;
	//第二次onLayoutChildren,执行完毕后子view已经到达所有操作结束后的位置和大小,
	//该删除该变化的都删除变化了,该新进入屏幕的也进来了,
	//但是这只是measure和layout完成,没到draw所以现实屏幕上暂时看不到
    mLayout.onLayoutChildren(mRecycler, mState);

    mState.mStructureChanged = false;
    mPendingSavedState = null;

    mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
	//State.STEP_LAYOUT阶段结束,下面进入State.STEP_ANIMATIONS阶段,应该是要处理动画相关了
    mState.mLayoutStep = State.STEP_ANIMATIONS;
    onExitLayoutOrScroll();
    stopInterceptRequestLayout(false);
}

dispatchLayoutStep3

d3只是处理动画相关的一些操作,判断每个子view(包括刚刚被删除的view)是否需要动画需要何种动画(平移还是淡出)。在for循环中对每个子view在mViewInfoStore中存储的旧位置信息做判断,增加响应的flag以区分做何种动画。具体操作见源码和注释。

private void dispatchLayoutStep3() {
    mState.assertLayoutStep(State.STEP_ANIMATIONS);
    startInterceptRequestLayout();
    onEnterLayoutOrScroll();
    mState.mLayoutStep = State.STEP_START;
    if (mState.mRunSimpleAnimations) {
		//经过第二次onLayoutChildren,此处的所有holder都是界面更新后应该在屏幕上的
        for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
            ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
			//key实际上就是position
            long key = getChangedHolderKey(holder);
			//这里有两个信息,animationInfo是新的位置信息,oldChangeViewHolder是旧的位置信息
            final ItemHolderInfo animationInfo = mItemAnimator
                    .recordPostLayoutInformation(mState, holder);
			//从mViewInfoStore的mOldChangedHolders容器中按key找,在dispatchLayoutStep1中分析过
			//mOldChangedHolders存储的是更新却未删除的view的旧位置信息
			//按position取出来
            ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
			//view更新却不删除if才成立
            if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
                final boolean oldDisappearing = mViewInfoStore.isDisappearing(
                        oldChangeViewHolder);
                final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
                if (oldDisappearing && oldChangeViewHolder == holder) {
                    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 {
						//如果view之前之后都在且有变化,直接就执行动画,不等process方法了
                        //判断是否变化的标准是oldChangeViewHolder和holder是否是同一个
                        //默认情况下变化的view会创建新的viewholder,所以肯定不是同一个
                        //但是如果采用局部刷新,会复用viewHolder,而不执行change动画
                        animateChange(oldChangeViewHolder, holder, preInfo, postInfo,
                                oldDisappearing, newDisappearing);
                    }
                }
            } else {
				//flag有四种:FLAG_PRE、FLAG_POST、FLAG_APPEAR、FLAG_DISAPPEARED
				//FLAG_DISAPPEARED是由addDisappearingView方法才能设定
                //用于预布局列出,世纪布局未进入屏幕的view
				//相当于flag增加一个FLAG_POST,只有更新后还在屏幕上且内容没有变化的才会执行此句
				//如果view之前之后都在且没有变化,flag为FLAG_PRE|FLAG_POST
				//如果view之前在之后不在,flag为FLAG_PRE
				//如果view之前不在之后在,flag为FLAG_APPEAR|FLAG_POST
                mViewInfoStore.addToPostLayout(holder, animationInfo);
            }
        }

        //根据flag的不同确定不同的动画
        mViewInfoStore.process(mViewInfoProcessCallback);
    }

    ......
	//做一些后续工作
    mLayout.onLayoutCompleted(mState);
    onExitLayoutOrScroll();
    stopInterceptRequestLayout(false);
    mViewInfoStore.clear();
    if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1])) {
        dispatchOnScrolled(0, 0);
    }
    recoverFocusFromState();
    resetFocusInfo();
}

最后根据flag的不同在mViewInfoStore.process中执行相应的对话,我的源码是androidx.recyclerview.weight包下的,所以最终执行的是属性动画,兼容包下的可能会有所不同。与常见方法不同,这里的属性动画不是立即提交执行,而是延后到下一次屏幕刷新。

void process(ProcessCallback callback) {
	//以下动画均是在下一次屏幕刷新时执行,而不是立即提交执行
    for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
        final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
        final InfoRecord record = mLayoutHolderMap.removeAt(index);
        if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
            //预布局列出,实际布局未进入屏幕,不需要动画
            callback.unused(viewHolder);
        } else if ((record.flags & FLAG_DISAPPEARED) != 0) {
            // Set as "disappeared" by the LayoutManager (addDisappearingView)
            //由于布局变化(插入view或者view变大),导致view被挤出屏幕,执行平移动画
			//FLAG_DISAPPEARED标记位是在LayoutManager的onLayoutChildren中设置的
            if (record.preInfo == null) {
                // similar to appear disappear but happened between different layout passes.
                // this can happen when the layout manager is using auto-measure
                callback.unused(viewHolder);
            } else {
                callback.processDisappeared(viewHolder, record.preInfo, record.postInfo);
            }
        } else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) {
            // Appeared in the layout but not in the adapter (e.g. entered the viewport)
            callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
        } else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
            //如果view之前之后都在且没有变化,最终使用view.setTranslationX(-deltaX)
			//和view.setTranslationY(-deltaY);实现必要的平移
            callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
        } else if ((record.flags & FLAG_PRE) != 0) {
            //如果view之前在之后不在,实际执行的是透明度变0,即渐变退出animation.setDuration(getRemoveDuration()).alpha(0)
            callback.processDisappeared(viewHolder, record.preInfo, null);
        } else if ((record.flags & FLAG_POST) != 0) {
            //如果view之前不在之后在,也是执行平移动画
            callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
        } else if ((record.flags & FLAG_APPEAR) != 0) {
            // Scrap view. RecyclerView will handle removing/recycling this.
        } else if (DEBUG) {
            throw new IllegalStateException("record without any reasonable flag combination:/");
        }
        InfoRecord.recycle(record);
    }
}

结语

通过阅读d1、d2、d3的源码能大致看到RecyclerView更新时的操作,如果是第一次显示自然不会进行预布局(相关条件为false),不存在旧位置信息,d3的动画也就无从谈起。如果Adapter调用了notifyItemXXX系列方法引起数据变化,RecyclerView在进行新布局前会先进行预布局,把所有可能需要动画的子view全部列出来,然后再执行新布局,最后在d3中确定子view的动画。分析完毕会发现并不复杂,当然RecyclerView的内容远不止于此,后续还会开很多篇继续深入解析RecyclerView源码。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章