Compose 自定义 - 布局 SubcomposeLayout
一、概念
当需要根据可用空间(约束)来动态加载子元素或布局,约束在 Layout() 中获取,但是所有子元素在布局阶段之前(也就是组合阶段)就完成了生成。这时就需要使用 SubcomposeLayout(),会将子元素的生成推迟到组合阶段再执行,让你在 measurePolicy() 中手动调用 subcompose() 来生成内容并返回 Measurables,更简单的方式是使用 BoxWithConstraints() 组件。
| SubcomposeLayout() | @Composable fun SubcomposeLayout( modifier: Modifier = Modifier, measurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult, ) |
| subcompose() | fun subcompose(slotId: Any?, content: @Composable () -> Unit): List<Measurable> 参数 slotId 唯一标识避免重复生成子元素。确保 slotId 的稳定性,否则每次都会重组影响性能。 参数 content 需要生成的子元素。 |
1.1 使用场景
| 独立上下文 | 每一次 subcompose调用(针对不同的 slotId)都会创建一个独立的 CompositionContext。这比主 Composition 树稍重。 |
| 布局阶段的阻塞 | 布局阶段通常是同步运行的,如果在 measure 块里做了极其繁重的组合逻辑,可能会导致掉帧(Jank)。 |
动态加载子元素或布局:只有当你必须知道父容器尺寸才能决定生成什么子元素。
跨子项依赖测量结果:先测量一个子元素才能决定下一个子元素是否生成,或需要在生成第二个子元素时使用第一个子元素的尺寸。
基于可用尺寸的懒加载:包含 100 个条目的列表,仅加载可见的5个条目,并在滚动时再加载后续条目。
1.2 BoxWithContraints() 组件
源码底层就是通过 SubcomposeLayout 实现的,避免在 LazyList 的条目中使用,每个条目都会创建一个独立的 Subcomposition 上下文,它比普通的 Layout() 成本高得多,列表滚动时会产生大量的对象分配和上下文切换。
| BoxWithConstraints() | @Composable 作用域中共5个属性可以使用:constraints、maxHeight、maxWidth、minHeight、minWidth。 |
BoxWithConstraints { val maxHeight = constraints.maxHeight if (maxWidth > 600.dp) {...} }二、使用举例
2.1 简易版 LazyRow
一个包含 1000 个数字的列表,横向排列,但只渲染屏幕内可见的数字。真实的 LazyColumn 逻辑要复杂得多(处理复用、回收、预加载、双向滚动等),但核心就是这个 while() 配合 subcompose()。
@Composable fun SimpleLazyRow( modifier: Modifier = Modifier, itemCount: Int, itemContent: @Composable (Int) -> Unit ) { SubcomposeLayout(modifier) { constraints -> var currentX = 0 //当前x坐标 var index = 0 val children = mutableListOf<Pair<Int, Placeable>>() //存储测量好的子元素 //循环:只要还没占满可用宽度就继续生成子元素,满了剩下的(屏幕外不可见的)条目不会被生成 while (currentX < constraints.maxWidth && index < itemCount) { //生成子元素并测量 //slotId 唯一标识确保重组时会复用之前的Composition上下文,只update而不是create val placeable = subcompose(index) { itemContent(index) }.map { measurable -> measurable.measure(constraints) }.first() //这里只有一个子元素 //存储子元素 children.add(currentX to placeable) //更新游标坐标 currentX += placeable.width index ++ } //只放置可见的条目 layout(currentX, constraints.maxHeight) { children.forEach { (x, placeable) -> placeable.placeRelative(x, 0) } } } }2.2 展开折叠的 Text
文字默认折叠,点击后展开。 如果用普通的maxLines属性,可能很难拿到“展开按钮”应该放在哪里的精确坐标(尤其是文字最后一行没有填满时)。SubcomposeLayout 能先测一遍,效果不行就换另一种策略。
@Composable fun ExpandableText( modifier: Modifier = Modifier, text: String, isExpanded: Boolean ) { SubcomposeLayout(modifier) { constraints -> //先测量全部显示的子元素看看有多大 val placeable = subcompose(Unit) { Text(text) }.map { it.measure(constraints) }.first() //如果开启这贴并且子元素高度超过50.dp就折叠 if (!isExpanded && placeable.height > 50.dp) { //需要折叠逻辑: //1.限制文本重新生成子元素(通过约束或字符串) //2.生成按钮,subcompose(Unit) { Button(...) } //3.将按钮放在文本右下角 layout(...) {...} } else { //不需要折叠,直接摆放 layout(placeable.width, placeable.height) { placeable.place(0, 0) } } } }