RecycleView LayoutManage-GridLayoutManage源码浅析
创始人
2025-05-31 06:27:47

背景

项目有用到阿里的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. VERTICAL方向
  3. mReverseLayout为false(不反转 )

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

@Override
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {final int otherDirSpecMode = mOrientationHelper.getModeInOther();//默认EXACTLYfinal boolean layingOutInPrimaryDirection =layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_TAIL;//是否按顺序添加也就是 item排列方式为123456int count = 0;int remainingSpan = mSpanCount;//列数//1while (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);consumedSpanCount += spanSize;//消耗一个item位置mSet[count] = view;//存储到set数组后续用到count++;}int maxSize = 0;//当前行最大高度//2assignSpans(recycler, state, count, layingOutInPrimaryDirection);//3for (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都为0measureChild(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;//5if (mOrientation == VERTICAL) {if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {//layoutState.mLayoutDirection是由锚点方向决定 一般都是LAYOUT_ENDbottom = layoutState.mOffset;top = bottom - maxSize;} else {top = layoutState.mOffset;//mOffset为layoutManage上一次填充后的结束点bottom = top + maxSize;}}//6for (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();//LinearLayoutManagervoid 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;//设置真实indexspan += 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) {//是否存在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都为0measureChild(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的上下Marginfinal 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) {//1if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {//layoutState.mLayoutDirection是由锚点方向决定 初始化首屏是LAYOUT_ENDbottom = layoutState.mOffset;top = bottom - maxSize;} else {//2top = 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项为0int 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相关源码

相关内容

热门资讯

对话李惠森:百年家族企业李锦记... 永远创业,而非守业,将“变”视为唯一不变的核心。文|《中国企业家》记者 李欣编辑|米娜头图摄影|佘贵...
北京通州站将于12月30日开通... 12月26日,记者从北京市交通委、国铁北京局等部门获悉,坐落于北京城市副中心综合交通枢纽(以下简称“...
【财闻联播】白银基金,明日再度... ★ 宏观动态 ★ 我国将抓紧出台新就业形态劳动者权益保障办法 据新华社,国务院关于灵活就业和新就业形...
和讯投顾孔晓云:7连阳的市场蕴... 七连阳的市场蕴含何种机会?和讯投顾孔晓云表示,今天在没有外资的情况下,盘面是放量收红,早盘商业航天继...
徽商银行:以数智化破解融资难题... 当普惠金融迈入“提质增效”的2.0时代,如何突破传统模式,实现精准赋能?徽商银行以一场数智化转型,给...