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.mzph.cn/news/784264.shtml

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

相关文章

DAY10,DAY11|逆波兰表达式,有效括号,删除字符串中重复项

文章目录 232.用栈实现队列225.用队列实现栈20.有效的括号1047.删除字符串中的所有重复项150.逆波兰表达式 232.用栈实现队列 文档讲解&#xff1a;用栈实现队列 视频链接&#xff1a;代码随想录-用栈实现队列 状态&#xff1a;ok 思路&#xff1a; 用两个栈实现队列操作&…

大模型面试准备(十一):怎样让英文大语言模型可以很好的支持中文?

节前&#xff0c;我们组织了一场算法岗技术&面试讨论会&#xff0c;邀请了一些互联网大厂朋友、参加社招和校招面试的同学&#xff0c;针对大模型技术趋势、大模型落地项目经验分享、新手如何入门算法岗、该如何备战、面试常考点分享等热门话题进行了深入的讨论。 合集在这…

设计模式(11):适配器模式

一.什么使适配器模式&#xff1f; 将一个类的接口转换成客户希望的另外一个接口&#xff0c;Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作。 二.模式中的角色 目标角色(Target)&#xff1a; 客气所期待的接口。目标可以是具体的或抽象的类&#…

C++20中的jthread

一、多线程开发 c11以前&#xff0c;是不包含线程库的&#xff0c;开发多线程&#xff0c;必须使用OS系统自带的线程API&#xff0c;这就导致了很多问题&#xff0c;最主要的是&#xff0c;跨平台的开发&#xff0c;一般要保持几个主流应用的库的代码支持&#xff0c;特别是对…

关于缓存的一些问题

关于缓存的一些问题 1、缓存穿透 什么是缓存穿透&#xff1a; 缓存穿透指的是在使用缓存系统的过程中&#xff0c;对于不存在的数据不断地进行查询请求&#xff0c;导致这些请求都无法从缓存中获取到数据&#xff0c;最终达到了绕过缓存的目的&#xff0c;直接访问后端数据源…

C语言-文件操作函数基础+进阶标准输入流输出流

学习的流程 ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————…

RedisDesktopManager 安装

简介&#xff1a;安装redis可视化工具 一、下载压缩包 Redis 可视化工具 链接&#xff1a;https://pan.baidu.com/s/1P2oZx9UpQbXDsxJ3GPUeOQ 提取码&#xff1a;6rft Redis 命令窗口版本 链接&#xff1a;https://pan.baidu.com/s/1mIuxCEWwD__aoqp1Cx8MFQ 提取码&#xf…

Lucene及概念介绍

Lucene及概念介绍 基础概念倒排索引索引合并分析查询语句的构成 基础概念 Document&#xff1a;我们一次查询或更新的载体&#xff0c;对比于实体类 Field&#xff1a;字段&#xff0c;是key-value格式的数据&#xff0c;对比实体类的字段 Item&#xff1a;一个单词&#xff0…

如何做一个知识博主? 行动

任何事情都需要经过两次创造 提前做一个预演&#xff0c;计划 【以一个计划开始】 你的解决方案究竟将怎样应用? 要根据目标想到所有的关键因素并列出所有的步骤和所有重要的细节。 需要想清楚这个事情成事最核心的胜负手关键因素&#xff1f; 【抓关键】 涨粉&#xff1a…

Decoupled Multimodal Distilling for Emotion Recognition 论文阅读

Decoupled Multimodal Distilling for Emotion Recognition 论文阅读 Abstract1. Introduction2. Related Works2.1. Multimodal emotion recognition2.2. Knowledge distillation3. The Proposed Method3.1. Multimodal feature decoupling3.2. GD with Decoupled Multimodal …

简单易懂的SQL添加数据教程

