Android Render(三)supportVersion 27.0.0源码RecyclerView绘制流程解析

96
Dawish_大D
0.4 2017.11.05 16:13* 字数 1982

阅读者三篇Android绘制文章,会让你对理解Android绘制有帮助:


RecyclerView绘制三步骤.png

RecyclerViewsupportVersion 27.0.0为目前的最新版本,其实Support Library每一个版本之间都有一定的差异,有API Changes或者Bug fixes,可以通过API Diffs来查看其区别。具体的可以参考:https://developer.android.google.cn/topic/libraries/support-library/revisions.html

就来我们现在要说的 RecyclerView来说,从刚开始出来到现在的我们开发人员大面积使用,修改了很多次了,里面的绘制流程也是重构过很多次,网上有一些讲解RecyclerView绘制的文章,大都是老版本的support library,跟目前最新的RecyclerView的绘制有所差异的。

1.基本使用

    @BindView(R.id.recyclerView)
    RecyclerView recyclerView;
    GridLayoutManager gridLayoutManager;

        gridLayoutManager = new GridLayoutManager(ActivityMain.this, 2);
        gridLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        //设置LayoutManager
        recyclerView.setLayoutManager(gridLayoutManager);
        //设置左上边距
        recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
            @Override
            public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
                super.getItemOffsets(outRect, view, parent, state);
                outRect.left = 4;
                outRect.top = 4;
            }
        });
        //设置item动画
        recyclerView.setItemAnimator(new DefaultItemAnimator());
        MAdapter mAdapter = new MAdapter();
        //设置Adapter
        recyclerView.setAdapter(mAdapter);
        mAdapter.notifyDataSetChanged();

上面是RecyclerView的基本使用,就是设置LayoutManager,设置一些装饰,设置一些动画,设置Adapter,如果在设置完成之前Adapter中我们没有填充数据,UI界面是不会发生变化的。如果我们设置完成之后填充数据就需要调用notifyDataSetChanged()方法请求绘制刷新。

2.初始化-绘制之前的准备

2.1 setAdapter(Adapter adapter)

这一步是必须的,不设置Adapter ,那么数据跟UI就无法关联起来,也就谈不上什么绘制了,绘制都是要基于数据的。

setAdapter(Adapter adapter)方法源代码:

    //为RecyclerView设置Adapter,为RecyclerView提供子itemView
    public void setAdapter(Adapter adapter) {
        // 设置布局绘制不被冻结,以便刷新界面UI
        setLayoutFrozen(false);
        //设置Adapter内部实现
        setAdapterInternal(adapter, false, true);
        //请求重新布局
        requestLayout();
    }

一共调用了三个方法,第一个和最后一个方法都很好理解,我们重点要说setAdapterInternal方法,说之前我要知道setAdapter主要是适配数据,当我们设置的数据有变化时,UI界面能够及时刷新。要实现这样的逻辑,就必须存在数据变更观察者和被观察者,就是RecyclerView要观察Adapter的数据变化。

Adapter类中有一个被观察者:

 private final AdapterDataObservable mObservable = new AdapterDataObservable();  //默认就创建好了

RecyclerView中有一个观察者:

 private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver();  //默认就创建好了

RecyclerViewsetAdapterInternal方法中使观察者和被观察者发生关系:

    //设置Adapter内部实现
    private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
        boolean removeAndRecycleViews) {
    if (mAdapter != null) {
        //移除之前观察者和被观察者之间的关系
        mAdapter.unregisterAdapterDataObserver(mObserver);
        //移除之前RecyclerView和Adapter之间的关系
        mAdapter.onDetachedFromRecyclerView(this);
    }
    if (!compatibleWithPrevious || removeAndRecycleViews) {
        //不兼容之前的并且移除所以的itemView
        removeAndRecycleViews();
    }
    //重置删除、移动、增加辅助工具类AdapterHelper
    mAdapterHelper.reset();
    //记录之前的Adapter
    final Adapter oldAdapter = mAdapter;
    //赋值新的Adapter
    mAdapter = adapter;
    if (adapter != null) { //新的Adapter不为空
        //注册观察者跟被观察者之间的关系
        adapter.registerAdapterDataObserver(mObserver);
        //注册RecyclerView与Adapter之间的关系
        adapter.onAttachedToRecyclerView(this);
    }
    if (mLayout != null) {
        //通知LayoutManager Adapter发生了变化
        mLayout.onAdapterChanged(oldAdapter, mAdapter);
    }
    //通知回收复用管理者Recycler Adapter发生了变化
    mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
    mState.mStructureChanged = true;
    markKnownViewsInvalid();
}

