RecyclerView源码剖析

本文深入剖析了Android的RecyclerView,从基本使用到源码分析,包括绘制流程、滑动机制、Recycler的ItemView复用、数据集变化与动画。通过分析,揭示了RecyclerView在measure、layout、draw阶段的细节,以及滑动时的scroll和fling过程。同时,解释了Recycler如何重用ItemView,以及数据集变更时的动画处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介

本文将从RecyclerView实现原理并结合源码详细分析这个强大的控件。
阅读本文要求:

  1. 熟悉android控件绘制
  2. 了解动画
  3. 了解Scroller
  4. You`re a fucking kind person。

本文所示源码版本是androidx-recycleView1.1.0。

基本使用

RecyclerView的基本使用并不复杂,只需要提供一个RecyclerView.Apdater的实现用于处理数据集与ItemView的绑定关系,和一个RecyclerView.LayoutManager的实现用于 测量并布局 ItemView。

绘制流程

众所周知,android控件的绘制可以分为3个步骤:measure、layout、draw。RecyclerView的绘制自然也经这3个步骤。
但是,RecyclerView将它的measure与layout过程委托给了RecyclerView.LayoutManager来处理,并且,它对子控件的measure及layout过程是逐个处理的,也就是说,执行完成一个子控件的measure及layout过程再去执行下一个。下面看下这段代码:

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;
        }

        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.
        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 {
   
        ...
    }
}

这是RecyclerView的测量方法,再看下dispatchLayoutStep2()方法:

private void dispatchLayoutStep2() {
   
    ...
    mLayout.onLayoutChildren(mRecycler, mState);
    ...
}

上面的mLayout就是一个RecyclerView.LayoutManager实例。通过以上代码(和方法名称),不难推断出,RecyclerView的measure及layout过程委托给了RecyclerView.LayoutManager。接着看onLayoutChildren方法,在兼容包中提供了3个RecyclerView.LayoutManager的实现,这里我就只以LinearLayoutManager来举例说明:

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.
    ...
    mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
    // calculate anchor position and coordinate
    updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
    ...
    if (mAnchorInfo.mLayoutFromEnd) {
   
        ...
    } else {
   
        // fill towards end
        updateLayoutStateToFillEnd(mAnchorInfo);
        mLayoutState.mExtra = extraForEnd;
        fill(recycler, mLayoutState, state, false);
        endOffset = mLayoutState.mOffset;
        final int lastElement = mLayoutState.mCurrentPosition;
        if (mLayoutState.mAvailable > 0) {
   
            extraForStart += mLayoutState.mAvailable;
        }
        // fill towards start
        updateLayoutStateToFillStart(mAnchorInfo);
        mLayoutState.mExtra = extraForStart;
        mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
        fill(recycler, mLayoutState, state, false);
        startOffset = mLayoutState.mOffset;
        ...
    }
    ...
}

源码中的注释部分我并没有略去,它已经解释了此处的逻辑了。这里我以垂直布局来说明,mAnchorInfo为布局锚点信息,包含了子控件在Y轴上起始绘制偏移量(coordinate),ItemView在Adapter中的索引位置(position)和布局方向(mLayoutFromEnd)——这里是指start、end方向。这部分代码的功能就是:确定布局锚点,以此为起点向开始和结束方向填充ItemView,如图所示:
在这里插入图片描述
在上一段代码中,fill()方法的作用就是填充ItemView,而图(3)说明了,在上段代码中fill()方法调用2次的原因。虽然图(3)是更为普遍的情况,而且在实现填充ItemView算法时,也是按图(3)所示来实现的,但是mAnchorInfo在赋值过程(updateAnchorInfoForLayout)中,只会出现图(1)、图(2)所示情况。现在来看下fill()方法:

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
       RecyclerView.State state, boolean stopOnFocusable) {
   
   ...
   int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
   LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
   while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
   
       ...
       layoutChunk(recycler, state, layoutState, layoutChunkResult);

       ...
       if (...) {
   
           layoutState.mAvailable -= layoutChunkResult.mConsumed;
           remainingSpace -= layoutChunkResult.mConsumed;
       }
       if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
   
           layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
           if (layoutState.mAvailable < 0) {
   
               layoutState.mScrollingOffset += layoutState.mAvailable;
           }
           
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值