Jetpack Compose ParentDataModifier

ParentDataModifier

什么是 ParentDataModifier?

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

kotlin 复制代码
/**
 * 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() 修饰符:

kotlin 复制代码
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 函数:

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

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

kotlin 复制代码
@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 的匿名实现类。

kotlin 复制代码
interface MeasurePolicy {
    fun MeasureScope.measure(
        measurables: List<Measurable>,
        constraints: Constraints
    ): MeasureResult

    ...
}

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

kotlin 复制代码
interface Measurable : IntrinsicMeasurable {
    fun measure(constraints: Constraints): Placeable
}

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

kotlin 复制代码
interface IntrinsicMeasurable {
    /**
     * Data provided by the [ParentDataModifier].
     */
    val parentData: Any?
    
    ...
}

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

自定义 ParentDataModifier

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

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

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

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

kotlin 复制代码
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 链中即可:

kotlin 复制代码
fun Modifier.myWeight(weight: Float): Modifier = this then MyLayoutWeightElement(weight)

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

kotlin 复制代码
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 组件范围之外使用了。

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

kotlin 复制代码
import com.example.MyRowScopeInstance.myWeight

Box {
    Modifier.myWeight(1f) // ✅
}

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

kotlin 复制代码
@LayoutScopeMarker // 👈
interface MyRowScope {
    fun Modifier.myWeight(weight: Float): Modifier
}

ParentDataModifier 的顺序

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

kotlin 复制代码
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 <--
}

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

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

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

相关推荐
豆 腐1 小时前
MySQL【四】
android·数据库·笔记·mysql
想取一个与众不同的名字好难3 小时前
android studio导入OpenCv并改造成.kts版本
android·ide·android studio
Jewel1054 小时前
Flutter代码混淆
android·flutter·ios
卡卡_R-Python4 小时前
子集选择——基于R语言实现(最优子集选择法、逐步回归法、Lasso回归法、交叉验证法)
回归·r语言·kotlin
Yawesh_best5 小时前
MySQL(5)【数据类型 —— 字符串类型】
android·mysql·adb
曾经的三心草7 小时前
Mysql之约束与事件
android·数据库·mysql·事件·约束
guoruijun_2012_411 小时前
fastadmin多个表crud连表操作步骤
android·java·开发语言
Winston Wood11 小时前
一文了解Android中的AudioFlinger
android·音频
B.-13 小时前
Flutter 应用在真机上调试的流程
android·flutter·ios·xcode·android-studio
有趣的杰克13 小时前
Flutter【04】高性能表单架构设计
android·flutter·dart