经过一系列的重置后调用adapter.registerAdapterDataObserver(mObserver)使二者发现关系。之后请求重绘。

2.2 setLayoutManager(LayoutManager layout)

这一步也是必须的,用什么类型的LayoutManager来绘制RecyclerView,就算设置了Adapter ,没设置LayoutManagerRecyclerView也是不知道该如何绘制。

setLayoutManager方法源代码:

    //设置LayoutManager
   public void setLayoutManager(LayoutManager layout) {
        if (layout == mLayout) { //设置的LayoutManager跟之前的一样就返回
            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.
        if (mLayout != null) {
            // end all running animations
            if (mItemAnimator != null) {
                mItemAnimator.endAnimations();  //马上结束动画
            }
            //LayoutManager负责移除回收所有的itemView
            mLayout.removeAndRecycleAllViews(mRecycler);
            //LayoutManager负责移除回收所有的已经废弃itemView缓存
            mLayout.removeAndRecycleScrapInt(mRecycler);
            //清除所有缓存
            mRecycler.clear();

            if (mIsAttached) {
                mLayout.dispatchDetachedFromWindow(this, mRecycler);
            }
            //重置LayoutManager中RecyclerView的状态
            mLayout.setRecyclerView(null);
            mLayout = null;
        } else {
            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);
            }

            //把当前的RecyclerView交给设置进来的LayoutManager
            mLayout.setRecyclerView(this);

            if (mIsAttached) {
                mLayout.dispatchAttachedToWindow(this);
            }
        }
        //更新Recycler中的缓存大小
        mRecycler.updateViewCacheSize();
        //请求重绘
        requestLayout();
    }

做了一些重置后调用调用LayoutManagersetRecyclerView(RecyclerView recyclerView)方法使LayoutManagerRecyclerView发生关联。之后请求重绘。

2.3 addItemDecoration(ItemDecoration decor)

这一步不是必须的,给ItemView的绘制增加一些装饰,比如分割线,悬浮的指示等等,通过重写ItemDecoration中的几个方法可以实现很多炫酷的RecyclerView显示效果。

addItemDecoration(ItemDecoration decor)方法源代码跟踪:


    public void addItemDecoration(ItemDecoration decor) {
        //调用addItemDecoration方法
        addItemDecoration(decor, -1);
    }
            
    public void addItemDecoration(ItemDecoration decor, int index) {
        if (mLayout != null) {
            mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll  or"
                    + " layout");
        }
        if (mItemDecorations.isEmpty()) { //保存ItemDecoration的ArrayList为空说明是第一次初始化
            setWillNotDraw(false);  //为第一次初始化设置 etWillNotDraw(false)保证RecyclerView的onDraw(canvas)方法可以调用,
                                    //应为RecyclerView所有Item的装饰是在RecyclerView的onDraw(canvas)里面被调用绘制的
        }
        if (index < 0) {  //把ItemDecoration加入ArrayList中
            mItemDecorations.add(decor);  
        } else {
            mItemDecorations.add(index, decor);
        }
        //使每一个itemView的装饰都可以被绘制
        markItemDecorInsetsDirty();
        //请求重绘
        requestLayout();
    }

经过一些的判断后把需要的装饰ItemDecoration加入到ArrayList中保存并请求重绘。

2.4 setItemAnimator(ItemAnimator animator)

这一步也不是必须的,RecyclerView的itemView的动画,一般都是itemView第一次创建展示的时候会播放动画,或者是删除移动itemView的时候。如果没有设置,也会有一个默认的动画DefaultItemAnimator

setItemAnimator(ItemAnimator animator)方法源代码:

    public void setItemAnimator(ItemAnimator animator) {
        if (mItemAnimator != null) {
            //如果之前的动画不为空就结束动画移除监听
            mItemAnimator.endAnimations();
            mItemAnimator.setListener(null);
        }
        //赋值
        mItemAnimator = animator;
        if (mItemAnimator != null) {
            //设置监听
            mItemAnimator.setListener(mItemAnimatorListener);
        }
    }

