ParentDataModifier
ParentDataModifier 的作用
ParentDataModifier 的底层存储方式和 DrawModifier 以及 PointerInputModifier 相同,都是存储在链表中,整个链表在指定的数组索引位置:
LayoutNode.kt
override var modifier: Modifier = Modifier
set(value) {
...
node.updateFrom(value)
...
}
NodeChain.kt
private fun insertParent(node: Modifier.Node, child: Modifier.Node): Modifier.Node {
// 链表头插法将 ParentDataModifier 插入链表头
val theParent = child.parent
if (theParent != null) {
theParent.child = node
node.parent = theParent
}
child.parent = node
node.child = child
return node
}
因为 Compose 是一个纯 UI 的框架,无论是 DrawModifier、PointerInputModifier 两者之间都没有关联,同理 ParentDataModifier,不过 ParentDataModifier 是和测量布局有一点关联用于辅助的 Modifier。
我们在 LinearLayout 设置某个控件宽度/高度填充满,会使用到 layout_weight 属性,在 Compose 同样的也是提供了 weight:
setContent {
Row {
// weight 横向填满
Box(Modifier.size(40.dp).background(Color.Blue).weight(1f))
Box(Modifier.size(40.dp).background(Color.Red))
Box(Modifier.size(40.dp).background(Color.Green))
}
}
Modifier.weight() 毕竟和测量布局有关,或许你第一反应会认为它应该是一个 LayoutModifier,到底是不是,我们去看下 Modifier.weight() 的源码:
Row.kt
@Stable
fun Modifier.weight(
weight: Float,
fill: Boolean = true
): Modifier
internal object RowScopeInstance : RowScope {
@Stable
override fun Modifier.weight(weight: Float, fill: Boolean): Modifier {
...
return this.then(
LayoutWeightImpl(
weight = weight,
fill = fill,
inspector = debugInspectorInfo {
name = "weight"
value = weight
properties["weight"] = weight
properties["fill"] = fill
}
)
)
}
}
RowColumnImpl.kt
// 实现的是 ParentDataModifier
internal class LayoutWeightImpl(
val weight: Float,
val fill: Boolean,
inspectorInfo: InspectorInfo.() -> Unit
) : ParentDataModifier, InspectorValueInfo(insepectorInfo) {
...
}
ParentDataModifier.kt
internal ParentDataModifier : Modifier.Element {
fun Density.modifyParentData(parentData: Any?): Any?
}
Modifier.weight() 并不是一个 LayoutModifier,而是一个 ParentDataModifier,因为 ParentDataModifier 就只继承了 Modifier.Element 接口。
那么 ParentDataModifier 是怎么作用影响到测量布局的?
ParentDataModifier 实际上是一个辅助的 Modifier,用于给外层包裹的 Composable 使用的,更具体讲是提供给被设置的子组件的父组件在测量子组件的时候使用,让父组件能更好的测量子组件。用一开始的例子来理解:
setContent {
Row {
// ParentDataModifier 是提供给 Row 使用的帮助测量 Box
Box(Modifier.size(40.dp).background(Color.Blue).weight(1f))
Box(Modifier.size(40.dp).background(Color.Red))
Box(Modifier.size(40.dp).background(Color.Green))
}
}
也许你会有疑问:为什么不是 LayoutModifier?LayoutModifier 怎么就不能完成这个处理呢?
LayoutModifier 它只能作用于单一的组件,即对单一的组件处理测量布局,比如上面例子的 Box(),对 Box() 设置的 LayoutModifier 只会它生效;而类似 Modifier.weight() 这种场景,如果有多个组件都设置,它就不能依靠单一组件就计算出测量结果:
setContent {
Row {
// 三个组件都设置了 Modifier.weight(),依靠单一组件无法测量出需要多少宽度
// 需要互相知道组件宽度才能确定自己宽度
Box(Modifier.size(40.dp).background(Color.Blue).weight(1f))
Box(Modifier.size(40.dp).background(Color.Red).weight(1f))
Box(Modifier.size(40.dp).background(Color.Green).weight(1f))
}
}
为了能测量出子组件所需要的宽度/高度,Compose 没有提供给子组件获取相邻组件之间的测量结果,这样反而复杂了,所以提供了更好的方案,让父组件协助计算测量。
实际上传统的 View 系统也是用这种方案,在 ViewGroup 提供的子 View 设置的 layout_width、layout_height、layout_weight 等属性也是让 ViewGroup 综合测量规则之后完成最终测量结果。
所以这也是为什么命名为 ParentDataModifier,ParentDataModifier 虽然设置给子组件,但却是提供给父组件使用的数据,辅助子组件进行尺寸测量和绘制计算的。
当然不止是 Modifier.weight() 是 ParentDataModifier,比如 Modifier.layoutId() 也是 ParentDataModifier,它并不是传统 View 系统的 layout_id 属性,更类似于 tag 的功能的辅助工具。使用它能更方便我们在自定义布局时根据不同的 Modifier.layoutId() 处理不同的测量方式:
@Composable
fun CustomLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
Layout(content, modifier) { measurables, constraints ->
measurables.forEach {
val data = it.parentData as? Layout2Data
// 根据提供的不同的 Modifier.layoutId() 使用不同的测量方式
when (it.layoutId) {
"big" -> it.measure(constraints.xxx)
"small" -> it.measure(constraints.yyy)
else -> it.measure(constraints)
}
}
layout(100, 100) {
....
}
}
}
setContent {
CustomLayout(Modifier.size(40.dp)) {
Text("BigText", Modifier.layoutId("big"))
Text("SmallText", Modifier.layoutId("small"))
Box(Modifier.size(20.dp).background(Color.Red))
}
}
ParentDataModifier 的写法
那如果我们要自己写一个 ParentDataModifier,要怎么写呢?
一般情况下我们是不需要自己去写 ParentDataModifier,因为这些外层组件已经规范了 ParentDataModifier 的处理,直接写返回增加了复杂度;更多时候我们是直接使用已经封装好的 ParentDataModifier 使用,例如 Modifier.weight():
setContent {
Row {
Box(
Modifier
.size(40.dp)
.background(Color.Blue)
.weight(1f) // 正确使用方式
// 一般不会这么写
.then(object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? {
...
}
})
)
}
}
正确的 ParentDataModifier 的使用是,当自定义的 Composable 函数里面需要用到子组件的独特的信息的时候,才会使用到 ParentDataModifier。
使用 ParentDataModifier 会有三个条件:
-
自定义 Composable 函数需要用到 Layout(),不然就没办法拿到每个子组件的 Measurable
-
需要在内部测量布局的地方获取 ParentDataModifier 给我们的信息,利用它们来计算
-
写一个可以提供数据的 ParentDataModifier 函数,即提供一个类似 Modifier.weight() 的函数,让用户可以方便的使用
@Composable
fun CustomLayout2(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
// 第一个条件:需要用到 Layout()
Layout(content, modifier) { measurables, constraints ->
measurables.forEach {
// 第二个条件:获取 ParentDataModifier 所提供的信息
// parentData 类型为 Any?,自己提供什么类型就强转为甚么类型
// weightData() 提供的是 Float,所以这里强转的 Float
val data = it.parentData as? Float
}
}
}
// 第三个条件:提供一个方便使用 ParentDataModifier 的函数
fun Modifier.weightData(weight: Float) = then(object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? {
return weight // 直接返回了传参
}
})
根据 demo 按第三个条件的实现要求提供了 ParentDataModifier,并且直接返回了传参 weight,它有一个参数 parentData,它是右边的 ParentDataModifier 提供的参数:
// 第一个 weightData() 的 parentData 参数就是右边 weightData() 的参数 2f
// 第一个 weightData() 提供的数据最终会给到外层 CustomLayout 使用
// 即只有第一个 weightData() 最终提供给 CustomLayout 使用,其他的 ParentDataModifier 会被丢弃
CustomLayout {
Text("1", Modifier.weightData(1f).weightData(2f))
Text("2")
}
如果在同一个组件使用了相同的 ParentDataModifier,那么只有一个最终提供给外层的 ParentDataModifier 生效。因为 ParentDataModifier 就是提供属性的,就像 xml 我们不会在控件提供两个 layout_weight。所以一般自己写的 ParentDataModifier 我们直接返回传参,没使用 parentData 参数。
那么 parentData 参数是什么作用?
如果需求需要不同的 ParentDataModifier 叠加作用,为了能在外层统一类型强转 parentData 参数,我们会需要用一个数据类包装承载不同的 ParentDataModifier 信息,并且糅合 parentData 参数:
// 用一个类包装两种不同的 ParentDataModifier 提供的信息
class Layout2Data(var weight: Float = 0f, var big: Boolean = false)
fun Modifier.weightData(weight: Float) = then(object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? {
// 糅合 parentData 参数
return if (parentData == null) Layout2Data(weight)
else (parentData as Layout2Data).apply { this.weight = weight }
}
})
fun Modifier.bigData(big: Boolean) = then(object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? {
return ((parentData as? Layout2Data) ?: Layout2Data()).also { it.big = big }
}
})
@Composable
fun CustomLayout2(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
Layout(content, modifier) { measurables, constraints ->
measurables.forEach {
// parentData 用了一个数据类包装为同一种类型
// 这样就能直接强转类型为数据类,也能拿到不同的 ParentDataModifier 信息
val data = it.parentData as? Layout2Data
val big = data?.big
val weight = data?.weight
}
}
}
CustomLayout {
Text("1", Modifier.weightData(1f).bigData(true))
Text("2")
}
如何编写 ParentDataModifier 重点已经讲解结束,但还有一个需要留意的事项:ParentDataModifier 应该只限制在外层 Composable 使用。比如我们看下 Row() 就有对 ParentDataModifier 的限制使用:
@Composable
inline fun Row(
modifier: Modifier = Modifier,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
verticalAlignment: Alignment.Vertical = Alignment.Top,
content: @Composable RowScope.() -> Unit // 限制在 RowScope
) {
val measurePolicy = rowMeasurePolicy(horizontalArrangement, verticalAlignment)
Layout(
content = { RowScopeInstance.content() },
measurePolicy = measurePolicy,
modifier = modifier
)
}
@LayoutScopeMarker
@Immutable
@JvmDefaultWithCompatibility
interface RowScope {
@Stable
fun Modifier.weight(
/*@FloatRange(from = 0.0, fromInclusive = false)*/
weight: Float,
fill: Boolean = true
): Modifier
...
}
Row() 的源码其实是使用了 @LayoutScopeMarker
限制 ParentDataModifier 的使用范围,我们也可以照葫芦画瓢给自己的 ParentDataModifier 做一个限制:
@LayoutScopeMarker
@Immutable
object CustomLayout2Scope {
fun Modifier.weightData(weight: Float) = then(object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? {
// 糅合 parentData 参数
return if (parentData == null) Layout2Data(weight)
else (parentData as Layout2Data).apply { this.weight = weight }
}
})
fun Modifier.bigData(big: Boolean) = then(object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? {
return ((parentData as? Layout2Data) ?: Layout2Data()).also { it.big = big }
}
})
}
@Composable
fun CustomLayout2(modifier: Modifier = Modifier, content: @Composable, CustomLayout2Scope.() -> Unit) {
Layout({ CustomLayout2Scope.content() }, modifier) { measurable, constraints ->
measureables.forEach {
}
}
}
setContent {
CustomLayout(Modifier.size(40.dp)) {
Text("1",
Modifier
.layoutId("big")
.weightData(1f)
)
Box {
Text("3", Modifier.weightData(1f)) // 提示不在范围无法使用
}
}
}
ParentDataModifier 的原理
接下来开始讲解 ParentDataModifier 的原理。
我们在 ParentDataModifier 写法讲解时,其中一个步骤是获取的 parentData,所以源码也是从 parentData 参数开始分析:
interface IntrinsicMeasurable {
val parentData: Any?
...
}
NodeCoordinator.kt
override val parentData: Any?
get() {
var data: Any? = null
val thisNode = tail
with(layoutNode.density) {
// 循环遍历调用
layoutNode.nodes.tailToHead {
if (it === thisNode) return@tailToHead
if (it.isKind(Nodes.ParentData) && it is ParentDataModifierNode) {
// 获取 ParentDataModifier 的信息回调给下一个
data = with(it) { modifyParentData(data) }
}
}
}
return data
}
源码不多,总的来说可以分成三个步骤:
-
尝试获取 ParentDataModifier 的链表表头节点
-
如果没有 ParentDataModifier,那么就返回 null
-
如果有 ParentDataModifier,获取 parentData 数据给到下一个节点继续传递,直到循环结束
从源码也可以分析到,ParentDataModifier 和 LayoutModifier 没有任何关联,即 ParentDataModifier 写在 LayoutModifier 的左边还是右边是没有影响的,因为 ParentDataModifier 是提供给外层 Composable 组件使用的。
// ParentDataModifier 写在 LayoutModifier 前面还是后面都不影响显示结果
setContent {
Row {
Box(
Modifier
.size(40.dp)
.background(Color.Green)
// .weight(1f)
.padding(20.dp)
.weight(1f)
)
}
}