RecycleView LayoutManage-GridLayoutManage源码浅析

文章详细解析了GridLayoutManager的布局流程,从onLayoutChildren到layoutChunk方法,介绍了如何创建并设置view的位置,以及处理不同spanSize和高度不一致的情况。重点讨论了如何保证同一行的宽高一致,并涉及到了RecyclerView的添加视图、测量子视图和布局过程。

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

背景

项目有用到阿里的tangram3动态布局框架,有时候某些特殊需求想定制的时候会比较头疼,其中这个框架又依赖vlayout,所以你都要了解内部原理,最近看到vlayout的layoutManager相关代码,想着之前只看过LinearLayoutManager的布局流程 但是还没看过GridLayoutManager的,所以就有了这篇学习记录

首先

GridLayoutManager是继承于LinearLayoutManager
工作流程大概是:
RecyclerView.onLayout -> RecyclerView.dispatchLayout -> LinearLayoutManager.onLayoutChildren -> LinearLayoutManager.fill -> LinearLayoutManager.layoutChunk

GridLayoutManager最重要的一个方法就是layoutChunk,它主要是负责添加view和设置view的实际位置

为了方便理解我们设置以下条件:

  1. spanCount(列数)为2
  2. VERTICAL方向
  3. mReverseLayout为false(不反转 )

大概原理
1、一次性创建两个view(spanCount决定数量)
2、设置view的top为 layoutState.mOffset,bottom为maxsize(最高的item高度)
3、完成后layoutState.mOffset +=maxsize 然后再循环

排除干扰代码后伪代码如下,分七步,大概了解下下面代码就行,下面拆分代码,记录这次学习记录

@Override
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
                 LayoutState layoutState, LayoutChunkResult result) {
    final int otherDirSpecMode = mOrientationHelper.getModeInOther();//默认EXACTLY
    final boolean layingOutInPrimaryDirection =
            layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_TAIL;//mReverseLayout为false,layingOutInPrimaryDirection为true
    int count = 0;
    int remainingSpan = mSpanCount;//列数
    //1
    while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {//一次性循环处理两个item 也就是一行
        int pos = layoutState.mCurrentPosition;
        final int spanSize = getSpanSize(recycler, state, pos);
        remainingSpan -= spanSize;
        if (remainingSpan < 0) {//有些一个item占两行 这时候就提前占完一行
            break; // item did not fit into this row or column
        }
        View view = layoutState.next(recycler);
        consumedSpanCount += spanSize;//消耗一个item位置
        mSet[count] = view;//存储到set数组后续用到
        count++;
    }
    int maxSize = 0;//当前行最大高度
    //2
    assignSpans(recycler, state, count, layingOutInPrimaryDirection);
    //3
    for (int i = 0; i < count; i++) {
        View view = mSet[i];
        if (layoutState.mScrapList == null) {//是否存在ScrapList缓存 没有就直接添加
            if (layingOutInPrimaryDirection) {
                addView(view);//添加进recyclerView
            } else {
                addView(view, 0);
            }
        } else {//不为空则执行淡入动画方式添加
            if (layingOutInPrimaryDirection) {
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0);
            }
        }
        calculateItemDecorationsForChild(view, mDecorInsets);//获取开发者自定义的mItemDecorations信息至mDecorInsets 没设置Rect都为0
        measureChild(view, otherDirSpecMode, false);//测量子view宽高
        final int size = mOrientationHelper.getDecoratedMeasurement(view);//获取view的垂直方向大小,也就是高度
        if (size > maxSize) {//maxSize初始为0 这时候赋值
            maxSize = size;
        }
    }
    //4
    // 如果子view 高度不统一 则根据子view的边距大小 按照EXACTLY模式测量,应该是子view在warp_content下 保证同一行的宽高是一样的
    for (int i = 0; i < count; i++) {
        final View view = mSet[i];
        if (mOrientationHelper.getDecoratedMeasurement(view) != maxSize) {
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            final Rect decorInsets = lp.mDecorInsets;
            final int verticalInsets = decorInsets.top + decorInsets.bottom
                    + lp.topMargin + lp.bottomMargin;
            final int horizontalInsets = decorInsets.left + decorInsets.right
                    + lp.leftMargin + lp.rightMargin;
            final int totalSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize);
            final int wSpec;
            final int hSpec;
            if (mOrientation == VERTICAL) {
                wSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY,
                        horizontalInsets, lp.width, false);
                hSpec = View.MeasureSpec.makeMeasureSpec(maxSize - verticalInsets,
                        View.MeasureSpec.EXACTLY);
            }
            measureChildWithDecorationsAndMargin(view, wSpec, hSpec, true);
        }
    }
    //消耗的高度-用于是否填充满一屏view计算
    result.mConsumed = maxSize;

    int left = 0, right = 0, top = 0, bottom = 0;
    //5
    if (mOrientation == VERTICAL) {
        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {//layoutState.mLayoutDirection是由锚点方向决定 一般都是LAYOUT_END
            bottom = layoutState.mOffset;
            top = bottom - maxSize;
        } else {
            top = layoutState.mOffset;//mOffset为layoutManage上一次填充后的结束点
            bottom = top + maxSize;
        }
    }
    //6
    for (int i = 0; i < count; i++) {
        View view = mSet[i];
        LayoutParams params = (LayoutParams) view.getLayoutParams();
        if (mOrientation == VERTICAL) {//确定左右开始点和结束点
            left = getPaddingLeft() + mCachedBorders[params.mSpanIndex];
            right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
        } else {
            top = getPaddingTop() + mCachedBorders[params.mSpanIndex];
            bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
        }
        // We calculate everything with View's bounding box (which includes decor and margins)
        // To calculate correct layout position, we subtract margins.
        //7 真正设置view实际高度的地方
        layoutDecoratedWithMargins(view, left, top, right, bottom);
    }
    Arrays.fill(mSet, null);
}
步骤1创建grid一整行的item view 加入mSet数组
    while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
        int pos = layoutState.mCurrentPosition;
        final int spanSize = getSpanSize(recycler, state, pos);
        remainingSpan -= spanSize;
        if (remainingSpan < 0) {//有些一个item占两行 这时候就提前占完一行
            break; // item did not fit into this row or column
        }
        View view = layoutState.next(recycler);//通过缓存机制获取view,没有则创建
        consumedSpanCount += spanSize;//消耗一个item位置
        mSet[count] = view;//存储到set数组后续用到
        count++;
    }