经过判断后赋值重新设置动画监听。

其实前面可以看到调用RecyclerViewsetLayoutManageraddItemDecorationsetAdapter三个方法都会去调用requestLayout()方法请求重绘。如果在创建Adapter时候我们就绑定了数据就不需要调用notifyDataSetChanged()来绘制UI界面了。如果是之后绑定数据那就要手动调用notifyDataSetChanged()方法绘制。

Adapter.notifyDataSetChanged()方法源码阅读:

public final void notifyDataSetChanged() {
   mObservable.notifyChanged();
}

我们接着看AdapterDataObservable.notifyChanged()方法实现:

//mObservers是保存观察者的一个ArrayList
public void notifyChanged() {
    for (int i = mObservers.size() - 1; i >= 0; i--) {
        //通知所有的观察者数据发送改变了
        mObservers.get(i).onChanged();
    }
}

我们再来看RecyclerViewDataObserver.onChanged()方法:

@Override
public void onChanged() {
    ......
    //Adapter目前没有待更新或者正在更新的操作才可以重新绘制
    if (!mAdapterHelper.hasPendingUpdates()) {
        requestLayout();
    }
}

从上面我们可以看到一个Adapter中可能存在多个观察者,那就意味中Adapter可以同时被多个RecyclerView公用。

调用requestLayout()方法后就会走下面的onMeasure onLayout onDraw三个流程了。

3.onMeasure

    @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        if (mLayout == null) {  //LayoutM为空就走默认测量然后返回
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
        if (mLayout.mAutoMeasure) {  //是不是自动测量,我们创建LayoutManager时默认为支持自动测量
            final int widthMode = MeasureSpec.getMode(widthSpec);
            final int heightMode = MeasureSpec.getMode(heightSpec);
            //当前RecyclerView的宽高是否都为精确值
            final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
                    && heightMode == MeasureSpec.EXACTLY;
            //委托LayoutManager来测量RecyclerView的宽高(还是走defaultOnMeasure方法)
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);

            //如果RecyclerView的宽高都是写死的精确值或者是match_parent并且Adapter还没有设置就结束测量
            if (skipMeasure || mAdapter == null) {
                return;
            }

            //当RecyclerView的宽高设置为wrap_content时,skipMeasure=false 就是不跳过测量
            //当宽高为wrap_content时,就先不能确定RecyclerView的宽高,因为需要先测量子itemView的宽高后才可以确定自己的宽高
            if (mState.mLayoutStep == State.STEP_START) { //还未测量过
                //绘制第一个,收集
                dispatchLayoutStep1();
            }
            // set dimensions in 2nd step. Pre-layout should happen with old dimensions for
            // consistency
            mLayout.setMeasureSpecs(widthSpec, heightSpec);
            mState.mIsMeasuring = true;
            //
            dispatchLayoutStep2();

            // now we can get the width and height from the children.
            //通过对子ItemView的测量布局来确定宽高为WARP_CNTENT的RecyclerView的宽高
            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 {
                  //如果是自定义LayoutManager就要自己实现
          }
    }

RecyclerView的onMeasure方法测量是分两种情况的:

  1. RecyclerView的宽高设置为match_parent具体值的时候,skipMeasure=true,此时会只需要测量其自身的宽高就可以知道RecyclerView的大小,这时是onMeasure方法测量结束。
  2. RecyclerView的宽高设置为wrap_content时,skipMeasure=falseonMeasure会继续执行下面的dispatchLayoutStep2(),其实就是测量RecyclerView的子视图的大小最终确定RecyclerView的实际大小,这种情况真正的测量操作都是在方法dispatchLayoutStep2()里执行的:

下面看一下onMeasure方法中调用到的dispatchLayoutStep1方法和dispatchLayoutStep2方法。

dispatchLayoutStep1()方法:

