GridLayoutManager 中的一些坑

前言

如果GridLayoutManager使用item的布局都是wrap_cotent 那么会在布局更改时会出现一些出人意料的情况。(本文完全不具备可读性和说教性,仅为博主方便查找问题)

布局item:

<!--layout_item.xml-->
<?xml version="1.0" encoding="utf-8"?>
<com.vb.rerdemo.MyConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="10dp"
    android:background="#f0f">
    <com.google.android.material.card.MaterialCardView
        android:layout_width="match_parent"
        app:layout_constraintTop_toTopOf="parent"
        app:cardCornerRadius="10dp"
        app:cardBackgroundColor="#908000"
        android:layout_height="240dp">
        <TextView
            android:id="@+id/tv"
            android:layout_gravity="center"
            android:layout_width="wrap_content"
            android:text="hello world"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </com.google.android.material.card.MaterialCardView>
</com.vb.rerdemo.MyConstraintLayout>

在这里插入图片描述

//LastGapDecoration.kt
//给最后一行的item添加一个高度
class LastGapDecoration : ItemDecoration() {
    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        super.getItemOffsets(outRect, view, parent, state)
        super.getItemOffsets(outRect, view, parent, state)
        val itemPosition = parent.getChildAdapterPosition(view)
        val gridLayoutManager = parent.layoutManager as? GridLayoutManager ?: return
        val spanCount = gridLayoutManager.spanCount
        val itemCount = gridLayoutManager.itemCount
        if (spanCount <= 0) {
            return
        }
        val lastRowItemCount = itemCount % spanCount
        val lastRow =
            isLastRow(itemPosition, itemCount, spanCount, lastRowItemCount)
        Log.d("fmy","lastRow ${lastRow} itemPosition ${itemPosition} lastRowItemCount ${lastRowItemCount} itemCount ${itemCount} viewid ${view.hashCode()}")

        if (lastRow) {
            outRect.bottom = ScreenUtil.dp2px(40f,App.myapp)
        } else {
            outRect.bottom = 0
        }
    }
    private fun isLastRow(
        itemPosition: Int,
        itemCount: Int,
        spanCount: Int,
        lastRowItemCount: Int
    ): Boolean {
        // 如果最后一行的数量不足一整行,则直接判断位置
        if (lastRowItemCount != 0 && itemPosition >= itemCount - lastRowItemCount) {
            return true
        }
        // 如果最后一行的数量足够一整行,则需要计算
        val rowIndex = itemPosition / spanCount
        val totalRow = ceil(itemCount.toDouble() / spanCount).toInt()
        return rowIndex == totalRow - 1
    }
}

当我们填充6个布局后的效果:
在这里插入图片描述

红色区域和45之间的间距通过LastGapDecoration完成。

此时我们移除3后:
在这里插入图片描述
根本原因在于GridLayoutManager#layoutChunk函数中

 
public class GridLayoutManager extends LinearLayoutManager {

 View[] mSet;
 
 //layoutChunk 每次调用只拿取当前行view进行对比计算
 //比如GridLayoutManager一行两个那么每次会拿取每行的对应view进行计算
 void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
       
       
        int count = 0;
        while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
           //略... 经过一些的计算mSet放入本次要进行摆放的view
           //count 一般为GridLayoutManager的spanCount数量
            mSet[count] = view;
            count++;
        }
       
        //maxSize是指在本次layoutChunk中所有view里面最大的高度数据。(包含view自身和ItemDecorations得到的)
        int maxSize = 0;

        // we should assign spans before item decor offsets are calculated
        for (int i = 0; i < count; i++) {
            //计算ItemDecorations
            calculateItemDecorationsForChild(view, mDecorInsets);
            //调用measure计算view宽高 核心!!!
            //核心代码点:注意这里调用子view的measure参数为layoutparameter高度
            //我们把这里称为操作A
            measureChild(view, otherDirSpecMode, false);
            //核心代码点:这里这里会得到这个view的宽高和ItemDecorations填充的高度和
            final int size = mOrientationHelper.getDecoratedMeasurement(view);
            //核心代码点: 记录最大数值
            if (size > maxSize) {
                maxSize = size;
            }
           
        }
        //我们把这里称为操作B
        //取出当前行中的所有view。保证行高度一致
        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);
                    //核心代码点: 这里会强制当前行所有view的高度与最高的view保持一致。       
                    hSpec = View.MeasureSpec.makeMeasureSpec(maxSize - verticalInsets,
                            View.MeasureSpec.EXACTLY);
                } else {
                  //略
                }
                //执行测量
                measureChildWithDecorationsAndMargin(view, wSpec, hSpec, true);
            }
        } 
    }
}    

