Compose 自定义 - 布局(测量+摆放) Layout
一、概念
1.1 过程
测量和摆放是一个连续的过程,在 View 体系中分开是因为可能会对子元素进行多次测量:例如父容器宽度是 wrap_content 会依次测量所有子元素选取最宽的作为自己宽度,如果有子元素的宽度是 match_parent 会先将该子元素保存起来(先以0为强制宽度测量)并正常测量其它子元素,然后构建新的 MeasuredSpec(size为测量其它子元素得到的最宽值,mode为EXACTLY) 来二次测量之前保存的子元素,再将最终计算的结果设置为自身尺寸。随着嵌套层级的加深会造成测量次数呈指数级增长。
不允许多次测量 | 为了解决多次测量的性能问题,Compose 禁止了多次测量子元素,否则抛出异常 IllegalStateException,使得我们可以进行深层次嵌套而不用担心影响性能。 |
对应 View 体系中父容器是 wrap_content 需要累加子元素确定自身的情况:首先会要求每个节点对自身进行测量,然后一样是通过递归方式将约束条件 constraints 沿着树向下传递完成所有子元素的测量,根据叶子节点的尺寸和摆放位置向上回溯,进而调整父节点最终确定根节点。简而言之,节点元素会先粗略测量自身,后面再根据叶子元素的位置大小调整自己。 | |
提供固有特性测量 | 为了解决需要父容器和子元素共同决定尺寸问题并避免多次测量,Compose 加入了固有特性测量(为官方翻译,英文字面有“自身尺寸”的意思),指的是允许父容器在对子元素进行正式测量前,先获得子元素的最大或最小尺寸。 |
对应 View 体系中父容器是 wrap_content 子元素是 match_parent 的情况:不进行适配的话子元素的大小将直接撑满父容器能获得的最大尺寸。因此 Compose 提供固有特性测量机制(Intrinsic Measurement),用于在正式测量前父容器获取子元素能正常显示的尺寸范围(宽高最大最小值),这样设置了 Modifier.height(IntrinsicSize.Min) 的父容器就能从所有子元素的 minIntrinsicHeight(能正常显示的最小高度)中选出最大的那个,就能得到自身高度并设置给 Modifier.fillMaxHight() 的子元素。 |
通过从上往下测量(如果存在子节点则测量子节点,测量完子节点后决定自身的尺寸)、从下往上摆放(根据子节点的尺寸摆放子节点)来决定该节点的宽高和坐标。:
- 系统要求根节点 Row 测量自身。
- 根节点 Row 要求第一个子元素 Image 测量自身。
- 由于 Image 是叶子节点能确定自身的尺寸和摆放并上报。
- 根节点 Row 要求第二个子元素 Column 测量自身。
- 父容器 Column 要求第一个子元素 Text 测量自身。
- 由于 Text 是叶子节点能确定自身的尺寸和摆放并上报。
- 父容器 Column 要求第二个子元素 Text 测量自身。
- 由于 Text 是叶子节点能确定自身的尺寸和摆放并上报。
- 父容器 Column 所有子元素都测量摆放完毕,可以确定自身的尺寸和摆放并上报。
- 根节点 Row 所有子元素都测量摆放完毕,可以确定自身的尺寸和摆放。
1.2 自定义用到的核心方法
Modifier.layout() 修饰符 | 用于修改单个可组合项的测量和摆放(会取代父容器原本对它的测量和摆放)。 |
Layout() 可组合项 | 外层用一个自定义的组合函数包裹使用,界面首次渲染时会将可组合项转化为一个个布局节点 Layout Node。 |
layout() 方法 | 布局阶段摆放子元素的入口,在测量完子 Composable 后进行。 |
二、Modifier.layout() 修饰符
就是自定义一个 Modifier 的扩展函数并返回 Modifier 对象,实现它的 layout( ) 方法如何测量及摆放自身。
fun Modifier.XXX(): Modifier = then(
layout { measurable, constraints ->
//测量自身并返回结果
//通过contraints可以拿到自身宽或高可以设置的最大和最小值
val placeable = measurable.measure(constraints)
//获取测量后的控件宽高
val measuredWidth = placeable.width
val measuredHeight = placeable.height
//计算实际宽高
val needWidth = ...
val needHeight = ...
//指定控件宽高
layout(needWidth, needHeight) {
//指定摆放的左上角偏移(在xy上移动的距离)
placeable.placeRelative(0, 0)
}
}
)
2.1 例子一
需求:手写一个设置基准线到屏幕的距离,即图中的a。(已有系统API:Modifier.paddingFromBaseline() )
fun Modifier.firstBaseLine2Top(a: Dp): Modifier = then(
layout { measurable, constraints ->
//对自身进行测量
val placeable = measurable.measure(constraints)
//计算实际宽高
check(placeable[FirstBaseline] != AlignmentLine.Unspecified) //检查该组件是否支持FirstBaseline(false抛出IllegalStateException )
val c = placeable[FirstBaseline] //存在的情况下,获取FirstBaseline离组件顶部的距离
val b = a.roundToPx() - c //计算y轴上组件放置位置(x轴直接是0)
val needHeight = placeable.height + b //测量的控件高度 + 控件距离顶部的距离
val needWidth = placeable.width //宽度直接就是测量出来的
//设置控件宽高
layout(needWidth, needHeight) {
//设置偏移
placeable.placeRelative(0, b)
}
}
)
//使用
@Composable
fun Show() {
Text(
text = "Hello Word!",
modifier = Modifier.firstBaseLine2Top(24.dp)
)
}
三、Layout() 可组合项
@Composable 外层用一个自定义的组合函数包裹后使用。 | |
MeasurePolicy | fun MeasureScope.measure( 重写以实现测量和摆放。 |
fun IntrinsicMeasureScope.minIntrinsicWidth(measurables: List<IntrinsicMeasurable>, height: Int): Int fun IntrinsicMeasureScope.minIntrinsicHeight(measurables: List<IntrinsicMeasurable>, width: Int): Int fun IntrinsicMeasureScope.maxIntrinsicWidth(measurables: List<IntrinsicMeasurable>, height: Int): Int fun IntrinsicMeasureScope.maxIntrinsicHeight(measurables: List<IntrinsicMeasurable>, width: Int): Int 重写以实现固有特性测量,例如宽度固定时自身正常显示的高度最大最小值(例如当 Text 宽度固定时,minIntrinsicHeight 为它的高度)。 |
Measurable | fun measure(constraints: Constraints): Placeable 对子元素进行测量 |
Constraints | val minWidth: Int 可以拿到宽高的最大最小值 |
MeasureScope | fun layout( 摆放子元素的入口。 |
PlacementScope | fun Placeable.placeRelative(x: Int, y: Int, zIndex: Float = 0f) 摆放元素 |
3.1 测量和摆放
由测量和摆放两个步骤组成,该顺序使用 DSL 实现,嵌套的方式使得无法摆放未经测量的子元素(MeasurePolicy.measure() 提供测量作用域,layout() 提供摆放作用域) 。
当一个节点测量子节点的时候会提供 contraints 让子元素了解自己能设置的最大值和最小值,需要修改对子元素的约束可以通过 contraints.copy(minWidth = 20dp) 修改后在子元素测量时传入。
测量方法执行后会返回一个 placeables 列表,表示子元素已经准备好被摆放了。调用 layout() 在摆放作用域中摆放它们,决定好坐标后调用 place() 摆放。
- 重写 MeasurePolicy 的 measure() 方法来自定义布局(测量+摆放)。
- 遍历 measurables 拿到每个子元素的测量句柄 measurable,调用 measure() 并传入约束条件 constraints 对子元素进行测量,返回的 placable 对象可以拿到子元素测量后的宽高,就可以累积计算出自身的尺寸。
- 调用 layout() 并传入计算出的尺寸摆放自己,会返回一个 MeasureResult 对象用来上报。
- 遍历 placables 拿到每个子元素测量后的尺寸,调用 placeRelative() 摆放子元素。
@Composable
fun Demo(
content: @Composable () -> Unit, //被包裹的内容
modifier: Modifier = Modifier //用于外部修饰自己
) {
Layout(
modifier = modifier,
content = content
) {measurables, constraints ->
//【测量阶段】
val needHeight = 0
val needWidth = 0
//遍历并测量子元素,返回结果集合
val placeables = measurables.map { measurable ->
//返回子元素测量结果
//不需要修改约束就直接将constraints传入
//需要修改约束就使用 constraints.copy(minWidth = 20dp)
measurable.measure(constraints).also { placeable ->
//获取测量后的子元素宽高
val childMeasuredWidth = placeable.width
val childMeasuredHeight = placeable.height
//通过子元素计算自身宽高
needHeight = ...
needWidth = ...
//用集合保存每一行或每一列的宽高信息
}
}
//【摆放阶段】
layout(needWidth, needHeight) {
//遍历子元素,设置它们在xy上的偏移
placeables.forEachIndexed { index, placeable ->
//通过每一行或每一列的宽高信息,摆放子元素
val x = ...
val y = ...
placeable.placeRelative(x, y)
}
}
}
}
3.2 提供固有特性测量
@Composable
fun Demo(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
){
Layout(
modifier = Modifier,
content = content,
measurePolicy = object : MeasurePolicy {
override fun MeasureScope.measure(measurables: List<Measurable>, constraints: Constraints): MeasureResult {
//省略...同上方3.1中的代码
}
//自身能正常显示的minIntrinsicWidth为所有子元素中最大的那个minIntrinsicWidth
override fun IntrinsicMeasureScope.minIntrinsicWidth(measurables: List<IntrinsicMeasurable>, height: Int): Int {
var maxWidth = 0
measurables.forEach { intrinsicMeasurable->
maxWidth = max(maxWidth, intrinsicMeasurable.minIntrinsicWidth(height))
}
return maxWidth
}
}
)
}
四、一些例子
4.1 测量和摆放
4.1.1 例子一(简单)
需求:自定义一个纵向摆放子元素的布局,且尽可能大的占用父容器。
//自定义一个纵向摆放子元素
@Composable
fun MyColumn(
modifier: Modifier = Modifier, //用于外部修饰自己
content: @Composable () -> Unit //接收子元素
){
//对子元素进行测量和摆放
Layout(
modifier = modifier,
content = content
) {measurables, constraints ->
// 测量每个子元素的尺寸
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
var yPosition = 0 //下一个子元素在y轴上摆放的y坐标
//摆放子元素
//尽可能大的占用父布局类似于match_parent(官方Column是尽可能小的占用类似于wrap_content)
layout(constraints.maxWidth, constraints.maxHeight) {
placeables.forEach { placeable ->
placeable.placeRelative(0, yPosition)
yPosition += placeable.height
}
}
}
}
//使用
@Composable
fun Show() {
MyColumn(modifier = Modifier.padding(10.dp)) {
Text(text = "条目1")
Text(text = "条目2")
Text(text = "条目3")
}
}
3.1.2 例子二(复杂)
需求:横向滑动的瀑布流,可以设置行数。
@Composable
fun StaggeredGrid(
modifier: Modifier = Modifier, //用于外部修饰自己
rows: Int = 3, //默认显示三行
content: @Composable () -> Unit //接收子元素
){
//【第一步:对所有子元素尺寸测量】
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val rowWidths = IntArray(rows){0} //记录每一行的宽度
val rowHeights = IntArray(rows){0} //记录每一行的高度
val placeables = measurables.mapIndexed { index, measurable ->
val placeable = measurable.measure(constraints)
//根据索引对子元素分组,记录每一行的宽高
val row = index % rows //确保只有3行,该值只会得到 0,1,2
rowWidths[row] += placeable.width //一行的宽度=这行所有元素宽度之和
rowHeights[row] = max(rowHeights[row], placeable.height) //一行的高度=这行最高的元素
placeable //测量完要返回placeable对象
}
//【第二步:计算自身的尺寸】
val width = rowWidths.maxOrNull() //宽度取所有行中宽度最大值
?.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth)) //宽度限制在最大值和最小值之间
?: constraints.minWidth //为null就设为最小值
val height = rowHeights.sumOf { it } //高度为所有行高之和
.coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))
val rowY = IntArray(rows){0} //每行子元素在y轴上摆放的坐标
for (i in 1 until rows) { //第一行肯定是0,从第二行开始赋值
rowY[i] = rowY[i - 1] + rowHeights[i - 1] //当前行y坐标=前一行y坐标+前一行高度
}
//【第三步:摆放子元素】
layout(width, height) { //在自身的尺寸里摆放
val rowX = IntArray(rows){0} //每行子元素在x轴上的坐标
placeables.forEachIndexed { index, placeable ->
val row = index % rows
placeable.placeRelative(rowX[row], rowY[row])
rowX[row] += placeable.width
}
}
}
}
//使用
val topics = listOf(
"Arts & Crafts", "Beauty", "Books", "Business", "Comics", "Culinary",
"Design", "Fashion", "Film", "History", "Maths", "Music", "People", "Philosophy",
"Religion", "Social sciences", "Technology", "TV", "Writing"
)
@Composable
fun Show() {
StaggeredGrid {
for (topic in topics) {
Text(
text = topic,
modifier = Modifier.padding(8.dp).background(MaterialTheme.colors.error)
)
}
}
}
4.2 提供固有特性测量
Compose为提高效率子元素只能测量一次(再次会抛异常)。为了实现预先获得子元素宽高信息再确定自身宽高信息,Compose提供了固有特性测量机制,让我们手动重写方法提供值,使得在子元素正式测量前能获宽高的最大最小值。
4.1 举例一
有一个包含五个 Item 的 Column,需要每个 Item 宽度一致。实际效果是 Item 大小各不相同。
很容易想到让每个 Item 都占用允许的最大尺寸,但最终导致 Column 扩充为最大尺寸。
对 Column 宽度使用 IntrinsicSize.Max 达到目标效果。正确显示内容所需要的最大宽度的意思。
而如果使用 IntrinsicSize.Min 的效果是这样,Text 的最小固有宽度是每行一个词时的宽度,因此是一个按单词换行的效果。
4.2 举例二
希望分割线 Divider 与最高的 Text 长度相等,发现 Divider 扩展到整个屏幕。Row 会逐个测量子元素,而测得的 Text 高度无法用在限制 Divider。
@Composable
fun WithoutIntrinsics() {
Row {
Text(
modifier = Modifier
.weight(1f)
.wrapContentWidth(Alignment.CenterHorizontally), text = "Text 1"
)
//分割线本意是和左右两个文本一样高
//Row没有对它的子元素测量做任何限制,填充父容器会尽可能的撑大父容器
Divider(
color = Color.Black, modifier = Modifier
.fillMaxHeight()
.width(1.dp)
)
Text(
modifier = Modifier
.weight(1f)
.wrapContentWidth(Alignment.CenterHorizontally), text = "Text 2"
)
}
}
使用修饰符,可将其子项的高度强行调整为最小固有高度,会递归查询 Row 和子元素的 minIntrinsicHeight。Divider 的 minIntrinsicHeight 为0(即没有给出约束条件它不会占用任何空间),而 Text 在设置了固定宽度的情况下它的 minIntrinsicHeight 即为文本的高度,因此 Row 高度的约束条件将为 Text 的最大高度
//方式一:官方API
@Composable
fun WithIntrinsics(){
//高度设为最小(刚好包裹子元素)
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
//其他代码未变
}
}
更多推荐
所有评论(0)