ParentDataModifier
什么是 ParentDataModifier?
在 Compose 中,有一类 Modifier 叫做 ParentDataModifier,它们的作用是为父节点提供数据,这些数据可以在父节点的测量和布局过程中被读取,通常用于告诉父节点子节点应该如何被测量和布局。
kotlin
/**
* A [Modifier.Node] that provides data to the parent [Layout]. This can be read from within the
* the [Layout] during measurement and positioning, via [IntrinsicMeasurable.parentData].
* The parent data is commonly used to inform the parent how the child [Layout] should be measured
* and positioned.
*
* This is the [androidx.compose.ui.Modifier.Node] equivalent of
* [androidx.compose.ui.layout.ParentDataModifier]
*/
interface ParentDataModifierNode : DelegatableNode {
/**
* Provides a parentData, given the [parentData] already provided through the modifier's chain.
*/
fun Density.modifyParentData(parentData: Any?): Any?
}
最常见的 ParentDataModifier 是 weight()
修饰符:
kotlin
Row(Modifier.border(1.dp, Black).fillMaxWidth()) {
Box(Modifier.background(Red).height(100.dp).weight(1f))
Box(Modifier.background(Orange).height(100.dp).weight(2f))
Box(Modifier.background(Blue).height(100.dp).width(30.dp))
}
在布局测量过程中,Row 会读取子节点设置的 ParentData 也就是 weight 权重值,并根据 weight 值来计算子节点的宽度。
"ParentDataModifier" 的命名还是很准确的,它的作用就是为父节点(Parent)提供数据(Data),也就是说,虽然 weight()
修饰符写在子节点上,但是填入的数据真正被使用的地方是在父节点。
如何自定义 ParentDataModifier?
读取子组件的 parentData
要自定义 ParentDataModifier,首先我们要搞明白:对于父组件来说,怎么获取子组件设置的 ParentData?
在 Compose 中,像 Row、Box 等等 Layout 组件,背后都使用了 Layout 函数:
kotlin
@UiComposable
@Composable
inline fun Layout(
content: @Composable @UiComposable () -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
) { ... }
如果要手写一个 Layout 组件,并获取子组件设置的 ParentData,我们可以这样写:
kotlin
@Composable
fun MyRow(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier,
measurePolicy = { measurables, constraints ->
// 遍历获取所有子组件的 ParentData
measurables.forEach { measurable ->
val parentData = measurable.parentData // 📌
}
...
}
)
}
Layout 函数的 measurePolicy
参数类型是 MeasurePolicy,虽然不是函数类型,但由于 MeasurePolicy 是一个单抽象方法接口,所以传参时可以使用 lambda 表达式,相当于是创建了一个 MeasurePolicy 的匿名实现类。
kotlin
interface MeasurePolicy {
fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult
...
}
lambda 表达式的第一个参数 measurables 的类型是 List<Measurable>
,表示子组件的集合,每个子组件都是一个 Measurable 对象,我们刚刚就是调用 Measurable.parentData 来获取子组件的 ParentData。
kotlin
interface Measurable : IntrinsicMeasurable {
fun measure(constraints: Constraints): Placeable
}
Measurable 接口里面只有一个方法,那么 parentData
属性很明显是定义在父接口 IntrinsicMeasurable 里面的:
kotlin
interface IntrinsicMeasurable {
/**
* Data provided by the [ParentDataModifier].
*/
val parentData: Any?
...
}
可以看到 parentData 的类型是 Any?
,读取的时候要手动转型为想要的数据类型。
自定义 ParentDataModifier
了解完父组件怎么读取子组件的 parentData 后,我们接下来就要看看怎么给子组件设置 parentData 了。
第一步,我们需要定义一个 ParentDataModifierNode 用来承载数据:
kotlin
// 继承 Modifier.Node 并实现 ParentDataModifierNode 接口
class MyLayoutWeightNode(var weight: Float): Modifier.Node(), ParentDataModifierNode {
override fun Density.modifyParentData(parentData: Any?): Any? = weight
}
第二步,定义一个 Modifier.Element:
kotlin
class MyLayoutWeightElement(val weight: Float) : ModifierNodeElement<MyLayoutWeightNode>() {
// 创建 Modifier.Node
override fun create(): MyLayoutWeightNode = MyLayoutWeightNode(weight)
// 判断 Modifier.Element 是否相等
override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherModifier = other as? MyLayoutWeightElement ?: return false
return weight == otherModifier.weight
}
// 判断数据是否需要更新
override fun hashCode(): Int = weight.hashCode()
// 更新 ParentData 数据
override fun update(node: MyLayoutWeightNode) {
node.weight = weight
}
}
最后一步就是创建自定义修饰符了,只要用 Modifier.then() 方法把 Modifier.Element 添加到 Modifier 链中即可:
kotlin
fun Modifier.myWeight(weight: Float): Modifier = this then MyLayoutWeightElement(weight)
现在,我们就可以使用 myWeight()
修饰符向父组件 MyRow 提供 parent data 数据了
kotlin
MyRow {
Box(Modifier.myWeight(1f))
Box(Modifier.myWeight(2f))
}
限制使用范围
代码是可以用了,但还不够好。Compose 官方提供的 weight() 修饰符,它的使用范围会被限制在 RowScope / ColumnScope 的显式上下文,而不能在其他地方使用,无论是在外部范围还是 RowScope / ColumnScope 的隐式上下文。
而我们上面写的 myWeight()
修饰符是可以在任意地方使用的,这显然不是我们想要的,在 MyRow 组件范围之外使用 myWeight()
修饰符本来就没有意义,不仅会造成 API 污染,还可能与其他 ParentDataModifier 起冲突。那么,怎么把使用范围限制在 MyRow 组件里呢?
照葫芦画瓢,仿照官方的写法,先新建一个 MyRowScope 接口,在里面定义 myWeight()
修饰符,然后再创建一个MyRowScope 的单例对象,实现 myWeight()
修饰符。注意单例对象 MyRowScopeInstance
的可见性是 private,这样可以保证:调用 myWeight()
修饰符的前提是要在 MyRowScope 的上下文环境中,MyRowScopeInstance 是现成的 MyRowScope 环境,但它的可见性是 private,只能由我们提供给外部,外部无法自行获取。
最后再改造一下 MyRow 函数,为函数参数 content 添加 MyRowScope 上下文,然后使用 MyRowScopeInstance 调用 content,这样就成功限制 myWeight()
修饰符不能在 MyRow 组件范围之外使用了。
kotlin
@LayoutScopeMarker
interface MyRowScope {
fun Modifier.myWeight(weight: Float): Modifier
}
private object MyRowScopeInstance : MyRowScope {
override fun Modifier.myWeight(weight: Float): Modifier =
this then MyLayoutWeightElement(weight)
}
@Composable
fun MyRow(
modifier: Modifier = Modifier,
// content: @Composable () -> Unit
content: @Composable MyRowScope.() -> Unit // 📌 为函数参数 content 添加 MyRowScope 上下文
) {
Layout(
// content = content
content = { MyRowScopeInstance.content() }, // 📌 使用 MyRowScopeInstance 调用 content
...
)
}
如果单例对象 MyRowScopeInstance 的可见性是 public,那么外部只要 import 一下就可以直接使用 MyRowScopeInstance 里的 myWeight()
函数,从而失去了限制使用范围的效果:
kotlin
import com.example.MyRowScopeInstance.myWeight
Box {
Modifier.myWeight(1f) // ✅
}
另外,在声明 MyRowScope
接口时,使用了 @LayoutScopeMarker
注解,这个注解的作用是限制 MyRowScope 中的方法不能在隐式上下文中访问。
kotlin
@LayoutScopeMarker // 👈
interface MyRowScope {
fun Modifier.myWeight(weight: Float): Modifier
}
ParentDataModifier 的顺序
不同于 LayoutModifier 会从左到右传递约束条件,ParentDataModifier 会从右往左传递 parent data 数据。也就是说下面代码里的第二个 Box 权重值是最左边的 1f。
kotlin
Row(Modifier.border(1.dp, Black).fillMaxWidth()) {
Box(Modifier.background(Red).height(100.dp).weight(1f))
Box(Modifier.background(Blue).height(100.dp).weight(1f).weight(2f)) // 1f <-- 2f <--
}
注意,刚刚说的是从右往左传递 parent data,到底是怎么传递的呢?回头看刚才我们定义的 ParentDataModifierNode,Density.modifyParentData(parentData)
方法有一个参数 parentData: Any?
,它就是右边传递过来的 parent data,如果没有那么就是 null,而 modifyParentData()
方法的返回值会继续传递给左边的 ParentDataModifier,如果左边没有 ParentDataModifier 了,那么就会传递给父组件。
kotlin
class MyLayoutWeightNode(var weight: Float): Modifier.Node(), ParentDataModifierNode {
override fun Density.modifyParentData(parentData: Any?): Any? = weight
}
通篇下来,不知你是否注意到一件事,子组件只能给父组件传递 1 个 parent data,如果要传递多个数据,那么只能把多个数据封装成一个类,用这个类作为 parent data 的实际类型,然后在传递过程中合并不同的数据。