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

前语

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