一、前言
在实际开发中会出于各种原因进行自定义布局,所以这里简单记录下使用Compose
进行自定义布局的方式
二、使用布局修饰符
本段参考代码是google的代码
我们可以使用layout修饰符来元素的测量和布局方式,大概方式如下:
fun Modifier.customLayoutModifier(...) = Modifier.layout { measurable, constraints ->
...
})
不过实际应用中通常使用以下写法:
fun Modifier.customLayoutModifier(...) =
this.layout { measurable, constraints ->
...
})
比如想控制显示的Text顶部到第一行基线的位置,示例如下:
fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = layout { measurable, constraints ->
// Measure the composable
val placeable = measurable.measure(constraints)
// 检查是否包含基线,如果不包含则会引发异常
check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
val firstBaseline = placeable[FirstBaseline]
// Height of the composable with padding - first baseline
val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
val height = placeable.height + placeableY
layout(placeable.width, height) {
// Where the composable gets placed
placeable.placeRelative(0, placeableY)
}
}
三、创建自定义布局
layout
修饰符仅更改调用可组合项。如需测量和布置多个可组合项,请改用 Layout
可组合项。此可组合项允许您手动测量和布置子项。Column
和 Row
等所有较高级别的布局都使用 Layout
可组合项构建而成。大都数自定义布局遵循以下方式:
@Composable
fun MyBasicColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
children = content
) { measurables, constraints ->
// measure and position children given constraints logic here
}
}
比如我们自定义一个Column布局,示例如下:
@Composable
fun MyBasicColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// Don't constrain child views further, measure them with given constraints
// List of measured children
val placeables = measurables.map { measurable ->
// Measure each children
measurable.measure(constraints)
}
// Set the size of the layout as big as it can
layout(constraints.maxWidth, constraints.maxHeight) {
// Track the y co-ord we have placed children up to
var yPosition = 0
// Place children in the parent layout
placeables.forEach { placeable ->
// Position item on the screen
placeable.placeRelative(x = 0, y = yPosition)
// Record the y co-ord placed up to
yPosition += placeable.height
}
}
}
}
四、固有特性测量
一般来说,在自定义布局中使用默认测量方式就可以了,但是有时候可能并不能满足需求。因此要指定自定义 Layout
的固有特性测量,则在创建该布局时替换 MeasurePolicy的 minIntrinsicWidth
、minIntrinsicHeight
、maxIntrinsicWidth
和 maxIntrinsicHeight
。
代码结构如下:
@Composable
fun MyCustomComposable(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
return object : MeasurePolicy {
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult {
// Measure and layout here
}
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
) = {
// Logic here
}
// Other intrinsics related methods have a default value,
// you can override only the methods that you need.
}
}
而在创建自定义 layout
修饰符时,替换 LayoutModifier
界面中的相关方法。
fun Modifier.myCustomModifier(/* ... */) = this.then(object : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
// Measure and layout here
}
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurable: IntrinsicMeasurable,
height: Int
): Int = {
// Logic here
}
// Other intrinsics related methods have a default value,
// you can override only the methods that you need.
})
五、分析修饰符
这里分析下Modifier.padding
的原理,代码如下(以下代码源自Google,不过已经修改为更易懂的方式):
// How to create a modifier
@Stable
fun Modifier.padding(all: Dp) =
this.then(
PaddingModifier(start = all, top = all, end = all, bottom = all, rtlAware = true)
)
// Implementation detail
private class PaddingModifier(
val start: Dp = 0.dp,
val top: Dp = 0.dp,
val end: Dp = 0.dp,
val bottom: Dp = 0.dp,
val rtlAware: Boolean,
) : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val horizontal = start.roundToPx() + end.roundToPx() //获取padding的横向长度
val vertical = top.roundToPx() + bottom.roundToPx() //获取padding的垂直长度
// val placeable = measurable.measure(constraints.offset(horizontal, vertical)) //偏移horizontal、vertical距离后进行测量,偏移只会更改内容位置,不会影响测量大小,因为下面已经进行偏移了,所以可以不用这么麻烦
val placeable = measurable.measure(constraints)
val width = constraints.constrainWidth(placeable.width + horizontal)
val height = constraints.constrainHeight(placeable.height + vertical)
return layout(width, height) {//定义父布局宽高
if (rtlAware) {
placeable.placeRelative(start.roundToPx(), top.roundToPx()) //将组件在现有位置上进行移动,该移动是在布局里面,所以并不会超出布局宽高
} else {
placeable.place(start.roundToPx(), top.roundToPx())
}
}
}
}
这里面有一个有意思的问题,就是调用placeable.placeRelative
偏移后为什么不会超出设置的宽高?
这里解释下Placeable
,文档上解释的意思是:Placeable
对应于可以由其父布局定位的子布局。大多数Placeable
是Measurable.measure
调用的结果。Placeable
不应该在测量调用之间存储。其中placeable.width
是父布局所需要留出的宽度,placeable.height
是父布局所需要留出的高度。而placeable.measuredWidth
才是控件真正的测量宽度,placeable.measuredHeight
是控件真正的测量高度。因此调用placeable.placeRelative
函数并不会导致组件超出布局。
六、参考链接
-
Compose中的自定义布局:
https://developer.android.google.cn/jetpack/compose/layout#decoupled
-
compose的codelabs
https://developer.android.google.cn/codelabs/jetpack-compose-layouts#6
-
Placeable的类说明
https://developer.android.google.cn/reference/kotlin/androidx/compose/ui/layout/Placeable?hl=en