上面的代码可以总结为:

  1. 取出当前的所有view
  2. 对所有view执行一次高度测量,并记录当前最高的view数据
  3. 在此执行一次测量,保证当前行的所有view高度一致

我们重点再看一眼measureChild函数

 private void measureChild(View view, int otherDirParentSpecMode, boolean alreadyMeasured) {
        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 availableSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize);
        final int wSpec;
        final int hSpec;
        if (mOrientation == VERTICAL) {
            wSpec = getChildMeasureSpec(availableSpaceInOther, otherDirParentSpecMode,
                    horizontalInsets, lp.width, false);
            //mOrientationHelper.getTotalSpace()可以先忽略
            //verticalInsets 就是decorate中的高度和一些margin等数值
            //lp.height如果是wrapcontent那么一返回高度为0的MeasureSpec.UNSPECIFIED
            //lp.height如果不是wrapcontent那么一返回高度为父亲高度减去verticalInsets的MeasureSpec.EXACTLY
            //lp.height如果是一个明确数值那么一返回高度为设置的高度的MeasureSpec.EXACTLY
            //总结getChildMeasureSpec传入布局参数高度和decorate高度
            hSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(), getHeightMode(),
                    verticalInsets, lp.height, true);
        } else {
            //略
        }
        //透传给子view测量
        measureChildWithDecorationsAndMargin(view, wSpec, hSpec, alreadyMeasured);
    }

measureChildWithDecorationsAndMargin函数会根据必要性确定是否要执行子view的测量操作。

private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec,
            boolean alreadyMeasured) {
        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
        final boolean measure;
        if (alreadyMeasured) {
       
            measure = shouldReMeasureChild(child, widthSpec, heightSpec, lp);
        } else {
            measure = shouldMeasureChild(child, widthSpec, heightSpec, lp);
        }
        //根据情况是否执行
        if (measure) {
            child.measure(widthSpec, heightSpec);
        }
    }

 boolean shouldReMeasureChild(View child, int widthSpec, int heightSpec, LayoutParams lp) {
            return !mMeasurementCacheEnabled
                    || !isMeasurementUpToDate(child.getMeasuredWidth(), widthSpec, lp.width)
                    || !isMeasurementUpToDate(child.getMeasuredHeight(), heightSpec, lp.height);
        }
  boolean shouldMeasureChild(View child, int widthSpec, int heightSpec, LayoutParams lp) {
            return child.isLayoutRequested()
                    || !mMeasurementCacheEnabled
                    || !isMeasurementUpToDate(child.getWidth(), widthSpec, lp.width)
                    || !isMeasurementUpToDate(child.getHeight(), heightSpec, lp.height);
        }       

shouldReMeasureChild可以总结为:

  1. 如果没有开启缓存那么一定执行测绘
  2. 如果开启了缓存那么判断之前是否执行过相同参数测量

在了解上面的信息我们可以总结一下流程发现问题:
我们假设假设wrapcotent计算的高度为50
decorate插入的高度为10

插入0时:0执行操作A,不执行操作B

插入1时:

  • 0和1同时执行操作A,不执行操作B。0由于之前测绘过不会触发onmeasure。 1触发onmeasure

插入2时:

  • 0和1同时执行操作A,不执行操作B , 0和1不会触发onmeasure。 2执行操作A并触发onmeasure

插入3时:

  • 0和1同时执行操作A,不执行操作B , 0和1不会触发onmeasure。 3和2执行操作A ,2不会触发onmeasure,3触发onmeasure。

移除1时:

  • 0 和 1 同时执行操作A (0 和1不会触发onmeasure)操作B不会执行(虽然1被移除 但是由于预布局存在还需要进行一次比较)
  • 2 和 3 同时执行操作A (2 和3不会触发onmeasure). 由于2移动第一行不会有decorate高度,因此2执行操作B并触发onmeasure。2 高度为60(移除后2和3虽然不在一行但需要执行预布局)
  • 0和2进行同时执行操作A (0 和2不会触发onmeasure),同时0会被执行操作B把高度填充到60. (虽然2没有decorate的高度 但是上一次预布局引起了2高度错误)
  • 3 同时执行操作A (不会触发onmeasure) 不会触发操作B

