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

相关推荐

  1. go 基础一些(2)

    2024-04-01 10:48:04       35 阅读
  2. go 基础一些

    2024-04-01 10:48:04       32 阅读
  3. Vue3`ref`和`reactive使用遇到一些

    2024-04-01 10:48:04       56 阅读
  4. 谈谈Mongodb insertMany一些

    2024-04-01 10:48:04       50 阅读
  5. 关于LLaMA Tokenizer一些...

    2024-04-01 10:48:04       52 阅读
  6. Mysql 8.0 一些

    2024-04-01 10:48:04       38 阅读
  7. 安装docker+mysql一些

    2024-04-01 10:48:04       29 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-01 10:48:04       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-01 10:48:04       101 阅读
  3. 在Django里面运行非项目文件

    2024-04-01 10:48:04       82 阅读
  4. Python语言-面向对象

    2024-04-01 10:48:04       91 阅读

热门阅读

  1. 【Go】面向萌新的Gin框架知识梳理学习笔记

    2024-04-01 10:48:04       30 阅读
  2. StatefulSet介绍-更新-扩容缩容-HPA

    2024-04-01 10:48:04       27 阅读
  3. 2024.3.31力扣(1200-1400)刷题记录

    2024-04-01 10:48:04       37 阅读
  4. 著名的分布式数据库

    2024-04-01 10:48:04       30 阅读
  5. 从适用场景看,Spring Boot和Spring的不同

    2024-04-01 10:48:04       38 阅读
  6. Servlet

    Servlet

    2024-04-01 10:48:04      31 阅读
  7. Spring Boot集成Elasticsearch 8.12.2客户端

    2024-04-01 10:48:04       35 阅读
  8. ZooKeeper 负载均衡和 Nginx 负载均衡的区别

    2024-04-01 10:48:04       36 阅读
  9. Docker Swarm入门

    2024-04-01 10:48:04       38 阅读
  10. Redis 的常见问题及解决方案

    2024-04-01 10:48:04       43 阅读
  11. Meme币如何赋能Web3社交?

    2024-04-01 10:48:04       38 阅读
  12. 价值投资已死,MEME币永生?

    2024-04-01 10:48:04       38 阅读