private void dispatchLayoutStep1() {
    mState.assertLayoutStep(State.STEP_START);
    mState.mIsMeasuring = false;
    //禁止布局请求
    eatRequestLayout();
    //清空itemView信息保存类
    mViewInfoStore.clear();
    onEnterLayoutOrScroll();
    //1.重排序所有UpdateOp,比如move操作会排到末尾
    //2.依次执行所有UpdateOp事件,更新VH的position(这里是前移mPosition,mPreLayoutPosition不变),如果VH被remove了标记它。
    /*在2中,“会决定是否在prelayout之前把更新告诉LM”,
    这里把更新告诉LM指的是把更新反应在VH的mPreposition上(VH中有mPosition、mPreLayoutPosition等成员,注意,prelayout中是使用的mPreLayoutPosition)mPosition是一定会更新的,mPreLayoutPosition则不一定。
    如果RV决定不把更新再prelayout之前告诉LM,则会对VH更新时的参数applyToPreLayout传入false,mPosition更新了而mPreLayoutPosition则是旧值,反之mPreLayoutPosition则和mPosition同步。
    当然,如何“决定”我们就不说了,有兴趣可以看下原文和源码。*/
    processAdapterUpdatesAndSetAnimationFlags();
    processAdapterUpdatesAndSetAnimationFlags();
    //保存焦点信息
    saveFocusInfo();
    //对RecyclerView的情况存储类State赋值
    mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
    mItemsAddedOrRemoved = mItemsChanged = false;
    mState.mInPreLayout = mState.mRunPredictiveAnimations;
    //我们需要展示的所有ItemView个数 就是我们写Adapter给的getItemCount返回值
    mState.mItemCount = mAdapter.getItemCount();
    //找到屏幕上可以绘制的最小position和最大position
    findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);

    if (mState.mRunSimpleAnimations) {
        // Step 0: Find out where all non-removed items are, pre-layout

        //获得界面上所以显示的itemView的个数
        int count = mChildHelper.getChildCount();

        for (int i = 0; i < count; ++i) {

            ......略

            //保存所有ViewHolder的动画信息
            mViewInfoStore.addToPreLayout(holder, animationInfo);
            if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
                    && !holder.shouldIgnore() && !holder.isInvalid()) {
                long key = getChangedHolderKey(holder);
               
                //如果ViewHolder有变更就保存起来
                mViewInfoStore.addToOldChangeHolders(key, holder);
            }
        }
    }
    if (mState.mRunPredictiveAnimations) {

        ......略
        final boolean didStructureChange = mState.mStructureChanged;
        mState.mStructureChanged = false;  //设置RecyclerView的结构没有改变,因为这里是为了测量ReyclerView宽高而预布局子ItemV
        // 让LayoutManager布局所有ItemView的位置
        mLayout.onLayoutChildren(mRecycler, mState);
        //恢复本来的状态
        mState.mStructureChanged = didStructureChange;
        ......略
        // 其实走到这里把子View的位置和大小测量位置完了,但是没有去draw子itemView,界面应该还是不可见的
        for (int i = 0; i < mChildHelper.getChildCount(); ++i) {

                ......略

                //为每一个ViewHolder创建一个ItemHolderInfo,保存ViewHolder的位置信息动画信息等
                final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(
                        mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
                ......略

            }
        }
        // we don't process disappearing list because they may re-appear in post layout pass.
        clearOldPositions();
    } else {
        clearOldPositions();
    }
    //恢复绘制锁定
    resumeRequestLayout(false);
    mState.mLayoutStep = State.STEP_LAYOUT;
}

dispatchLayoutStep2()方法:

private void dispatchLayoutStep2() {
    //锁定布局请求
    eatRequestLayout();
    onEnterLayoutOrScroll();
    //设置RecyclerView为布局和动画状态
    mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
    mAdapterHelper.consumeUpdatesInOnePass();
    mState.mItemCount = mAdapter.getItemCount();
    mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

    // Step 2: Run layout
    mState.mInPreLayout = false;  //预布局已经完成,dispatchLayoutStep1方法中此值为true,为false的时候才会去真正的测量子View
    mLayout.onLayoutChildren(mRecycler, mState);  //LayoutManager去测量布局子ItemView

    mState.mStructureChanged = false;  //设置RecyclerView结构没变化
    mPendingSavedState = null;

    ......略
    //恢复布局锁定
    resumeRequestLayout(false);
}