解决方案:

val manager = GridLayoutManager(this, 2)
manager.isMeasurementCacheEnabled = false

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/506214.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

论文阅读: Visual Attention Network

Motivation 自注意力机制在2D自然图像领域面临3个挑战&#xff1a; 视二维图像为一维序列。对于高分辨率图像&#xff0c;二次复杂度消耗太大。只捕捉空间适应性&#xff0c;忽略通道适应性。 Contribution 设计了 Large Kernel attention(LKA)&#xff0c;包含卷积和自注意…

SpringBoot整合knife4J 3.0.3

Knife4j的前身是swagger-bootstrap-ui,前身swagger-bootstrap-ui是一个纯swagger-ui的ui皮肤项目。项目正式更名为knife4j,取名knife4j是希望她能像一把匕首一样小巧,轻量,并且功能强悍,更名也是希望把她做成一个为Swagger接口文档服务的通用性解决方案,不仅仅只是专注于前端Ui…

受益于边缘计算的三个关键应用

边缘计算和 5G 网络正在改变物联网&#xff0c;增强跨多个领域的广泛应用的功能&#xff0c;并催生大量新兴应用。我们通过研究三个突出的用例来说明边缘计算的强大功能。 工业4.0智能工厂 工业 4.0 为制造商提供了基于灵活的工业环境提高生产力和盈利能力的愿景&#xff0c;…

5.vector容器的使用

文章目录 vector容器1.构造函数代码工程运行结果 2.赋值代码工程运行结果 3.容量和大小代码工程运行结果 4.插入和删除代码工程运行结果 5.数据存取工程代码运行结果 6.互换容器代码工程运行结果 7.预留空间代码工程运行结果 vector容器 1.构造函数 /*1.默认构造-无参构造*/ …

STM32 can通信部分函数注释

相关截图: CAN模式初始化函数:u8 CAN1_Mode_Init(u8 tsjw,u8 tbs2,u8 tbs1,u16 brp,u8 mode) //CAN初始化 //tsjw:重新同步跳跃时间单元.范围:CAN_SJW_1tq~ CAN_SJW_4tq //tbs2:时间段2的时间单元. 范围:CAN_BS2_1tq~CAN_BS2_8tq; //tbs1:时间段1的时间单元. 范围:CAN_BS…

IO流c++

IO流类库 输入输出流 #include <iostream> using namespace std;class InCount { public:InCount(int a 0, int b 0){c1 a;c2 b;}void show(void){cout << "c1" << c1 << "\t" << "c2" << c2 << …

PHP三种方式读取RSA密钥加解密、签名验签完整教程

目录 第一步、生成公私钥 第二步、三种方式读取RSA密钥 第1种&#xff1a;公私钥弄成一行&#xff0c;必须一行没有空格和换行 第2种&#xff1a;直接复制生成公私钥 第3种;复制密钥存储为.pem文件后缀 第三步、RSA加解密 第四步、RSA签名以及验证签名 第五步、封装完整…

Linux的开发工具(二):编译器gcc/g++与Linux项目自动化构建工具-Makefile

Linux的编译器-gcc/g 基本概念&#xff1a;gcc是专门用来编译c语言的&#xff0c;g可以编译c或c语言 问题一&#xff1a;gcc有时候为什么不能编译带有for循环的c语言源文件&#xff1f; 答&#xff1a;gcc版本过低会不支持for循环等c99标准下的内容 解决方式&#xff1a;gcc…

手搓 Docker Image Creator(DIC)工具(02):预备知识

此节主要简单介绍一下 Docker、Dockerfile 的基本概念&#xff0c;Dockerfile 对的基本语法&#xff0c;Windows 和 macOS 下 Docker 桌面的安装&#xff0c;Docker 镜像的创建和运行测试等。 1 关于 Docker Docker 是一个开源的应用容器引擎&#xff0c;它允许开发者打包应用…

Open3D(C++) 基于随机抽样与特征值法的点云平面稳健拟合方法

