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相关源码

相关内容

热门资讯

去年营收13亿元亏了13亿元!... 本报(chinatimes.net.cn)记者赵奕 上海报道科创板再迎未盈利企业。日前,尚未实现盈利...
外卖战打了小半年,美团还是那个... 2025年的618“静悄悄”。即使是炮声响了半年的外卖即时零售,市场态势亦波澜不惊——就在本周,雷锋...
药不能停,中美差距最大的地方,... 长期征集日子很难,生活不容易,每个人都有自己的苦,有苦说不出的感觉,受苦的人最懂。和吃苦相比,有苦难...
原创 融... 6 月 15 日深夜,杭州网红烘焙品牌 “欢牛蛋糕屋” 在官方公众号发布《致消费者的告别公告》,直言...
SHEIN按需模式裂变 产业带... 坐落珠江口“黄金内湾”上的一家创意产业园里,机器发出的阵阵轰鸣声与键盘敲击声正交织成一首愉快的产业交...
中金黄金:公司的主要业务为黄金... 证券之星消息,中金黄金(600489)06月18日在投资者关系平台上答复投资者关心的问题。 投资者提...
京东0佣金杀入酒旅市场,刘强东... 618购物节今日到来,这个一年一度的电商年中大促,自2008年首次由京东举办以来,至今已持续18年。...
大和:周大福(01929.HK... 大和发布研报称,周大福(01929.HK)宣布发行可换股债券(CB),筹集约88亿元,并同步进行股份...
刘强东,“摊牌”了 6月17日,京东集团创始人、董事局主席刘强东在一个小型分享会上回应了胖东来的火爆出圈、与美团的竞争、...
河南财经政法大学举办中国法理学... 6月15日,中国法理学自主知识体系与“一带一路”涉外法治人才教育研讨会在河南财经政法大学召开。国内知...
科、创两板打通未盈利企业IPO... 21世纪经济报道 实习生 张长荣 记者 崔文静 北京报道“将进一步全面深化资本市场改革开放,推动科技...
浙江稠州商业银行落地 新型离岸... 为进一步加大金融服务实体经济力度,6月9日,国家外汇管理局在跨境金融服务平台上线了新型离岸国际贸易业...
京东:参与“京东酒店PLUS会... 上证报中国证券网讯(记者 曾庆怡)6月18日,京东旗下销售旅行相关非实物类商品的频道(京东旅行)发布...
证监会:未盈利科技型企业全部纳... 中国证监会发布《关于在科创板设置科创成长层 增强制度包容性适应性的意见》。其中提出,未盈利科技型企业...
设立数字人民币国际运营中心,央... 潘功胜表示,推进数字人民币的国际化运营与金融市场业务发展,服务数字金融创新 文|《财经》记者 康恺 ...
证监会:面向优质科技型企业试点... 中国证监会新闻发言人就《关于在科创板设置科创成长层 增强制度包容性适应性的意见》答记者问称,面向优质...
外卖“三国杀”,美团为什么稳占... 文丨文雨在平静如水的消费市场中,外卖是今年难得拥有看点和话题的行业。新玩家高调入局,不仅将行业推向了...
中国证监会公告允许合格境外投资... 中国证监会近日发布公告称,经商中国人民银行、国家外汇局,将从2025年10月9日起允许合格境外投资者...
上交所就进一步深化科创板改革配... 上交所就进一步深化科创板改革配套业务规则公开征求意见答记者问称,《科创板上市公司自律监管指引第5号—...
耐心资本崛起:九方智投股票学习... “杭州六小龙看似一夜崛起,实则是政府、企业与资本十年磨一剑的厚积薄发。”2025财新夏季峰会上,九方...
证监会:支持人工智能、商业航天... 6月18日,中国证监会发布实施《关于在科创板设置科创成长层 增强制度包容性适应性的意见》(以下简称《...
赵建:战争之谜与大争之世的资产... 赵建系西京研究院院长、中国首席经济学家论坛成员前言按照广义战争的定义,如果把科技战、贸易战、金融战、...
真·罗永浩直播干不过假·罗永浩... 在直播领域,出现了有趣的一幕,真·罗永浩似乎干不过假·罗永浩?这引发了网友们的热议。有的网友疑惑,难...
芦哲:经济叙事的三重分化——5... 芦哲 占烁(芦哲系东吴证券首席经济学家、中国首席经济学家论坛成员)核心观点5月经济仍然具有韧性。供给...
中国证监会公告允许合格境外投资... 新京报讯 据证监会网站消息,中国证监会近日发布公告称,经商中国人民银行、国家外汇局,将从2025年1...
M1“超长记忆”吊打R1,Mi... MiniMax 四处突围,终于撞上了自己的「好日子」。昨天凌晨,MiniMax正式开源它们的第一个推...
稀土的含金量还在上升,33年顶... 稀土的含金量,还在上升。最近,英国《金融时报》发表文章称,中国在稀土供应链上的成功,改变了贸易谈判中...
9000亿灰飞烟灭!五粮液,还... 2021年,五粮液(000858.SZ)总市值曾突破13300亿元,而今缩水至4500多亿,差不多9...
看得见,动不了,库存积压“吞噬... 看得见,动不了,库存积压仿佛是一只巨大的怪兽,正无情地“吞噬”着书业的利润。书店里,那些堆积如山的书...
赵伟:地方国补,缘何“暂停”?... 赵伟 贾东旭 侯倩楠(赵伟系申万宏源证券首席经济学家、中国首席经济学家论坛理事)摘要近期部分地区国补...