前言
SubcomposeLayout
是 Compose 提供的一个高级的布局函数,它有着更为强大的功能,但这也是要付出一定代价的。
SubcomposeLayout
的应用场景有很多,比如我们常见的 LazyColumn
和 LazyRow
滑动列表组件,其内部就使用了 SubcomposeLayout
。
核心概念
什么是 SubcomposeLayout?
那它是用来干嘛的?
首先在程序中,Subcompose
的意思是次级组合、子组合,就是这部分的组合不遵循主组合流程,是相对独立的部分,但还是属于整体组合的一部分。
具体到 SubcomposeLayout
函数的作用就是让我们可以将一部分 Composable 函数的组合(Composition)过程延后执行,推迟到测量(Measure)阶段甚至是布局(Layout)阶段再进行。
有了这种“延迟组合”的能力,我们就能拿到组合过程中拿不到的数据,比如说测量结果。
为何需要“延迟组合”
为什么呢?我们先来看看界面的渲染流程。
在 Jetpack Compose 中,UI 的渲染流程中有四个阶段,分别是:组合(Composition)、测量(Measure)、布局(Layout)、绘制(Draw)。
通常情况下,这四个阶段是顺序执行的,它们之间是不进行穿插的。但有了 SubcomposeLayout
,就可以将局部的组合行为推迟到测量或布局阶段。
实践应用
典型应用:BoxWithConstraints
具体是怎么推迟的,我们先来看看 BoxWithConstraints
函数,它内部就使用了 SubcomposeLayout
,使得其内部组件的组合过程可以发生在测量阶段。
使用它的话,就和使用 Box
函数一样,因为它提供的 BoxWithConstraintsScope
上下文是继承自 BoxScope
上下文,只是额外提供了一些尺寸限制信息。
@Stable
interface BoxWithConstraintsScope : BoxScope
val constraints: Constraints // 父组件对当前组件的尺寸限制信息(像素)
val minWidth: Dp // 最小宽度(Dp)
val maxWidth: Dp // 最大宽度(Dp)
val minHeight: Dp // 最小高度(Dp)
val maxHeight: Dp // 最大高度(Dp)
}
注意:
minWidth
、maxWidth
、minHeight
和maxHeight
这几个属性,它们只是constraints
中对应限制的Dp
版本,以minWidth
为例,它的定义是:
override val minWidth: Dp get() = with(density) { constraints.minWidth.toDp() }
使用的还是
constraints
中的minWidth
。
这些尺寸限制信息你在别的组件中是拿不到的,就比如在 Box
中是拿不到 constraints
的,因为你在 Box
中编写的代码,在组合阶段就被调用了,是无法拿到组合阶段后的测量阶段父组件才传递的尺寸限制的。
BoxWithConstraints
函数为什么能拿到这些信息?正是因为它使用了 SubcomposeLayout
,使得其内容的组合过程被推迟到了测量阶段,此时就可以把在测量阶段获取的尺寸限制 constraints
传入到子组件的组合作用域当中。
这样做的目的,可以让我们实现与测量结果相关的动态布局。比如,根据设备的屏幕宽度,显示不同的布局,专为平板或者手机优化的布局。
SubcomposeLayout 的用法
那 SubcomposeLayout
具体怎么使用?
请看下面代码:
SubcomposeLayout { constraints: Constraints ->
// 当前是在测量阶段
// 1. 组合子 Composable 函数
// slotId 是做性能优化的,只要它不变,就不会重组,会复用之前的组合效果和状态
// 改变的话,之前的状态就会丢失,内部会完全重组
val measurables: List<Measurable> =
subcompose(slotId = Unit) {
// subcompose 提供了 Composable 的上下文
Text("我是文本")
Button(onClick = {}) {
Text("我是按钮")
}
}
// 2. 测量所有子组件
val placeables: List<Placeable> = measurables.map { measurable ->
measurable.measure(constraints) // 可以使用父级传递的 constraints,或根据需要创建新的
}
// 3. 摆放子组件,并确定自身尺寸
layout(width = constraints.maxWidth, height = constraints.maxHeight) { // 尺寸可以基于子项的测量结果或 constraints
placeables.forEach { placeable: Placeable ->
placeable.placeRelative(0, 0) // 根据测量结果定位子项
}
}
}
以上只是最基本的写法。
重要的是,SubcomposeLayout
可以让我们就能在测量阶段,来自由地控制每个子组件的组合、测量、布局过程。
例如:
我们可以给子组件传递 constraints
尺寸限制信息。或者先测量简介组件,根据简介组件的实际高度,来决定简介的文本是应该完整地显示,还是需要截断并添加一个“显示更多”的按钮。
在本质上就是让一部分的组合工作去依赖其他组件的测量结果。
常见的内置组件案例
另外呢,Compose 给我们提供的很多组件的内部都使用了 SubcomposeLayout
,最典型的就是Scaffold
和 LazyColumn
/ LazyRow
。
Scaffold
是脚手架的意思,可以让我们快速搭建一个具有标准结构的页面(如顶部栏、底部栏、浮动操作按钮等)。它的内部就使用了 SubcomposeLayout
来动态测量各个部分,然后根据测量结果来摆放子组件。比如测量 TopAppBar
的高度,以便能够正确地放置内容区域。
LazyColumn
/ LazyRow
滑动列表可能有成百上千的列表项,但是同一时间,出现在界面中的,可能就只有寥寥的几项。使用 SubcomposeLayout
可以让我们只去组合、测量当前需要显示或即将显示的列表项,而不用组合、测量所有的列表项,避免了不必要的性能消耗。
性能代价与使用原则
不可忽视的性能代价
我们开头说过,使用 SubcomposeLayout
是有代价的。
反过来想,如果没有任何代价的话,Box
函数就可以被抛弃了,因为功能上 BoxWithConstraints
比 Box
更强嘛。
而它的代价主要是性能不高。
-
因为
SubcomposeLayout
在内部维护了一个独立的“节点树”,这样导致它的重组,无法参与到整体的重组优化。 -
并且原本的测量、布局是不会导致重组的,但由于
SubcomposeLayout
将组合过程穿插到了测量、布局阶段,所以当SubcomposeLayout
因为外部因素需要重新测量或重新布局时,其内部的subcompose
函数也会被重新执行,导致这种额外的、局部的重组。就比如
LazyColumn
进行尺寸变化的动画时,频繁的尺寸改变会导致频繁的重新测量,进而导致内部列表项频繁的重组,所以你可能会观察到卡顿现象。
使用原则
难道因为有性能消耗就不用了吗?当然不是。
我们只需遵循非必要不使用的原则就行了。
能使用常规的布局组件完成的,就优先使用它们。只需要获取到父组件的尺寸限制时,使用 BoxWithConstraints
组件。迫不得已,需要更底层、更精细的控制时,就使用 SubcomposeLayout
。
在很多时候,使用 SubcomposeLayout
带来的性能提升远大于其自身开销,例如:LazyColumn
,你不使用 SubcomposeLayout
,性能只会急剧下降。