这两天遇到一个需求,一堆按钮要排列成 一个3x2矩阵,中间几个可能会消失,后边的按钮依次往前递进占位,按照之前的相对布局约束变换来做的话就会非常复杂,要写一对逻辑控制约束,于是想到了网格布局,网格布局有两种:

  • RecyclerView搭配GirdLayoutManager非常灵活

  • GirdLayout网格布局,功能简单,适配相对简单

由于按钮比较少,根据网上的分析,GridLayout性能相对较好,这里记录下GridLayout的用法


在xml中写入固定元素

每个元素有相同的宽高,还指定了grid最大行数和列数

<GridLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:columnCount="2"
        android:rowCount="5">
    <Button
            android:layout_height="20dp"
            android:layout_width="40dp"/>
    <Button
            android:layout_rowSpan="2"
            android:layout_height="40dp"
            android:layout_width="40dp"/>
    <Button
            android:layout_height="20dp"
            android:layout_width="40dp"/>
    <Button
            android:layout_height="20dp"
            android:layout_width="40dp"/>
    <Button
            android:layout_height="20dp"
            android:layout_width="40dp"/>
</GridLayout>

跨行

第二个元素跨两行

android:layout_rowSpan="2"

一个3x2布局结构像这样

代码动态添加子view

如果有一些View在某些条件下是需要隐藏的,那么通过代码动态添加就很好处理了,接下来跟着代码算法捋一遍思路

定义变量

    // gridLayout 
    private lateinit var mGridLayout: GridLayout
    // gridLayout填充元素列表
    private val gridItems: ArrayList<View> = ArrayList(5)

函数解析

构造一个populateGridLayout函数,传入gridLayout, 清除所有子元素,避免元素重复

    private fun populateGridLayout(grid: GridLayout) {
        // 先清除所有元素, 指定最大5行x2列
        grid.apply {
            removeAllViews()
            this.columnCount = 2
            rowCount = 5
        }
        ....

最终添加view接口是gird.addView(child, param), 这里最重要的就是构建Param参数

首先对子View列表遍历:gridItems.forEach, 每个子View对应一个currentRowcurrentCol,最终会根据这两个变量去生成param,同时我们根据下标算出当前元素是否跨行rowSpan

在循环的最后,更新下一个标位置

      private fun populateGridLayout(grid: GridLayout) {
          ....
        var currentRow = 0
        var currentCol = 0
        gridItems.forEach { item ->
            //如果出现重复添加元素先清除parent,否则添加失败抛出异常
            item.parent?.let {
                (it as ViewGroup).removeView(item)
            }
            // 跨行处理:第二个元素跨两行
            val rowSpan = if (gridItems.indexOf(item) == 1) 2 else 1
            // 坐标[0,1]跨列之后,[1,1]位置不可用,跳过该点
            if (currentRow == 1 && currentCol == 1) {
                currentRow ++
                currentCol = 0
            }
            // 创建布局参数
            val params = createLayoutParams(currentRow, currentCol, rowSpan)
            grid.addView(item, params)
            // 更新位置计数器
            currentCol++
            if (currentCol >= COLUMN_COUNT) {
                currentRow ++
                currentCol = 0
            }
        }

构造LayoutParams

元素在gridLayout中定位是通过 currentRow,currentCol 共同决定的,可以理解为坐标[x,y]

函数中的rowSpec第一个参数表示x行,第二个参数表示跨的行,默认1,比如我们的需求是第一行第二列元素跨两行,则构造参数为:

rowSpec=GridLayout.spec(0,2),

columnSpec=GridLayout.spec(1, 1)

表示添加到位置[0,1]的元素跨2行,1列

    private fun createLayoutParams(
        currentRow: Int,
        currentCol: Int,
        rowSpan: Int
    ): GridLayout.LayoutParams {
        return GridLayout.LayoutParams().apply {
            // 核心参数设置
            rowSpec = GridLayout.spec(currentRow, rowSpan)
            columnSpec = GridLayout.spec(currentCol, 1)
            // 非第一行,top margin
            if (notFirstColumn(currentRow)) {
                topMargin = binding.root.context.dpToPx(MARGIN_TOP)
            }
            if (currentCol >0 && currentRow > 1 ) {
                leftMargin = binding.root.context.dpToPx(MARGIN_TOP)
            }
        }
    }

同时我们还可以设置每个元素的边距

GridLayout子元素坐标说明

对应二维数组如下,蓝色字体为跨两行元素

[0,0]

[0,1]

[1,0]

[1,1]

[2,0]

[2,1]

[3,0]

[3,1]

回到populateGridLayout函数中,假设我们添加了[0,0],[0,1],[1,0]三个元素,当下试图将第4个元素添加到[1,1]的时候,我们检测出当前坐标被占用,只能将第4个元素添加到[2,0], 如果仍然将第4个元素添加到[1,1],效果就是第4个元素重叠在第2个元素之上,判断条件如下

            // 坐标[0,1]跨列之后,[1,1]位置不可用,跳过该点
            if (currentRow == 1 && currentCol == 1) {
                currentRow ++
                currentCol = 0
            }

元素自动递补

假设有6个元素,删除第4,5个元素, 那么第6个元素会自动移动到第4个位置, 我们只需要删除gridItems列表中的元素,然后调用populateGridLayout函数刷新View

排序

同时我们还可以对列表元素进行排序,由于是View,所以我们可以通过id进行排序

    private fun sortGridItems() {
        //添加了一个自定义的Comparator
        gridItems.sortWith { v1, v2 ->
            // 获取视图在顺序列表中的索引(未定义的视图排在最后)
            val index1 = positionOrder.indexOf(v1.id).takeIf { it != -1 } ?: Int.MAX_VALUE
            val index2 = positionOrder.indexOf(v2.id).takeIf { it != -1 } ?: Int.MAX_VALUE
            index1.compareTo(index2)
        }
    }

    // 定义元素位置顺序
    private val positionOrder by lazy {
        listOf(
            view1.id,
            view2.id,
            view3.id,
            view4.id,
            view5.id,
            view6.id,
            view7.id
        )
    }

春风花气馥,秋月寒江湛