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 的实际类型,然后在传递过程中合并不同的数据。

相关推荐
雨白10 小时前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
kk爱闹11 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空13 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭13 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日14 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安14 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑14 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟19 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡20 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi0020 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体