Compose编程思想 -- Compose中的经典Modifier(ParentDataModifier)

676 阅读6分钟

前言

前面两节中,我介绍了在Modifier中最重要的LayoutModifierDrawModifier,这涉及到了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返回,那么父容器就会拿到对应的子组件的数据信息。