这里的getSpanSize方法获取的就是开发者调用setSpanSizeLookup 设置item占几列的处理,

public void setSpanSizeLookup(SpanSizeLookup spanSizeLookup) {
    mSpanSizeLookup = spanSizeLookup;
}

而其中的otherDirSpecMode测量模式,默认是取LinearLayoutManager中width的模式,而它在初始化的时候设置为精确模式

    final int otherDirSpecMode = mOrientationHelper.getModeInOther();
    //LinearLayoutManager
    void setRecyclerView(RecyclerView recyclerView) {
        if (recyclerView == null) {
            mRecyclerView = null;
            mChildHelper = null;
            mWidth = 0;
            mHeight = 0;
        } else {
            mRecyclerView = recyclerView;
            mChildHelper = recyclerView.mChildHelper;
            mWidth = recyclerView.getWidth();
            mHeight = recyclerView.getHeight();
        }
        mWidthMode = MeasureSpec.EXACTLY;//默认设置
        mHeightMode = MeasureSpec.EXACTLY;//默认设置
    }
步骤2 assignSpans 设置相对一行的下标和所占的列数-后续计算有用到
private void assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count,
    ...
    span = 0;
    for (int i = start; i != end; i += diff) {
        View view = mSet[i];
        LayoutParams params = (LayoutParams) view.getLayoutParams();
        params.mSpanSize = getSpanSize(recycler, state, getPosition(view));
        params.mSpanIndex = span;//设置真实index
        span += params.mSpanSize;
    }
}
步骤3
  1. 往RecycleView中添加view

  2. 获取view中的Decoration(间隙)添加进入mDecorInsets(Rect)中(好像全局变量没用到,用到都是LayoutParams.mDecorInsets)

  3. 测量子view的宽高

  4. 获取到这一行最大item的高度

     for (int i = 0; i < count; i++) {
         View view = mSet[i];
         if (layoutState.mScrapList == null) {//添加view 只是有无动画的区别
             if (layingOutInPrimaryDirection) {
                 addView(view);//添加进recyclerView
             } else {
                 addView(view, 0);
             }
         } else {
             if (layingOutInPrimaryDirection) {
                 addDisappearingView(view);
             } else {
                 addDisappearingView(view, 0);
             }
         }
         calculateItemDecorationsForChild(view, mDecorInsets);//获取开发者自定义的mItemDecorations信息至mDecorInsets 没设置Rect都为0
         measureChild(view, otherDirSpecMode, false);//测量子view宽高
         final int size = mOrientationHelper.getDecoratedMeasurement(view);//获取view的垂直方向大小,也就是高度
         if (size > maxSize) {//maxSize初始为0 这时候赋值
             maxSize = size;
         }
     }
    
