前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >RecyclerView源码解析(一)开篇

RecyclerView源码解析(一)开篇

原创
作者头像
好好学习吧
修改2021-11-05 14:30:09
6070
修改2021-11-05 14:30:09
举报
文章被收录于专栏:今天学习了吗

1 前言

RecyclerView使用的广泛程度,相信大家都深有体会。作为android的一个大杀器,本身的代码量也是很可观的。本着整理学习的心态,于是就开始了本次的RecyclerView的源码分析之旅。

先说明一下,本系列使用的源码是androidx.recyclerview1.1.0。

本质上来说,所有的view控件解决的都是两个问题,一是绘制,二是交互。RecyclerView也不例外。所以,我们可以按照这个脉络去拆解RecyclerView。在这个系列文章中,我准备按照绘制,交互两条线去梳理RecyclerView的整个脉络。

本文是绘制的开篇,先梳理绘制的整体框架,后面将有系列文章来分析具体实现。

2 绘制

绘制,这里指的是measure, layout, draw。

measure

众所周知,measure的目的是递归测量view的长宽,即计算当前view及子view的measureWidth/measureHeight。那么,RecyclerView是怎么实现的呢?

代码语言:txt
复制
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
    if (mLayout == null) {
       ...
       return;
    }
    if (mLayout.isAutoMeasureEnabled()) {
        ...
    } else {
        ...
    }
}

RecyclerView的onMeasure的代码结构很清晰,分三种情况:

一,mLayout==null

代码语言:txt
复制
if (mLayout == null) {
    defaultOnMeasure(widthSpec, heightSpec);
    return;
}

这种情况是LayoutManager对象为空的时候。

RecyclerView并没有提供默认的LayoutManager对象,当没有设置LayoutManager对象,也就是mLayout是null时,调用defaultOnMeasure()方法。

代码语言:txt
复制
/**

* An implementation of {@link View#onMeasure(int, int)} to fall back to in various scenarios
* where this RecyclerView is otherwise lacking better information.
*/

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

defaultOnMeasure的作用注释说得很清楚,就是保底。看到了setMeasuredDimension,就知道是在这里设置的measuredWidth/measuredHeight。

二,mLayout.isAutoMeasureEnabled()为true

代码语言:txt
复制
if (mLayout.isAutoMeasureEnabled()) {
    final int widthMode = MeasureSpec.getMode(widthSpec);
    final int heightMode = MeasureSpec.getMode(heightSpec);
    
    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
    ...
    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()) {
       ...
        dispatchLayoutStep2();
        // now we can get the width and height from the children.
        mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
    }
}

这是LayoutManager支持自动测量的情况,是RecyclerView最常见的情况。

官方提供的LinearLayoutManager,GridLayoutManager,StaggeredGridLayoutManager都是支持自动测量的。

在这种情况下,先调用LayoutManager的onMeasure方法。系统提供的LayoutManager都没有重写该方法,如果使用的是系统LayoutManager,最后都会变相调用defaultOnMeasure。

接着判断如果父控件提供的长宽mode都是EXACTLY,也就是RecyclerView的测量长宽都是确定的数值,这种情况下,不需要再考虑子view的的长宽对RecyclerView的影响,直接返回;如果Adapter为空,意味着没有子view,也直接返回。在这两种情况下,之前defaultOnMeasure中测量的RecyclerView的长宽就视为是最终的measuredWidth/measuredHeight。

接下来处理的就是父控件提供的长宽mode中有AT_MOST的情况,也就是考虑子view对RecyclerView长宽的影响。根据mState.mLayoutStep的状态是否是State.STEP_START来判断是否执行dispatchLayoutStep1(),接着执行dispatchLayoutStep2(),然后会调用LayoutManager的setMeasuredDimensionFromChildren(),代码如下,遍历当前屏幕上所有的可见子view,获取包含所有可见子view的区域rect的坐标。方法的最后一行,发现了setMeasuredDimension,和View中的setMeasuredDimension的参数不一样,多了一个Rect对象,这个是具体的设置measuredWidth/measuredHeight的地方,由具体的LayoutManager类实现,最终都会调用View的setMeasuredDimension,设置RecyclerView的measuredWidth/measuredHeight。RecyclerView提供了默认的实现方法,LinearLayoutManager使用的就是默认方法,GridViewLayoutManager,StaggeredGridLayoutManager重写了该方法。后续篇章会详细介绍到。

代码语言:txt
复制
void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) {
    final int count = getChildCount();
    if (count == 0) {
        mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
        return;
    }
    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;
        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;
        }
    }
    mRecyclerView.mTempRect.set(minX, minY, maxX, maxY);
    setMeasuredDimension(mRecyclerView.mTempRect, widthSpec, heightSpec);
}

