最近在学compose的滑动列表布局,查看api发现没有提供类似瀑布流
相关的,于是打算了解下compose的自定义view,毕竟我对自定义view还是蛮感兴趣的【狗头】,先上效果图 ,自定义竖向滑动的瀑布流布局
一、简介
借助网图介绍下什么是瀑布流式布局:大家有没有发觉这些照片分成左右、最多的还有分成三列的,但是每一行的照片、视频、内容的高度是"错开"的
在Android中我们的recycleview
的StaggeredGridLayoutManager
可以轻松帮助我们实现该效果,但是在Compose却无法轻易实现因为它只提供了类似,之后官方才推出瀑布流,所以这里学习一种自定义的思想
- 竖向滑动
LinearLayoutManager
的LazyColumn
- 水平滑动
LinearLayoutManager
的LazyRow
- 网格布局竖向滑动
GridLayoutManager
的LazyVerticalGrid
- 网格布局水平滑动
GridLayoutManager
的LazyHorizontalGrid
所以要实现 StaggeredGridLayoutManager
需要自定义ViewGroup
了
二、思路
根据观察可知瀑布流,其实就是将布局填充到内容较少的一列去,根据这个特性就可以去定位排列布局了。 思路,每次定位childview
的时候比较所有列的高度,找出最低的列将当前的child定位在下面
,这样的话我们就确定的view的高度位置
,宽度的话我们根据ViewGroup
的宽度以及需要的列数量进行平均分布
三、学习compose的自定义viewgroup的相关知识点
在 Jetpack Compose 中使用Layout
可组合,可以使用可组合实现布局视图Layout,其定义为:
less
/**
* [Layout] is the main core component for layout. It can be used to measure and position
* zero or more layout children.
*
* The measurement, layout and intrinsic measurement behaviours of this layout will be defined
* by the [measurePolicy] instance. See [MeasurePolicy] for more details.
*
* For a composable able to define its content according to the incoming constraints,
* see [androidx.compose.foundation.layout.BoxWithConstraints].
*
* Example usage:
* @sample androidx.compose.ui.samples.LayoutUsage
*
* Example usage with custom intrinsic measurements:
* @sample androidx.compose.ui.samples.LayoutWithProvidedIntrinsicsUsage
*
* @param content The children composable to be laid out.
* @param modifier Modifiers to be applied to the layout.
* @param measurePolicy The policy defining the measurement and positioning of the layout.
*
* @see Layout
* @see MeasurePolicy
* @see androidx.compose.foundation.layout.BoxWithConstraints
*/
@Suppress("ComposableLambdaParameterPosition")
@UiComposable
@Composable inline fun Layout(
content: @Composable @UiComposable () -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
) {
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val viewConfiguration = LocalViewConfiguration.current
ReusableComposeNode<ComposeUiNode, Applier<Any>>(
factory = ComposeUiNode.Constructor,
update = {
set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
set(density, ComposeUiNode.SetDensity)
set(layoutDirection, ComposeUiNode.SetLayoutDirection)
set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
},
skippableUpdate = materializerOf(modifier),
content = content
)
}
翻译可得:
- content -要布局的子组件。
- modifier -应用于布局的修饰符。
- measurePolicy------定义布局的度量和定位的策略。(其实就是对
childview
进行测量和摆放操作的,是一个lambda表达式,调用每个子组件的 measure(),传递约束条件,得到子组件的大小和位置信息。 - 根据自定义逻辑决定 Layout 的大小(调用 layout() 设置)。 - 为每个子组件调用 place(),放置它们的位置。
常规操作如下:
less
@Composable
fun CustomLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
){ measurables, constraints ->
// 对 children 进行测量和放置
val placeables = measurables.mapIndexed { index, measurable ->
//标准流程:测量每个 child 尺寸,也可以如下重新约束child 的宽高
measurable.measure(constraints)
// 测量每个 child 尺寸,重新约束宽度,
val placeable = measurable.measure(constraints.copy(maxWidth = 100))
// 这句别忘了,返回每个 child 的placeable
placeable
}
layout(constraints.maxWidth, constraints.maxHeight) {
// 摆放 children
placeable.placeRelative( x,y )
}
}
}
常规操作:通过遍历measurables
,使用measurable.measure(constraints)
得到placeable,获取每个控件的宽高
,然后使用 在layout
使用placeable.placeRelative(使用该方法每个组件都是相对于viewgroup的边界)
进行定位,基于这些知识我们可以愉快的开发了
四、使用Layout自定义瀑布流布局
根据上面的思路写出代码如下,详细流程看代码注释 下面只是让其摆放好位置了,要让其可以滑动该咋办,这个好说为CustomVerticalStaggeredGrid
的父布局Column项
添加Modifier
滑动能力verticalScroll(rememberScrollState()
即可
kotlin
class ColumnLayoutBean{
var columnArray:ArrayList<ItemHeight> = ArrayList<ItemHeight>()
var allHeight:Int = 0 //每一列的总高度
}
class ItemHeight{
//原始下标
var originIndex:Int = 0
//child的高度
var height:Int = 0
//当前控件需要放置的Y轴位置
var lastAllHeight:Int = 0
//位于哪一列
var column:Int = 0
}
@Composable
fun CustomVerticalStaggeredGrid(
modifier: Modifier = Modifier,
column: Int = 3, // 自定义的参数,控制展示的行数,默认为 2列
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
Log.d("tgw", "CustomVerticalStaggeredGridmaxWidth:${constraints.maxWidth} ")
Log.d("tgw", "CustomVerticalStaggeredGridminWidth:${constraints.minWidth} ")
Log.d("tgw", "CustomVerticalStaggeredGrid maxHeight:${constraints.maxHeight} ")
Log.d("tgw", "CustomVerticalStaggeredGrid minHeight:${constraints.minHeight} ")
val columnHeights = Array<ColumnLayoutBean>(column) { ColumnLayoutBean() }
//存储所有的item项
var columnArray: ArrayList<ItemHeight> = ArrayList<ItemHeight>()
//存储每一列的高度总和
val columnHeightArray = IntArray(column) { 0 }
// 测量每个 child 尺寸,获得每个 child 的 Placeable 对象
val placeables = measurables.mapIndexed { index, measurable ->
// 标准流程:测量每个 child 尺寸,获得 placeable,约束宽度,最大为控件的宽度除以列
val placeable = measurable.measure(constraints.copy(maxWidth = constraints.maxWidth / column))
if (index < column) {//初始化列,先占满坑位
columnHeights[index % column].allHeight = placeable.height
val item = ItemHeight().apply {
this.originIndex = index
this.height = placeable.height
this.column = index % column
this.lastAllHeight = 0
}
columnHeights[index % column].columnArray.add(item)
columnArray.add(item)
columnHeightArray[index % column] = placeable.height
}else {
// 取出高度最小的列的下标,添加
val minIndex = columnHeightArray.indices.minBy { columnHeightArray[it] }
columnHeights[minIndex].allHeight += placeable.height
columnHeightArray[minIndex] = columnHeights[minIndex].allHeight
//获取当前item的开始时Y轴的放置位置
var lastHeight = 0
columnArray.filter { it.column == minIndex }.forEach {
lastHeight += it.height
}
val item = ItemHeight().apply {
this.originIndex = index
this.height = placeable.height
this.column = minIndex
this.lastAllHeight = lastHeight
}
columnHeights[minIndex].columnArray.add(item)
columnArray.add(item)
}
placeable // 这句别忘了,返回每个 child 的placeable
}
// 取出最大列的高度
val height = columnHeightArray.max()
// 设置 自定义 Layout 的宽高
layout(constraints.maxWidth, height) {
// 摆放每个 child
placeables.forEachIndexed { index, placeable ->
val currentColumn = columnArray[index].column
val x = (constraints.maxWidth / column * currentColumn).toInt()
placeable.placeRelative(
x,
columnArray[index].lastAllHeight
)
}
}
}
}
使用流程如下:
less
@Composable
fun Greeting() {
Column(
modifier = Modifier
.fillMaxSize()
horizontalAlignment = Alignment.CenterHorizontally
) {
// 自定义viewgroup,数据
val topics = listOf(
"position1 谁发发发双法防谁发发发双法防谁发发发双法防",
"position2 ",
"position3 通天塔",
"position4 哈哈哈哈哈哈",
"position5 火花",
"position6 ",
"position7 灌灌灌灌",
"position8 000",
"position9 天啊啊啊",
"position10 来来来",
"position11 哦哦哦哦哦哦哦哦",
"position12 前期去去去去去去去去去",
"position13 与i一有意义有意义",
"position14 iiiiiiiii",
"position15 谁发发发双法防谁发发发双法防谁发发发双法防",
"position16 呜呜呜呜呜呜",
"position17 来了来了",
"position18 啊啊啊",
"position19 ",
)
Text(text = "自定义的瀑布流布局", modifier = Modifier.background(Color(0xFFC56565)))
Column(modifier = Modifier
.verticalScroll(rememberScrollState())
.weight(1f)
.background(Color(0xFF00FF00))) {
CustomVerticalStaggeredGrid() {
for (topic in topics) {
Column {
Text(text = topic,modifier = Modifier
.clickable {
Toast
.makeText(
this@CustomMainActivity,
"显示:$topic",
Toast.LENGTH_SHORT
)
.show()
}
.background(Color(0xFFB88076))
.padding(8.dp))
Spacer(modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(Color(0xFFFFFEFE)))
}
}
}
}
}
}
综上自定义了一个简单的瀑布流的viewgroup布局,借这个例子主要是学习一下自定义viewgroup的流程
参考: