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.MaterialCardViewandroid:layout_width="match_parent"app:layout_constraintTop_toTopOf="parent"app:cardCornerRadius="10dp"app:cardBackgroundColor="#908000"android:layout_height="240dp"><TextViewandroid: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 ?: returnval spanCount = gridLayoutManager.spanCountval itemCount = gridLayoutManager.itemCountif (spanCount <= 0) {return}val lastRowItemCount = itemCount % spanCountval 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 / spanCountval 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 calculatedfor (int i = 0; i < count; i++) {//计算ItemDecorationscalculateItemDecorationsForChild(view, mDecorInsets);//调用measure计算view宽高 核心!!!//核心代码点:注意这里调用子view的measure参数为layoutparameter高度//我们把这里称为操作AmeasureChild(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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

arm交叉编译器工具

下载地址&#xff1a; Builds & Downloads | Linaro 进入首页后&#xff0c;点击" GNU Toolchain Integration Builds" 有以下版本&#xff1a; 根据自己的选择下载对应的版本&#xff0c;本例选择14.0-2023.06-1 根据板端对应的版本选择相应的下载 比如下载3…

来个自定义的电子木鱼吧

<!DOCTYPE html> <html><head><meta charset"utf-8"><meta name"viewport" content"widthdevice-width, initial-scale1"><title>自定义木鱼</title> </head> <body style"background-…

R语言中的常用数据结构

目录 R对象的基本类型 R对象的属性 R的数据结构 向量 矩阵 数组 列表 因子 缺失值NA 数据框 R的数据结构总结 R语言可以进行探索性数据分析&#xff0c;统计推断&#xff0c;回归分析&#xff0c;机器学习&#xff0c;数据产品开发 R对象的基本类型 R语言对象有五…

运筹学基础(三):求解整数规划的切平面法(cutting plane method)

文章目录 算法思想一个例子参考文档 算法思想 先将整数规划问题松弛为线性规划问题&#xff0c;然后割掉线性规划问题可行域的一部分&#xff08;只包含非整数解&#xff09;&#xff0c;使得线性规划问题的最优解在原整数规划问题的可行域某顶点上取得。 因此&#xff0c;割平…

内存管理是如何影响系统的性能的

大家好&#xff0c;今天给大家介绍内存管理是如何影响系统的性能的&#xff0c;文章末尾附有分享大家一个资料包&#xff0c;差不多150多G。里面学习内容、面经、项目都比较新也比较全&#xff01;可进群免费领取。 内存管理对系统性能的影响至关重要&#xff0c;主要体现在以下…

揭开AI编程语言Mojo比Pyhon快6.8万倍的5个秘密!

最近&#xff08;2024年3月29日&#xff09;&#xff0c;号称比Python快6.8万倍的Mojo编程语言开源啦&#xff01;6.8万倍&#xff1f;你敢相信这个数字是真的吗&#xff1f;不过&#xff0c;就连Mojo官网都把这个结果贴了出来&#xff08;见下图&#xff09;&#xff0c;这就很…

瀚海贫者福,铜子恣意游

上学时打饭追求性价比的习惯一直不改&#xff0c;半个大鱼头三块钱&#xff0c;一份豆腐一块钱&#xff0c;还有一个红烧茄子2块5&#xff0c;再加三毛钱的饭&#xff0c;共6块8毛钱&#xff0c;早晚餐也会有这类性价比高又营养的选择&#xff0c;科大食堂现在越来越人性化&…

计算机视觉的应用26-关于Fast-R-CNN模型的应用场景,Fast-R-CNN模型结构介绍

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下计算机视觉的应用26-关于Fast-R-CNN模型的应用场景&#xff0c;Fast-R-CNN模型结构介绍。Fast R-CNN是一种深度学习模型&#xff0c;主要用于目标检测任务&#xff0c;尤其适用于图像中物体的识别与定位。该模型在基…

go包下载时报proxyconnect tcp: dial tcp 127.0.0.1:80: connectex错误的解决方案

一大早的GoLand就开始抽风了&#xff0c;好几个文件import都红了&#xff0c;于是我正常操作点击提示的sync&#xff0c;但是却报了一堆错&#xff1a; go: downloading google.golang.org/grpc v1.61.1 go: downloading google.golang.org/genproto v0.0.0-20240228224816-df9…

阿里云数据库服务器价格表查询,2024年最新

阿里云数据库服务器价格表&#xff0c;优惠99元一年起&#xff0c;ECS云服务器2核2G、3M固定带宽、40G ESSD Entry云盘&#xff0c;优惠价格99元一年&#xff1b;阿里云数据库MySQL版2核2G基础系列经济版99元1年、2核4GB 227.99元1年&#xff0c;云数据库PostgreSQL、SQL Serve…

Java中线程详解

文章目录 相关概念多线程概念实现方式继承Thread类实现Runnable接口比较 常用方法线程安全产生的原因解决思想同步同步代码块同步方法Lock锁机制 死锁概念避免 状态线程间的通讯介绍方法 相关概念 并行&#xff1a;在同一时刻&#xff0c;有多个任务在多个CPU上同时执行并发&a…

Docker 笔记

1.Ubuntu安装Docker 安装Docker看这篇文章 http://t.csdnimg.cn/IsSsJ 2.在docker中运行python代码 2.1搭建python环境 docker部署python环境看这篇文章 http://t.csdnimg.cn/TYz0G 2.2在python shell中运行python代码 2.2.1查看镜像 2.2.1启动python&#xff0c;厦门这个…

MHA高可用-解决MySQL主从复制的单点问题

目录 一、MHA的介绍 1&#xff0e;什么是 MHA 2&#xff0e;MHA 的组成 2.1 MHA Node&#xff08;数据节点&#xff09; 2.2 MHA Manager&#xff08;管理节点&#xff09; 3&#xff0e;MHA 的特点 4. MHA工作原理总结如下&#xff1a; 二、搭建 MySQL MHA 实验环境 …

JUC:synchronized优化——锁的升级过程(偏向锁->轻量级锁->重量级锁)以及内部实现原理

文章目录 锁的类型轻量级锁重量级锁自旋优化偏向锁偏向锁的细节偏向锁的撤销批量重偏向批量撤销锁消除 锁的类型 重量级锁、轻量级锁、偏向锁。 加锁过程&#xff1a;偏向->轻量级->重量级 轻量级锁 轻量级锁的使用场景&#xff1a;如果一个对象虽然有多线程要加锁&am…

【爬虫框架Scrapy】02 Scrapy入门案例

接下来介绍一个简单的项目&#xff0c;完成一遍 Scrapy 抓取流程。通过这个过程&#xff0c;我们可以对 Scrapy 的基本用法和原理有大体了解。 1. 本节目标 本节要完成的任务如下。 创建一个 Scrapy 项目。 创建一个 Spider 来抓取站点和处理数据。 通过命令行将抓取的内容…

HTTP——Cookie

HTTP——Cookie 什么是Cookie通过Cookie访问网站 我们之前了解了HTTP协议&#xff0c;如果还有小伙伴还不清楚HTTP协议&#xff0c;可以点击这里&#xff1a; https://blog.csdn.net/qq_67693066/article/details/136895597 我们今天来稍微了解一下HTTP里面一个很小的部分&…

面试题:RabbitMQ 消息队列中间件

1. 确保消息不丢失 生产者确认机制 确保生产者的消息能到达队列&#xff0c;如果报错可以先记录到日志中&#xff0c;再去修复数据持久化功能 确保消息未消费前在队列中不会丢失&#xff0c;其中的交换机、队列、和消息都要做持久化消费者确认机制 由spring确认消息处理成功后…

算法基础--递推

&#x1f600;前言 递推算法在计算机科学中扮演着重要的角色。通过递推&#xff0c;我们可以根据已知的初始条件&#xff0c;通过一定的规则推导出后续的结果&#xff0c;从而解决各种实际问题。本文将介绍递推算法的基础知识&#xff0c;并通过一些入门例题来帮助读者更好地理…

Redis 全景图(2)---- 关于 Redis 的三“高”

前言 我们继续写第一篇文章没写完的。其实我也不想将我写的一篇 Redis 文章分成几篇中短文来写&#xff0c;但是没办法&#xff0c;我一次写个1万字&#xff0c;会限流&#xff0c;所以将就一下吧。 上篇文章我用了 Redis 的6大模块这个思路来描绘我脑子中的 Redis。其实这6大…

AI学习-线性回归推导

线性回归 1.简单线性回归2.多元线性回归3.相关概念熟悉4.损失函数推导5.MSE损失函数 1.简单线性回归 ​ 线性回归&#xff1a;有监督机器学习下一种算法思想。用于预测一个或多个连续型目标变量y与数值型自变量x之间的关系,自变量x可以是连续、离散&#xff0c;但是目标变量y必…