最后判断是否需要二次测量,shouldMeasureTwice()是否为true。系统默认是false,LinearLayoutManager重写了这个方法,见下图,要求RecyclerVIew的宽高mode都必须是AT_MOST,也就是宽高都必须是wrap_content,或者宽高是match_parent,但是RecyclerView的父控件是wrap_content。同时RecyclerView的子view是match_paren/wrap_content,不能是具体的数值。在条件都满足的情况下,进行二次测量,也就是再走一遍dispatchLayoutStep2()和setMeasuredDimensionFromChildren()。

代码语言:txt
复制
@Override
boolean shouldMeasureTwice() {
    return getHeightMode() != View.MeasureSpec.EXACTLY
            && getWidthMode() != View.MeasureSpec.EXACTLY
            && hasFlexibleChildInBothOrientations();
}
代码语言:txt
复制
boolean hasFlexibleChildInBothOrientations() {
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View child = getChildAt(i);
        final ViewGroup.LayoutParams lp = child.getLayoutParams();
        if (lp.width < 0 && lp.height < 0) {
            return true;
        }
    }
    return false;
}

三,mLayout.isAutoMeasureEnabled()为false

代码语言:txt
复制
else {
    if (mHasFixedSize) {
        mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
        return;
    }
    ...

    if (mAdapter != null) {
        mState.mItemCount = mAdapter.getItemCount();
    } else {
        mState.mItemCount = 0;
    }
    ...
    mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
    ...
}

这是RecyclerView不支持自动测量的情况。

如果mHasFixedSize为true,即RecyclerView的长宽不随Adapter content变动,调用LayoutManager的onMeasure方法,最终变相调用defaultOnMeasure,返回。

如果mHasFixedSize为false,就需要开发者自己定制LayoutManager,自己实现onMeasure进行measureWidth/measureHeight的测量了。我们主要用到的是第二种情况,所以这里不过多开展了。

通过对measure源码的分析,了解了RecyclerView在测量阶段做了哪些工作。但是也产生了以下疑问:RecyclerView的子view是在哪里测量的呢?dispatchLayoutStep1()和dispatchLayoutStep2()看着像是布局用的方法,放在测量里是干什么的?

并且dispatchLayoutStep1()和dispatchLayoutStep2()方法在布局阶段也用到了,我们一起看看它们做了什么事情。

layout

RecyclerView的onLayout方法实现的很简洁,只有一个dispatchLayout()方法,且dispatchLayout()方法主要有三个方法dispatchLayoutStep1(),dispatchLayoutStep2(),dispatchLayoutStep3()。

代码语言:txt
复制
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    ...
    dispatchLayout();
    ...
}
代码语言:txt
复制
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;
    }
    mState.mIsMeasuring = false;
    if (mState.mLayoutStep == State.STEP_START) {
        dispatchLayoutStep1();
        ...
        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.
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    }
    ...
    dispatchLayoutStep3();
}

dispatchLayoutStep1记录的一些状态的变化,主要和动画相关;dispatchLayoutStep3主要做了做动画处理并且重置清理一些状态;这两个方法是对应的。实际的测量和布局工作是放在dispatchLayoutStep2中进行的。dispatchLayoutStep2的核心代码是onLayoutChildren(),RecyclerView的子view的测量和布局工作都是在这里处理的,LayoutManager中默认是空方法,系统提供的LayoutManager都实现了该方法,后续篇章会分析它们是如何测量和布局的子view。

dispatchLayoutStep2

代码语言:txt
复制
/**
* The second layout step where we do the actual layout of the views for the final state.
* This step might be run multiple times if necessary (e.g. measure).
*/

private void dispatchLayoutStep2() {
    ...
    // Step 2: Run layout
    ...
    mLayout.onLayoutChildren(mRecycler, mState);
    ...
}

到此,我们看到了RecyclerView里的测量和布局只是搭了个架子,具体的实现是放在一个个具体的LayoutManager中的。想要了解具体怎么处理的,还需要去分析具体的LayoutManager的实现。

draw

先来看RecyclerView重写的draw方法,调用了super.draw(),故这里会执行系统默认的draw流程,后面用了for循环调用mItemDecorations的onDrawOver方法。

代码语言:txt
复制
@Override
public void draw(Canvas c) {
    super.draw(c);
    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDrawOver(c, this, mState);
    }
    ...
}

再来看下onDraw方法,super.onDraw()是空方法,它主要的工作是for循环调用了mItemDecorations的onDraw方法。draw()和onDraw()中主要的工作都是和ItemDecoration相关。ItemDecoration从字面上看,可以给item添加装饰,它可以为item自定义实现很多漂亮的效果,后续也会分析它的。

代码语言:txt
复制
@Override
public void onDraw(Canvas c) {
    super.onDraw(c);
    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDraw(c, this, mState);
    }
}

3 总结

本文是绘制篇的开篇,介绍了RecyclerView的绘制框架。在RecyclerView类中,只实现了RecyclerView的长宽mode是EXACTLY时的长宽测量,其余的测量布局工作都是放在LayoutManager中进行的,官方提供了三种LayoutManager,后续将一一进行分析。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 前言
  • 2 绘制
    • measure
      • layout
        • draw
        • 3 总结
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档