专栏名称: 大胃粥
Android工程师
目录
相关文章推荐
物道  ·  天青一色,灵蛇出! ·  18 小时前  
物道  ·  一日道|心美,一切皆美 ·  昨天  
51好读  ›  专栏  ›  大胃粥

RecyclerView源码剖析: 基本显示

大胃粥  · 掘金  ·  · 2019-11-23 15:29

正文

阅读 26

RecyclerView源码剖析: 基本显示

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的布局

从ww前面的分析可知,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();
    }    
复制代码

根据例子中的情况,锚点坐标是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;
        // 保存锚点当前位置
        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






请到「今天看啥」查看全文