想啥呢
梦中“王哥”
你的梦中网格是不是这样的:
- 单行展示,数据超出截取
- 2(3)行展示,相同行等高
支持功能
垂直网格布局
- 支持两列和三列
- 支持设置列间距和行间距
- 支持动态添加子项
- 支持子项相同行等高
使用方式
@Preview(showBackground = true)
@Composable
private fun VerticalGridPreview() {
val jobTypes = listOf(
"房地产交易",
"房产销售",
"销售顾问",
"大客户代表",
"电话销售",
"销售工程师",
"网络销售网络销售网络销售网络销售",
"商品销售",
"外贸业务员",
"普工/技工",
"普工/操作工",
"化学原料/化学制品化学原料/化学制品化学原料/化学制品化学原料/化学制品",
"化工",
"物业/安保",
"保安",
"物业经理",
"物业维修",
"物业管理员"
)
AndroidXComposeTheme {
VerticalGrid(columns = 2, horizontalSpacing = 12.dp, verticalSpacing = 12.dp) {
items(
items = jobTypes,
anchor = { it },
) {
Text(
text = it,
textAlign = TextAlign.Center,
style = TextStyle(textAlign = TextAlign.Center)
)
}
}
}
}
参数说明
VerticalGrid
- modifier: Modifier = Modifier, // 修饰符
- columns: Int = 2, // 列数,目前只支持2,3
- horizontalSpacing: Dp = 0.dp, 水平间距
- verticalSpacing: Dp = 0.dp, // 垂直间距
- content: GridScope.() -> Unit = {} // items
GridScope
- items: List, // 列表数据
- noinline anchor: ((item: T) -> String)? = null, // 约束对象
- crossinline itemContent: @Composable GridScope.(item: T) -> Unit // 内容
源码
VerticalGrid
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ChainStyle
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import com.zl.androidxcompose.ui.theme.AndroidXComposeTheme
import kotlin.math.roundToInt
/**
* A vertical grid layout that places its children in a vertical grid.
* 垂直网格布局:
* 1.支持两列和三列
* 2.支持设置列间距和行间距
* 3.支持动态添加子项
* 4.支持子项相同行等高
* @param modifier 修饰符
* @param columns 列
* @param content 内容区
* @param horizontalSpacing 水平间距
* @param verticalSpacing 垂直间距
* @author Guo
* @date 2024-05-14
*/
@Composable
fun VerticalGrid(
modifier: Modifier = Modifier,
columns: Int = 2,
horizontalSpacing: Dp = 0.dp,
verticalSpacing: Dp = 0.dp,
content: GridScope.() -> Unit = {}
) {
val gridScope = object : GridScope {
val itemsList = mutableListOf<@Composable () -> Unit>()
val anchorObj = mutableListOf<String>()
override fun items(
count: Int,
anchor: ((index: Int) -> String)?,
contentType: (index: Int) -> Any?,
itemContent: @Composable GridScope.(index: Int) -> Unit
) {
itemsList.clear()
anchorObj.clear()
for (i in 0 until count) {
anchor?.let { anchorObj.add(anchor(i)) }
itemsList.add { itemContent(i) }
}
}
}
gridScope.content()
val range = (gridScope.itemsList.size / (columns.toDouble())).roundToInt()
Column(modifier = modifier) {
if (columns == 2) {
for (i in 0 until range) {
val leftContent = gridScope.itemsList[i * 2]
val rightContent =
if (i * 2 + 1 < gridScope.itemsList.size) gridScope.itemsList[i * 2 + 1] else {
{ Box {} }
}
val anchor = if (gridScope.anchorObj.isEmpty()) {
GridAnchor.NONE
} else {
val leftSize = gridScope.anchorObj[i * 2].length
val rightSize =
if (i * 2 + 1 < gridScope.anchorObj.size) gridScope.anchorObj[i * 2 + 1].length else 0
when {
leftSize > rightSize -> GridAnchor.Left
else -> GridAnchor.Right
}
}
VerticalGridItem(anchor, verticalSpacing, leftContent, rightContent)
if (i != range - 1) Spacer(modifier = Modifier.padding(vertical = horizontalSpacing / 2))
}
} else {
for (i in 0 until range) {
val leftContent = gridScope.itemsList[i * 3]
val centerContent =
if (i * 3 + 1 < gridScope.itemsList.size) gridScope.itemsList[i * 3 + 1] else {
{ Box {} }
}
val rightContent =
if (i * 3 + 2 < gridScope.itemsList.size) gridScope.itemsList[i * 3 + 2] else {
{ Box {} }
}
val anchor = if (gridScope.anchorObj.isEmpty()) {
GridAnchor.NONE
} else {
val leftSize = gridScope.anchorObj[i * 3].length
val centerSize =
if (i * 3 + 1 < gridScope.anchorObj.size) gridScope.anchorObj[i * 3 + 1].length else 0
val rightSize =
if (i * 3 + 2 < gridScope.anchorObj.size) gridScope.anchorObj[i * 3 + 2].length else 0
when {
leftSize > centerSize && leftSize > rightSize -> GridAnchor.Left
centerSize > leftSize && centerSize > rightSize -> GridAnchor.Center
else -> GridAnchor.Right
}
}
VerticalGridItem(anchor, verticalSpacing, leftContent, centerContent, rightContent)
if (i != range - 1) Spacer(modifier = Modifier.padding(vertical = horizontalSpacing / 2))
}
}
}
}
@Composable
private fun VerticalGridItem(
anchor: GridAnchor = GridAnchor.NONE,
horizontalSpacing: Dp = 0.dp,
leftContent: @Composable () -> Unit,
centerContent: @Composable () -> Unit,
rightContent: @Composable () -> Unit
) {
ConstraintLayout(modifier = Modifier.fillMaxWidth()) {
// Create references for the composables to constrain
val (left, center, right) = createRefs()
Box(
modifier = Modifier
.padding(end = horizontalSpacing / 2)
.constrainAs(left) {
top.linkTo(
when (anchor) {
GridAnchor.Left, GridAnchor.NONE -> parent.top
GridAnchor.Center -> center.top
else -> right.top
}
)
start.linkTo(parent.start)
bottom.linkTo(
when (anchor) {
GridAnchor.Left, GridAnchor.NONE -> parent.bottom
GridAnchor.Center -> center.bottom
else -> right.bottom
}
)
width = Dimension.fillToConstraints
height = when (anchor) {
GridAnchor.Left, GridAnchor.NONE -> Dimension.wrapContent
else -> Dimension.fillToConstraints
}
},
contentAlignment = Alignment.Center,
) {
leftContent()
}
Box(
modifier = Modifier
.padding(horizontal = horizontalSpacing / 2)
.constrainAs(center) {
top.linkTo(
when (anchor) {
GridAnchor.Left -> left.top
GridAnchor.Center, GridAnchor.NONE -> parent.top
else -> right.top
}
)
start.linkTo(left.end)
end.linkTo(right.start)
bottom.linkTo(
when (anchor) {
GridAnchor.Left -> left.bottom
GridAnchor.Center, GridAnchor.NONE -> parent.bottom
else -> right.bottom
}
)
width = Dimension.fillToConstraints
height = when (anchor) {
GridAnchor.Center, GridAnchor.NONE -> Dimension.wrapContent
else -> Dimension.fillToConstraints
}
},
contentAlignment = Alignment.Center
) {
centerContent()
}
Box(
modifier = Modifier
.padding(start = horizontalSpacing / 2)
.constrainAs(right) {
top.linkTo(
when (anchor) {
GridAnchor.Left -> left.top
GridAnchor.Center -> center.top
else -> parent.top
}
)
start.linkTo(center.end)
end.linkTo(parent.end)
bottom.linkTo(
when (anchor) {
GridAnchor.Left -> left.bottom
GridAnchor.Center -> center.bottom
else -> parent.bottom
}
)
width = Dimension.fillToConstraints
height = when (anchor) {
GridAnchor.Right -> Dimension.wrapContent
else -> Dimension.fillToConstraints
}
},
contentAlignment = Alignment.Center
) {
rightContent()
}
createHorizontalChain(left, center, right, chainStyle = ChainStyle.Spread)
}
}
@Composable
private fun VerticalGridItem(
anchor: GridAnchor = GridAnchor.NONE,
verticalSpacing: Dp = 0.dp,
leftContent: @Composable () -> Unit,
rightContent: @Composable () -> Unit,
) {
ConstraintLayout(modifier = Modifier.fillMaxWidth()) {
// Create references for the composables to constrain
val (left, right) = createRefs()
Box(
modifier = Modifier
.padding(end = verticalSpacing / 2)
.constrainAs(left) {
top.linkTo(
when (anchor) {
GridAnchor.Left, GridAnchor.NONE -> parent.top
else -> right.top
}
)
start.linkTo(parent.start)
bottom.linkTo(
when (anchor) {
GridAnchor.Left, GridAnchor.NONE -> parent.bottom
else -> right.bottom
}
)
width = Dimension.fillToConstraints
height = when (anchor) {
GridAnchor.Left, GridAnchor.NONE -> Dimension.wrapContent
else -> Dimension.fillToConstraints
}
},
contentAlignment = Alignment.Center,
) {
leftContent()
}
Box(
modifier = Modifier
.padding(start = verticalSpacing / 2)
.constrainAs(right) {
top.linkTo(
when (anchor) {
GridAnchor.Left -> left.top
else -> parent.top
}
)
start.linkTo(left.end)
end.linkTo(parent.end)
bottom.linkTo(
when (anchor) {
GridAnchor.Left -> left.bottom
else -> parent.bottom
}
)
width = Dimension.fillToConstraints
height = when (anchor) {
GridAnchor.Left -> Dimension.fillToConstraints
else -> Dimension.wrapContent
}
},
contentAlignment = Alignment.Center
) {
rightContent()
}
createHorizontalChain(left, right, chainStyle = ChainStyle.Spread)
}
}
GridScope
import androidx.compose.runtime.Composable
/**
* Receiver scope which is used by VerticalGrid.
* @see VerticalGrid
* 网格布局的作用域
* @author Guo
* @date 2024-05-14
*/
interface GridScope {
fun items(
count: Int,
anchor: ((index: Int) -> String)? = null,
contentType: (index: Int) -> Any? = { null },
itemContent: @Composable GridScope.(index: Int) -> Unit
) {
error("The method is not implemented")
}
}
inline fun <T> GridScope.items(
items: List<T>,
noinline anchor: ((item: T) -> String)? = null,
noinline contentType: (item: T) -> Any? = { null },
crossinline itemContent: @Composable GridScope.(item: T) -> Unit
) = items(
count = items.size,
anchor = if (anchor != null) { index: Int -> anchor(items[index]) } else null,
contentType = { index: Int -> contentType(items[index]) }
) {
itemContent(items[it])
}
inline fun <T> GridScope.items(
items: Array<T>,
noinline anchor: ((item: T) -> String)? = null,
noinline contentType: (item: T) -> Any? = { null },
crossinline itemContent: @Composable GridScope.(item: T) -> Unit
) = items(
count = items.size,
anchor = if (anchor != null) { index: Int -> anchor(items[index]) } else null,
contentType = { index: Int -> contentType(items[index]) }
) {
itemContent(items[it])
}
实现原理
约束布局-约束布局-约束布局
约束布局-约束布局-约束布局