这两天遇到一个需求,一堆按钮要排列成 一个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对应一个currentRow和currentCol,最终会根据这两个变量去生成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子元素坐标说明
对应二维数组如下,蓝色字体为跨两行元素
回到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
)
}