Jetpack Compose ParentDataModifier

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))
}

在这里插入图片描述

在布局测量过程中,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 的隐式上下文。

在这里插入图片描述

而我们上面写的 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 <--
}

ParentDataModifier从右往左传递数据.jpg

注意,刚刚说的是从右往左传递 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 的实际类型,然后在传递过程中合并不同的数据。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题
图片

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值