步骤4 如果子view 高度不统一 则根据子view的边距大小 按照EXACTLY模式测量,保证同一行的宽高是一样的
    //
    for (int i = 0; i < count; i++) {
        final View view = mSet[i];
        if (mOrientationHelper.getDecoratedMeasurement(view) != maxSize) {
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            final Rect decorInsets = lp.mDecorInsets;//步骤3 第二步获取的间隙
            final int verticalInsets = decorInsets.top + decorInsets.bottom
                    + lp.topMargin + lp.bottomMargin;//子view上下的间隙(开发者添加Decoration)+view的上下Margin
            final int horizontalInsets = decorInsets.left + decorInsets.right
                    + lp.leftMargin + lp.rightMargin;//同上
            final int totalSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize);//算出父类给子view最大的宽度
            final int wSpec;
            final int hSpec;
            if (mOrientation == VERTICAL) {
                wSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY,
                        horizontalInsets, lp.width, false);
                hSpec = View.MeasureSpec.makeMeasureSpec(maxSize - verticalInsets,
                        View.MeasureSpec.EXACTLY);//maxSize - verticalInsets为item的内容区域
            }
            measureChildWithDecorationsAndMargin(view, wSpec, hSpec, true);
        }
    }
    //消耗的高度-用于是否填充满一屏view计算
    result.mConsumed = maxSize;
步骤5 根据锚点方向确定子view的上下坐标,一般情况都走2的逻辑
    if (mOrientation == VERTICAL) {//1
        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {//layoutState.mLayoutDirection是由锚点方向决定 初始化首屏是LAYOUT_END
            bottom = layoutState.mOffset;
            top = bottom - maxSize;
        } else {//2
            top = layoutState.mOffset;//mOffset为layoutManage上一次填充后的结束点
            bottom = top + maxSize;
        }
    }
步骤6 确定left和right坐标
    for (int i = 0; i < count; i++) {
        View view = mSet[i];
        LayoutParams params = (LayoutParams) view.getLayoutParams();
        if (mOrientation == VERTICAL) {//确定左右开始点和结束点
            left = getPaddingLeft() + mCachedBorders[params.mSpanIndex];
            right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);//获取width+decorate的left和right+左右margin
        }
        //真正设置view实际高度的地方
        layoutDecoratedWithMargins(view, left, top, right, bottom);
    }

其中mCachedBorders是一个一维数组,以spanCount为2 设备宽度为1080为例,它里面存储这[0,540,1080]
调用链:
LinearLayoutManager.onLayoutChildren -> GridLayoutManager.onAnchorReady -> GridLayoutManager.updateMeasurements - > GridLayoutManager.calculateItemBorders

private void calculateItemBorders(int totalSpace) {
    mCachedBorders = calculateItemBorders(mCachedBorders, mSpanCount, totalSpace);
}

static int[] calculateItemBorders(int[] cachedBorders, int spanCount, int totalSpace) {
    if (cachedBorders == null || cachedBorders.length != spanCount + 1
            || cachedBorders[cachedBorders.length - 1] != totalSpace) {
        cachedBorders = new int[spanCount + 1];//比itemCount多一个元素
    }
    cachedBorders[0] = 0;//第0项为0
    int sizePerSpan = totalSpace / spanCount;//每个item占用的宽度
    int sizePerSpanRemainder = totalSpace % spanCount;//不足一个item剩下的间隙
    int consumedPixels = 0;
    int additionalSize = 0;
    for (int i = 1; i <= spanCount; i++) {
        int itemSize = sizePerSpan;
        additionalSize += sizePerSpanRemainder;
        if (additionalSize > 0 && (spanCount - additionalSize) < sizePerSpanRemainder) {//处理剩余间隙
            itemSize += 1;
            additionalSize -= spanCount;
        }
        consumedPixels += itemSize;
        cachedBorders[i] = consumedPixels;//赋值mCachedBorders
    }
    return cachedBorders;
}
步骤7 其中方法参数left top等属性都是grid item的最大坐标,如果设置了margin和Decoration 则需做对应的偏移
    public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,
            int bottom) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Rect insets = lp.mDecorInsets;
        child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
                right - insets.right - lp.rightMargin,
                bottom - insets.bottom - lp.bottomMargin);
    }

结语

记录下学习记录,下次学vlayout的相关layoutManager相关源码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值