美文网首页
Jetpack Compose ParentDataModifi

Jetpack Compose ParentDataModifi

作者: 头秃到底 | 来源:发表于2024-01-15 18:24 被阅读0次

    ParentDataModifier

    什么是 ParentDataModifier?

    在 Compose 中,有一类 Modifier 叫做 ParentDataModifier,它们的作用是为父节点提供数据,这些数据可以在父节点的测量和布局过程中被读取,通常用于告诉父节点子节点应该如何被测量和布局。

    /**
     * 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() 修饰符:

    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))
    }
    
    
    image.png

    在布局测量过程中,Row 会读取子节点设置的 ParentData 也就是 weight 权重值,并根据 weight 值来计算子节点的宽度。

    "ParentDataModifier" 的命名还是很准确的,它的作用就是为父节点(Parent)提供数据(Data),也就是说,虽然 weight() 修饰符写在子节点上,但是填入的数据真正被使用的地方是在父节点。

    如何自定义 ParentDataModifier?

    读取子组件的 parentData

    要自定义 ParentDataModifier,首先我们要搞明白:对于父组件来说,怎么获取子组件设置的 ParentData?

    在 Compose 中,像 Row、Box 等等 Layout 组件,背后都使用了 Layout 函数:

    @UiComposable
    @Composable
    inline fun Layout(
        content: @Composable @UiComposable () -> Unit,
        modifier: Modifier = Modifier,
        measurePolicy: MeasurePolicy
    ) { ... }
    
    

    如果要手写一个 Layout 组件,并获取子组件设置的 ParentData,我们可以这样写:

    @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 的匿名实现类。

    interface MeasurePolicy {
        fun MeasureScope.measure(
            measurables: List<Measurable>,
            constraints: Constraints
        ): MeasureResult
    
        ...
    }
    
    

    lambda 表达式的第一个参数 measurables 的类型是 List<Measurable>,表示子组件的集合,每个子组件都是一个 Measurable 对象,我们刚刚就是调用 Measurable.parentData 来获取子组件的 ParentData。

    interface Measurable : IntrinsicMeasurable {
        fun measure(constraints: Constraints): Placeable
    }
    
    

    Measurable 接口里面只有一个方法,那么 parentData 属性很明显是定义在父接口 IntrinsicMeasurable 里面的:

    interface IntrinsicMeasurable {
        /**
         * Data provided by the [ParentDataModifier].
         */
        val parentData: Any?
        
        ...
    }
    
    

    可以看到 parentData 的类型是 Any?,读取的时候要手动转型为想要的数据类型。

    自定义 ParentDataModifier

    了解完父组件怎么读取子组件的 parentData 后,我们接下来就要看看怎么给子组件设置 parentData 了。

    第一步,我们需要定义一个 ParentDataModifierNode 用来承载数据:

    // 继承 Modifier.Node 并实现 ParentDataModifierNode 接口
    class MyLayoutWeightNode(var weight: Float): Modifier.Node(), ParentDataModifierNode {
        override fun Density.modifyParentData(parentData: Any?): Any? = weight
    }
    
    

    第二步,定义一个 Modifier.Element:

    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 链中即可:

    fun Modifier.myWeight(weight: Float): Modifier = this then MyLayoutWeightElement(weight)
    
    

    现在,我们就可以使用 myWeight() 修饰符向父组件 MyRow 提供 parent data 数据了

    MyRow {
        Box(Modifier.myWeight(1f))
        Box(Modifier.myWeight(2f))
    }
    
    

    限制使用范围

    代码是可以用了,但还不够好。Compose 官方提供的 weight() 修饰符,它的使用范围会被限制在 RowScope / ColumnScope 的显式上下文,而不能在其他地方使用,无论是在外部范围还是 RowScope / ColumnScope 的隐式上下文。

    image.png

    而我们上面写的 myWeight() 修饰符是可以在任意地方使用的,这显然不是我们想要的,在 MyRow 组件范围之外使用 myWeight() 修饰符本来就没有意义,不仅会造成 API 污染,还可能与其他 ParentDataModifier 起冲突。那么,怎么把使用范围限制在 MyRow 组件里呢?

    照葫芦画瓢,仿照官方的写法,先新建一个 MyRowScope 接口,在里面定义 myWeight() 修饰符,然后再创建一个MyRowScope 的单例对象,实现 myWeight() 修饰符。注意单例对象 MyRowScopeInstance 的可见性是 private,这样可以保证:调用 myWeight() 修饰符的前提是要在 MyRowScope 的上下文环境中,MyRowScopeInstance 是现成的 MyRowScope 环境,但它的可见性是 private,只能由我们提供给外部,外部无法自行获取。

    最后再改造一下 MyRow 函数,为函数参数 content 添加 MyRowScope 上下文,然后使用 MyRowScopeInstance 调用 content,这样就成功限制 myWeight() 修饰符不能在 MyRow 组件范围之外使用了。

    @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() 函数,从而失去了限制使用范围的效果:

    import com.example.MyRowScopeInstance.myWeight
    
    Box {
        Modifier.myWeight(1f) // ✅
    }
    
    

    另外,在声明 MyRowScope 接口时,使用了 @LayoutScopeMarker 注解,这个注解的作用是限制 MyRowScope 中的方法不能在隐式上下文中访问。

    @LayoutScopeMarker // 👈
    interface MyRowScope {
        fun Modifier.myWeight(weight: Float): Modifier
    }
    
    

    ParentDataModifier 的顺序

    不同于 LayoutModifier 会从左到右传递约束条件,ParentDataModifier 会从右往左传递 parent data 数据。也就是说下面代码里的第二个 Box 权重值是最左边的 1f。

    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 <--
    }
    
    
    image.png

    注意,刚刚说的是从右往左传递 parent data,到底是怎么传递的呢?回头看刚才我们定义的 ParentDataModifierNode,Density.modifyParentData(parentData) 方法有一个参数 parentData: Any?,它就是右边传递过来的 parent data,如果没有那么就是 null,而 modifyParentData() 方法的返回值会继续传递给左边的 ParentDataModifier,如果左边没有 ParentDataModifier 了,那么就会传递给父组件。

    class MyLayoutWeightNode(var weight: Float): Modifier.Node(), ParentDataModifierNode {
        override fun Density.modifyParentData(parentData: Any?): Any? = weight
    }
    
    

    通篇下来,不知你是否注意到一件事,子组件只能给父组件传递 1 个 parent data,如果要传递多个数据,那么只能把多个数据封装成一个类,用这个类作为 parent data 的实际类型,然后在传递过程中合并不同的数据。

    相关文章

      网友评论

          本文标题:Jetpack Compose ParentDataModifi

          本文链接:https://www.haomeiwen.com/subject/rnuhodtx.html