三步测量解释:

  • dispatchLayoutStep1: Adapter的更新; 决定该启动哪种动画; 保存当前View的信息(getLeft(), getRight(), getTop(), getBottom()等); 如果有必要,先跑一次布局并将信息保存下来。
  • dispatchLayoutStep2: 真正对子View做布局的地方。
  • dispatchLayoutStep3: 为动画保存View的相关信息; 触发动画; 相应的清理工作。
    但是在onMeasure方法中没有调用dispatchLayoutStep3方法,下面的onLayout方法中会调用到这个方法。

3.onLayout

onLayout方法源代码阅读:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
    //直接调用dispatchLayout()方法布局
    dispatchLayout();
    TraceCompat.endSection();
    mFirstLayoutComplete = true;
}

dispatchLayout()方法源代码阅读:

void dispatchLayout() {
    if (mAdapter == null) {
        Log.e(TAG, "No adapter attached; skipping layout");
        // leave the state in START
        return;
    }
    if (mLayout == null) {
        Log.e(TAG, "No layout manager attached; skipping layout");
        // leave the state in START
        return;
    }
    //当Adapter或者LayoutManager为空的时候就直接返回,还测量布局个毛线球


    mState.mIsMeasuring = false;  //设置RecyclerView布局完成状态,前面已经设置预布局完成了。
    if (mState.mLayoutStep == State.STEP_START) { //如果没在OnMeasure阶段提前测量子ItemView
        dispatchLayoutStep1();  //收集ItemView的ViewHolder的信息并保存
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();  //正在测量布局子ItemView
    } 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);
        //有更新操作或者宽期望的宽高跟目前的宽高不一致就重新测量所有的子ItemView
        dispatchLayoutStep2();
    } else {
        // always make sure we sync them (to ensure mode is exact)
        mLayout.setExactMeasureSpecsFrom(this);
    }
    //执行测量第三部
    dispatchLayoutStep3();
}

可以看到在onLayout阶段也是跟onMeasure阶段一样会选择性地执行测量布局三个步骤,那么这三步在RecyclerView的什么情况下会有不同的执行呢?

测量布局总结:

就是RecyclerView的子ItemView可能会被测量布局多次,如果是RecyclerView的宽高是写死或者是match_parent
那么在onMeasure阶段不会提前测量布局子ItemView。如果宽或者高是wrap_content的,由于还没测量布局子ItemView,
所以不知道RecyclerView的内容的宽高,那么就要提前在onMeasure阶段就测量布局子ItemView确定内容显示区需要的宽高值来确定RecyclerView的宽高。

dispatchLayoutStep1 dispatchLayoutStep2 dispatchLayoutStep3这三步都一定会执行,只是在RecyclerView的宽高是写死或者是match_parent的时候会提前执行dispatchLayoutStep1 dispatchLayoutStep2者两个方法。会在onLayout阶段执行dispatchLayoutStep3第三步。

RecyclerView 写死宽高的时候onMeasure阶段很容易,直接设定宽高。但是在onLayout阶段会把dispatchLayoutStep1 dispatchLayoutStep2 dispatchLayoutStep3三步依次执行。

这就不看dispatchLayoutStep3这个方法了,有兴趣的自己去看,博客写着累,还没人看。
下面是dispatchLayoutStep3方法的解释:

The final step of the layout where we save the information about views for animations,trigger animations and do any necessary cleanup.
最后一步的布局,我们保存触发动画和做任何必要的清理。

3.onDraw

终于来到了最后一步绘制了,写了好几天,没人看!扎心了。
onDraw方法源代码阅读:

@Override
public void onDraw(Canvas c) {
    super.onDraw(c); //所有的ItemView先绘制

    //子ItemView绘制完了以后再绘制装饰
    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) { //遍历所有的装饰依次绘制
        mItemDecorations.get(i).onDraw(c, this, mState);
    }
}

ItemDecoration的onDraw方法其实是开发者自定义的,我们在创建ItemDecoration的时候需要重新其onDraw来自己绘制装饰,把更多的灵活性给开发者。关于ItemDecoration的自定义请看:http://www.jianshu.com/p/b46a4ff7c10a

测量布局绘制三个步骤只有onDraw看起来最简单了,其实正如我前面的文章Android Render(二)7.1源码硬件加速下draw绘制流程分析所说,onDraw方法如果不要需要一些特殊的效果,在TextView、ImageView这些控件中已经绘制完了,没必要在一些LinearLayout、RecyclerView等容器控件中去处理子View的绘制了。

Android进阶
Web note ad 1