前言
前面两节中,我介绍了在Modifier中最重要的LayoutModifier
和DrawModifier
,这涉及到了Compose中组件的布局和绘制能力,尤其是在Modifier初始化过程中,创建的NodeChain
,这个对理解Modifier摆放顺序的时候会有很大的帮助,从这一节开始,将会介绍一些常用的Modifier底层实现原理,比如本节的ParentDataModifier
。
1 ParentDataModifier介绍
官方的解释:
用于提供数据给到父容器,在父容器测量和摆放的过程中可以拿到这些数据。
/**
* A [Modifier] 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.
*/
@JvmDefaultWithCompatibility
interface ParentDataModifier : Modifier.Element {
/**
* Provides a parentData, given the [parentData] already provided through the modifier's chain.
*/
fun Density.modifyParentData(parentData: Any?): Any?
}
所以从命名来看,其实就是用来提供父容器的布局信息的,我们其实在之前已经用到过相关的Modifier,例如align
。
@Composable
fun ModifierParentData() {
Box(modifier = Modifier.size(100.dp)) {
Text(text = "文案", Modifier.align(Alignment.Center))
}
}
它具体的作用就是让Text
组件在Box
居中展示,看下源码:
@Stable
override fun Modifier.align(alignment: Alignment) = this.then(
BoxChildData(
alignment = alignment,
matchParentSize = false,
inspectorInfo = debugInspectorInfo {
name = "align"
value = alignment
}
)
)
private class BoxChildData(
var alignment: Alignment,
var matchParentSize: Boolean = false,
inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo
) : ParentDataModifier, InspectorValueInfo(inspectorInfo) {
override fun Density.modifyParentData(parentData: Any?) = this@BoxChildData
// ......
}
从源码看,align
会创建BoxChildData
与调用者融合,而BoxChildData
就是继承自ParentDataModifier
。在modifyParentData
函数中,是将自己返回了,那么BoxChildData
其实就是外层父组件会拿到的parentData
。
2 ParentDataModifier的使用
像先前我们使用LayoutModifier
或者DrawModifier
,我们可以通过自定义的方式来影响系统的测量绘制过程,例如:
@Composable
fun ModifierParentData() {
Box(
Modifier
.size(100.dp)
.drawWithContent {
drawCircle(Color.Red)
drawContent()
})
}
我只要调用了drawWithContent
,那么系统在绘制的时候,就会筛选拿到Node.Draw
类型的节点执行其内部的draw
函数,哪怕我就创建了一个匿名内部类,都可以执行其自定义绘制操作。
@Composable
fun ModifierParentData() {
Box(
Modifier
.size(100.dp)
.drawWithContent {
drawCircle(Color.Red)
drawContent()
}.then(object : DrawModifier{
override fun ContentDrawScope.draw() {
drawCircle(Color.Red)
drawContent()
}
}))
}
但是,ParentDataModifier
可以这么做吗?
@Composable
fun ModifierParentData() {
Box(
Modifier
.size(100.dp)
.then(object : DrawModifier {
override fun ContentDrawScope.draw() {
drawCircle(Color.Red)
drawContent()
}
})
.then(
object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? {
return null
}
}
))
}
显然不可以,因为自定义的ParentDataModifier
并不能影响现阶段Compose的测量布局流程,因为Box
外层的父布局不认这个Modifier。
2.1 如何使用自定义ParentDataModifier
例如,我们在Box
中使用align
函数的时候,通过源码我们可以看到,在Box
提供的BoxScope
作用域下,会显示声明一个align
函数。BoxScope
对应的实现类为BoxScopeInstance
,会实现align
函数,做Modifier具体实现类的创建。
/**
* A BoxScope provides a scope for the children of [Box] and [BoxWithConstraints].
*/
@LayoutScopeMarker
@Immutable
interface BoxScope {
/**
* Pull the content element to a specific [Alignment] within the [Box]. This alignment will
* have priority over the [Box]'s `alignment` parameter.
*/
@Stable
fun Modifier.align(alignment: Alignment): Modifier
// ......
}
其实不止是Box
,任意容器类型的组件,如果要使用ParentDataModifier
,那么都需要在其作用域内声明对应的函数,并完成实现。
所以:我们自定义的ParentDataModifier
,即单独实现匿名内部类的这种方式,是无效的。所以在使用官方提供的组件的时候,不能使用自定义ParentDataModifier
,只能使用它定义好的Modifier扩展函数。 那么ParentDataModifier
是什么场景下会使用呢?在自定义布局的时候会使用。
@Composable
fun MyContainer(modifier: Modifier = Modifier,content: @Composable MyContainerScope.() -> Unit) {
Layout(content = {content}, modifier) { measurables, constaints ->
measurables.forEach {
val value = it.parentData as? String
}
layout(100,100){
}
}
}
fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult
例如在自定义布局容器时,一般都会在底层使用Layout
函数,在测量的时候会拿到一组Measurable
数据,这些数据就是子组件测量之后统一给到父容器。通过遍历measurables
数组,可以拿到单一的Measurable
,可以通过这个具体的对象拿到对应的ParentData
。
但是拿到的前提是,子组件会设置这个Modifier属性,例如定义了getStringData
扩展函数,这个是在MyContainerScope
作用域下才会使用到,防止API被污染,防止开发者在其他作用域下随意调用
@LayoutScopeMarker
@Immutable // 减少不必要的重组
interface MyContainerScope {
@Stable
fun Modifier.getStringData(): Modifier
}
internal object MyContainerScopeInstance : MyContainerScope {
@Stable
override fun Modifier.getStringData(): Modifier {
return this.then(object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? {
return "MyContainerScope"
}
})
}
}
子组件使用如下所示,当Text
文本设置了getStringData
之后,父容器会接收到子组件的配置信息。
MyContainer{
Text(text = "",Modifier.getStringData())
}
2.2 ParentDataModifier注意事项
假设现在我这么使用,我重新提供了一个扩展函数weightAgain
,
@Stable
override fun Modifier.weightAgain(weight: Float): Modifier {
return this.then(object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? {
return weight
}
})
}
在使用的时候连续调用了2次,但是传值是不一样的。
MyContainer {
Text(text = "",
Modifier
.weightAgain(1f)
.weightAgain(2f))
}
前面我在讲Modifier初始化的时候,同步阶段会从右向左更新,如果是按照Modifier中定义的,将传入的值作为modifyParentData
的返回值,那么最终同步完成之后,innerCoordinator
内部的weightAgain
就是1f。
@Stable
override fun Modifier.weightAgain(weight: Float): Modifier {
return this.then(object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? {
return (parentData as? Float)?.plus(weight)
}
})
}
当然也可以组合,在modifyParentData(parentData: Any?)
函数中的参数,就是上一个调用weightAgain
传入的值,但是一般情况下没有这么用的,可以但是没有必要。
但是下面的这个场景,是非常可能遇到的:
MyContainer {
Text(text = "",
Modifier
.getStringData()
.weightAgain(2f))
}
两个不同的ParentDataModifier
组合在一起,那么在MyContainer
中取值ParentData
的时候,该怎么取?取Float
还是String
?
@LayoutScopeMarker
@Immutable
interface MyContainerScope {
@Stable
fun Modifier.getStringData(value: String): Modifier
@Stable
fun Modifier.weightAgain(weight: Float): Modifier
}
class MultiData(var value: String = "", var weight: Float = 0f)
internal object MyContainerScopeInstance : MyContainerScope {
@Stable
override fun Modifier.getStringData(value: String): Modifier {
return this.then(object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? {
return (parentData as? MultiData ?: MultiData()).also {
it.value = value
}
}
})
}
@Stable
override fun Modifier.weightAgain(weight: Float): Modifier {
return this.then(object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any? {
return (parentData as? MultiData ?: MultiData()).also {
it.weight = weight
}
}
})
}
}
这个情况下,需要通过实体数据类,来封装多参数的数据,在执行modifyParentData
函数的时候,取出对应的MultiData
,对其参数赋值。
3 ParentDataModifier原理
通过前面我对于ParentDataModifier
使用的介绍,已经知道了子组件在设置的Parentdata会在父容器Layout
的时候去获取,所以我们看下在获取parentData
的时候是如何拿到的?
// NodeCoordinator.kt
override val parentData: Any?
get() {
var data: Any? = null
val thisNode = tail
if (layoutNode.nodes.has(Nodes.ParentData)) {
with(layoutNode.density) {
layoutNode.nodes.tailToHead {
if (it === thisNode) return@tailToHead
if (it.isKind(Nodes.ParentData) && it is ParentDataModifierNode) {
data = with(it) { modifyParentData(data) }
}
}
}
}
return data
}
首先创建一个data
数据,默认为null;然后判断NodeChain
中是否存在Nodes.ParentData
类型的节点,如果不存在,那么直接返回null;
如果NodeChain
存在Nodes.ParentData
类型的节点,以上面介绍的例子:
graph LR
Head --> getStringData --> weightAgain --> Tail
会从tail遍历到head,如果当前节点是ParentDataModifierNode
,那么就会执行其modifyParentData
函数,注意这里是会给data
重新赋值,而且会把上次的data
作为参数传递到下一个遍历的ParentDataModifierNode
中的modifyParentData
函数。这也就是为什么在前面介绍的时候,如果两个连续调用的ParentDataModifier
参数会被覆盖的问题。
遍历完成之后,将data返回,那么父容器就会拿到对应的子组件的数据信息。