1. 引言&#xff1a; SQL&#xff08;Structured Query Language&#xff09;是一种处理关系型数据库的标准语言&#xff0c;包括插入&#xff08;INSERT)、更新&#xff08;UPDATE&#xff09;、删除&#xff08;DELETE&#xff09;等操作。在数据库操作中&#xff0c;添加数…

基于muduo网络库实现的集群聊天服务器

目录 项目内容开发环境安装说明技术介绍项目目录数据库设计项目介绍启动服务器启动客户端注册账号登录成功一对一聊天业务创建群聊业务加入群聊业务群聊业务添加好友业务离线消息存储业务 特殊说明 &#xff01;&#xff01;&#xff01;项目是照着腾讯课堂施磊老师的视频学习&…

NC20128 不重复数字

题目描述 给出N个数&#xff0c;要求把其中重复的去掉&#xff0c;只保留第一次出现的数。 例如&#xff0c;给出的数为1 2 18 3 3 19 2 3 6 5 4&#xff0c;其中2和3有重复&#xff0c;去除后的结果为1 2 18 3 19 6 5 4。 输入描述: 输入第一行为正整数T&#xff0c;表示…

04---java面试八股文——spring-----注解-------10题

31、ComponentScan注解的作用 ComponentScan 注解用于指定 Spring 容器在启动时要扫描的基础包路径&#xff0c;以及要扫描的包路径下应该被自动注册为 Spring bean 的类。 具体来说&#xff0c;ComponentScan 注解的作用有以下几个方面&#xff1a; 组件扫描&#xff1a;Comp…

docker部署DOS游戏

下载镜像 docker pull registry.cn-beijing.aliyuncs.com/wuxingge123/dosgame-web-docker:latestdocker-compose部署 vim docker-compose.yml version: 3 services:dosgame:container_name: dosgameimage: registry.cn-beijing.aliyuncs.com/wuxingge123/dosgame-web-docke…

How to install JDK on mac

文章目录 1. Install JDK on mac2. zshenv, zshrc, zprofile3. 查看java环境变量配置 1. Install JDK on mac Installation of the JDK on macOS 2. zshenv, zshrc, zprofile How Do Zsh Configuration Files Work? 3. 查看java环境变量配置 open Terminal&#xff0c;cd…

02-JDK新特性-Lambda表达式

JDK新特性 Lambda表达式 什么是Lambda表达式 Lambda表达式是一个匿名代码块&#xff0c;用于简单的传递一段代码片段。 Lambda表达式标准格式 格式&#xff1a;(形式参数) -> {代码块} 形式参数 如果有多个参数&#xff0c;参数只见用逗号隔开&#xff1b;如果没有&…

普通数据库索引与搜索引擎的索引有何区别

普通数据库索引&#xff0c;如关系型数据库&#xff08;RDBMS&#xff09;中的B树&#xff08;B-tree&#xff09;或哈希索引&#xff0c;与搜索引擎使用的倒排索引&#xff08;Inverted Index&#xff09;之间存在几个关键区别&#xff1a; 数据结构&#xff1a; 普通数据库索…

【Linux 10】环境变量

文章目录 &#x1f308; Ⅰ 命令行参数⭐ 1. main 函数的参数⭐ 2. main 函数参数的意义⭐ 3. 查看 argv 数组的内容⭐ 4. 命令行参数结论⭐ 5. 为什么要有命令行参数⭐ 6. 命令行参数传递由谁执行 &#x1f308; Ⅱ 环境变量基本概念⭐ 1. 常见环境变量 &#x1f308; Ⅲ 查看…

macOS Catalina for mac (macos 10.15系统)v10.15.7正式版

macOS Catalina是苹果公司专为麦金塔电脑推出的桌面操作系统&#xff0c;是macOS的第16个主要版本。它继承了苹果一贯的优雅与高效&#xff0c;不仅引入了分割视图和侧边栏&#xff0c;还带来了全新的音乐和播客应用&#xff0c;极大地提升了用户体验。在隐私保护和安全性方面&…