目录 一、算法原理1、论文概述2、参考文献二、代码实现三、结果展示四、测试数据本文由CSDN点云侠原创,原文链接。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的GPT爬虫。 一、算法原理 1、论文概述 针对点云数据含有异常值且传统拟合方法拟合结果不理想的情…

《自动机理论、语言和计算导论》阅读笔记:p115-p138

《自动机理论、语言和计算导论》学习第 6 天&#xff0c;p115-p138 总结&#xff0c;总计 24 页。 一、技术总结 1.associativity and comutativity (1)commutativity(交换性): Commutativity is the property of an operator that says we can switch the order of its ope…

Acwing-3418 杨辉三角形

关于杨辉三角形的一些规律&#xff08;更详细地去看参考&#xff09;&#xff1a; 下面这些图都来自其他人所做图片 因为杨辉三角形是对称的&#xff0c;并且与二项式有关&#xff1a; 将左半部分(左半部分的编号肯定比右半部分小&#xff0c;不考虑右半部分&#xff09;一个斜…

如何区分相对路径 与 绝对路径?

在网页中有很多需要使用我们URL路径的场景&#xff0c;包括a标签的href、link标签的href、script标签的src、imag标签的src、form中的action、ajax请求的url等等等等。它们都可以使用相对路径和绝对路径来引入文件&#xff0c;那么&#xff0c;我们如何区分相对路径与绝对路径呢…

MATLAB | 绘图复刻(十六) | 弦图2.1.0版本更新——弦末端弧形块颜色单独设置

Hey, 本人自主开发的弦图绘制工具迎来2.1.0版本了&#xff1a;起因是有粉丝问我前两天发布的文章中这张图咋画&#xff1a; 我本来一想我开发的工具画弦图还是很简单的哇&#xff08;下面文章中有基本用法&#xff09; https://slandarer.blog.csdn.net/article/details/126458…

Vue tree自定义滚动条位置

贴一张效果图&#xff0c;我的效果不方便贴出来 实现支持&#xff1a; 1、懒加载 2、普通加载 下面贴关键思想&#xff1a; document有一个获取element元素的方法。 let element document.getElementById(tree); let arr document.querySelectorAll(".nodelModel&quo…

编曲知识15:重复段落编写 尾奏编写 家庭工作室搭建 硬件设备使用常识

15 重复段落编写 尾奏编写 家庭工作室搭建 硬件设备使用常识小鹅通-专注内容付费的技术服务商https://app8epdhy0u9502.pc.xiaoe-tech.com/live_pc/l_6602a586e4b0694cc051476b?course_id=course_2XLKtQnQx9GrQHac7OPmHD9tqbv 重复段落设计 第二段落指代间奏过后的段落 第二…

uniapp 小程序发布体验版 http://198.18.0.1:7001 不在以下 request 合法域名列表中(踩坑记录二)

问题一&#xff1a; 小程序发布体验版时出现报错信息&#xff1a; http://198.18.0.1:7001 不在以下 request 合法域名列表中无法连接uniCloud本地调试服务&#xff0c;请检查当前客户端是否与主机在同一局域网下 解决方案&#xff1a; 请务必在HBuilderX内使用【发行】菜单打…

上位机图像处理和嵌入式模块部署(qmacvisual寻找圆和寻找直线)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 前面有几篇文章&#xff0c;我们谈到过直线拟合、圆拟合和椭圆拟合。当时&#xff0c;我们的做法是&#xff0c;先找到了轮廓&#xff0c;接着找到…

C++多线程:单例模式与共享数据安全(七)

1、单例设计模式 单例设计模式&#xff0c;使用的频率比较高&#xff0c;整个项目中某个特殊的类对象只能创建一个 并且该类只对外暴露一个public方法用来获得这个对象。 单例设计模式又分懒汉式和饿汉式&#xff0c;同时对于懒汉式在多线程并发的情况下存在线程安全问题 饿汉…

稀碎从零算法笔记Day35-LeetCode:字典序的第K小数字

要考虑完结《稀碎从零》系列了哈哈哈 这道题和【LC.42 接雨水】&#xff0c;我愿称之为【笔试界的颜良&文丑】 题型&#xff1a;字典树、前缀获取、数组、树的先序遍历 链接&#xff1a;440. 字典序的第K小数字 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1…