当前位置: 首页 > article >正文

Compose 实践与探索十 —— 其他预先处理的 Modifier

1、PointerInputModifier

PointerInputModifier 用于定制触摸(包括手指、鼠标、悬浮)反馈算法,实现手势识别。

1.1 基本用法

最简单的使用方式就是通过 Modifier.clickable() 响应点击事件:

Box(Modifier.size(40.dp).background(Color.Blue).clickable { println("点击事件") })

稍微复杂一点,功能更全面的是使用 Modifier.combinedClickable(),支持单击、双击、长按事件 :

    Box(
        Modifier
            .size(40.dp)
            .background(Color.Blue)
            .combinedClickable(
                onDoubleClick = { println("双击事件") },
                onLongClick = { println("长按事件") }
            ) { println("单击事件") }
    )

最后的 lambda 表达式 onClick 表示的单击事件必须要传:

@ExperimentalFoundationApi
fun Modifier.combinedClickable(
    enabled: Boolean = true,
    onClickLabel: String? = null,
    role: Role? = null,
    onLongClickLabel: String? = null,
    onLongClick: (() -> Unit)? = null,
    onDoubleClick: (() -> Unit)? = null,
    onClick: () -> Unit
)

再更加底层一点的 API 是 pointerInput(),在它内部调用 detectTapGestures() 可以检测到单击、双击、长按与按压:

    Box(modifier = Modifier
        .size(40.dp)
        .background(Color.Blue)
        .pointerInput(Unit) {
            detectTapGestures(
                onDoubleTap = { println("双击事件") },
                onLongPress = { println("长按事件") },
                onPress = { println("检测到按压") }
            ) {
                println("单击事件")
            }
        })

onPress() 是只要有屏幕触碰就会触发,比如分别进行单击、长按与双击,输出如下:

检测到按压
单击事件
检测到按压
长按事件
检测到按压
检测到按压
双击事件

detectTapGestures() 与 combinedClickable() 都提供了对单击、双击、长按的检测,二者有哪些区别?其实从 tap 与 click 这两个单词的含义中可以窥见一二:

  • tab 一般是指物理上的触摸,在 detectTapGestures() 中就是真实发生在屏幕上的触摸事件才会触发相应的事件回调,而不会反馈通过鼠标触发的事件
  • click 这个点击,除了在物理屏幕上的点击,也可以是系统指令发出的点击。也就是说它既能反馈物理屏幕触发的事件,也能响应诸如鼠标这类的,没有物理触碰,但由系统发出的指令事件

combinedClickable() 的底层是通过 detectTapGestures() 实现的,并且一些效果是通过 onPress 这个触摸反馈达成的。

PointerInputScope 还提供了更更底层的 API —— awaitPointerEventScope(),连事件监听都是自己做的:

    Modifier.pointerInput(Unit) {
        // 循环监听事件,就是监听一个手势中的所有事件,不加的话检测到一个事件协程就退出了
        forEachGesture {
            awaitPointerEventScope {
                val down = awaitFirstDown()
            }
        }
    }

1.2 基本原理

fun Modifier.pointerInput(
    key1: Any?,
    block: suspend PointerInputScope.() -> Unit
): Modifier = composed(
    inspectorInfo = debugInspectorInfo {
        name = "pointerInput"
        properties["key1"] = key1
        properties["block"] = block
    }
) {
    val density = LocalDensity.current
    val viewConfiguration = LocalViewConfiguration.current
    remember(density) { SuspendingPointerInputFilter(viewConfiguration, density) }.also { filter ->
        LaunchedEffect(filter, key1) {
            filter.coroutineScope = this
            filter.block()
        }
    }
}

SuspendingPointerInputFilter 实现了 PointerInputModifier 接口:

@Suppress("DEPRECATION_ERROR")
internal class SuspendingPointerInputFilter(
    override val viewConfiguration: ViewConfiguration,
    density: Density = Density(1f)
) : PointerInputFilter(),
    PointerInputModifier,
    PointerInputScope,
    Density by density {

PointerInputModifier 除了 SuspendingPointerInputFilter 还有一个实现类 PointerInteropFilter 是负责 Compose 与 View 系统交互的触摸反馈的,与 Compose 本身的内部结构没什么关系,因此我们主要看 SuspendingPointerInputFilter。

原理主要分两部分,一是看遍历 Modifier 链时如何处理 PointerInputModifier,二是看 PointerInputModifier 本身是如何工作的。

遍历 Modifier 链时,对 PointerInputModifier 的处理与上篇讲过的 DrawModifier 几乎完全相同。在 LayoutNode 的 modifier 属性的 set() 内,通过 foldOut() 遍历 Modifier 链,调用 addBeforeLayoutModifier() 将 PointerInputModifier 添加到当前正在遍历的 LayoutNodeWrapper 的 entities 数组中:

    override var modifier: Modifier = Modifier
        set(value) {
            ...
            val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->
                if (mod is RemeasurementModifier) {
                    mod.onRemeasurementAvailable(this)
                }

                // 将指定类型的 Modifier 添加到 toWrap 内的数组的指定类型的链表头上
                toWrap.entities.addBeforeLayoutModifier(toWrap, mod)

                if (mod is OnGloballyPositionedModifier) {
                    getOrCreateOnPositionedCallbacks() += toWrap to mod
                }

                val wrapper = if (mod is LayoutModifier) {
                    // Re-use the layoutNodeWrapper if possible.
                    (reuseLayoutNodeWrapper(toWrap, mod)
                        ?: ModifiedLayoutNode(toWrap, mod)).apply {
                        onInitialize()
                        updateLookaheadScope(mLookaheadScope)
                    }
                } else {
                    toWrap
                }
                wrapper.entities.addAfterLayoutModifier(wrapper, mod)
                wrapper
            }

            setModifierLocals(value)

            outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper
            layoutDelegate.outerWrapper = outerWrapper

            ...
        }

PointerInputModifier 与 DrawModifier 一样,是 addBeforeLayoutModifier() 内处理的四种类型之一,只不过 DrawModifier 链表在数组的第 0 个位置,而 PointerInputModifier 在第 1 个位置:

	fun addBeforeLayoutModifier(layoutNodeWrapper: LayoutNodeWrapper, modifier: Modifier) {
        if (modifier is DrawModifier) {
            add(DrawEntity(layoutNodeWrapper, modifier), DrawEntityType.index)
        }
        if (modifier is PointerInputModifier) {
            add(PointerInputEntity(layoutNodeWrapper, modifier), PointerInputEntityType.index)
        }
        if (modifier is SemanticsModifier) {
            add(SemanticsEntity(layoutNodeWrapper, modifier), SemanticsEntityType.index)
        }
        if (modifier is ParentDataModifier) {
            add(SimpleEntity(layoutNodeWrapper, modifier), ParentDataEntityType.index)
        }
    }

因此,PointerInputModifier 都是对它右侧,距离它最近的那个 LayoutModifier 生效的。并且,如果有多个 PointerInputModifier 作用于同一个 LayoutModifier,那么会按照从左到右的顺序逐个执行 PointerInputModifier。

因为遍历是从右至左,但是将 PointerInputModifier 放入链表时采用的是头插法,这样左侧虽然后被遍历到,但是它会被插入到链表头,执行时从链表头开始执行,从 Modifier 链的角度看就是从左到右的顺序执行。这些结论在讲 DrawModifier 时已经详细说明过。

接下来看被存起来的 PointerInputModifier 链表是如何响应触摸事件的。

在 LayoutNode 的 hitTest() 中:

	internal fun hitTest(
        pointerPosition: Offset,
        hitTestResult: HitTestResult<PointerInputFilter>,
        isTouchEvent: Boolean = false,
        isInLayer: Boolean = true
    ) {
        val positionInWrapped = outerLayoutNodeWrapper.fromParentPosition(pointerPosition)
        outerLayoutNodeWrapper.hitTest(
            LayoutNodeWrapper.PointerInputSource,
            positionInWrapped,
            hitTestResult,
            isTouchEvent,
            isInLayer
        )
    }

再次看到 outerLayoutNodeWrapper,也就是最外层的 LayoutNodeWrapper:

	fun <T : LayoutNodeEntity<T, M>, C, M : Modifier> hitTest(
        // 因为有多种类型,目前是两种 EntityType:PointerInputEntityType 与 
        // SemanticsEntityType 都会有取链表头的需求,所以这里用了参数让调用者指定具体类型
        hitTestSource: HitTestSource<T, C, M>,
        pointerPosition: Offset,
        hitTestResult: HitTestResult<C>,
        isTouchEvent: Boolean,
        isInLayer: Boolean
    ) {
        // hitTestSource 参数传的 PointerInputSource,因此这里取的是 PointerInputModifier 的链表头
        val head = entities.head(hitTestSource.entityType())
        if (!withinLayerBounds(pointerPosition)) {
            ...
        } else if (head == null) {
            hitTestChild(hitTestSource, pointerPosition, hitTestResult, isTouchEvent, isInLayer)
        } else if (isPointerInBounds(pointerPosition)) {
            // A real hit
            // 重点
            head.hit(
                hitTestSource,
                pointerPosition,
                hitTestResult,
                isTouchEvent,
                isInLayer
            )
        } else {
            ...
        }
    }

调用 PointerInputEntity 链表头节点的 hit():

    private fun <T : LayoutNodeEntity<T, M>, C, M : Modifier> T?.hit(
        hitTestSource: HitTestSource<T, C, M>,
        pointerPosition: Offset,
        hitTestResult: HitTestResult<C>,
        isTouchEvent: Boolean,
        isInLayer: Boolean
    ) {
        // this 指代调用者/接收者,在当前流程中就是 PointerInputEntity 链表头节点
        if (this == null) {
            hitTestChild(hitTestSource, pointerPosition, hitTestResult, isTouchEvent, isInLayer)
        } else {
            hitTestResult.hit(hitTestSource.contentFrom(this), isInLayer) {
                next.hit(hitTestSource, pointerPosition, hitTestResult, isTouchEvent, isInLayer)
            }
        }
    }

看头节点不为空的情况,调用参数 hitTestResult 的 hit(),它的第一个参数 hitTestSource.contentFrom(this),点进 contentFrom() 发现是一个接口函数,通过 ctrl + alt + B 找不到实现它的子类,这是因为当前 Android Studio 无法通过这种方式找到接口的匿名实现类,所以就在接口 HitTestSource 的名字上点 alt + F7 找到它的匿名对象,有两个 PointerInputSource 和 SemanticsSource,通过泛型参数可以确定 PointerInputSource 是我们要找的实现对象,看它的 contentFrom():

val PointerInputSource =
    object : HitTestSource<PointerInputEntity, PointerInputFilter, PointerInputModifier> {
        override fun entityType() = EntityList.PointerInputEntityType

        @Suppress("ModifierFactoryReturnType", "ModifierFactoryExtensionFunction")
        override fun contentFrom(entity: PointerInputEntity) = entity.modifier.pointerInputFilter
    }
}

entity 是 PointerInputEntity 链表的头节点,modifier 就是 PointerInputEntity 所在的 PointerInputModifier,最后的 pointerInputFilter 点进去看是接口属性:

interface PointerInputModifier : Modifier.Element {
    val pointerInputFilter: PointerInputFilter
}

接口有两个实现类,我们要的是处理 Compose 内部逻辑的 SuspendingPointerInputFilter:

internal class SuspendingPointerInputFilter(
    override val viewConfiguration: ViewConfiguration,
    density: Density = Density(1f)
) : PointerInputFilter(),
    PointerInputModifier,
    PointerInputScope,
    Density by density {

    override val pointerInputFilter: PointerInputFilter
        get() = this
}

这个属性就是返回自己,因此 hitTestSource.contentFrom(this) 就是拿到了头节点的 SuspendingPointerInputFilter。

然后再进入 HitTestResult 的 hit():

	fun hit(node: T, isInLayer: Boolean, childHitTest: () -> Unit) {
        hitInMinimumTouchTarget(node, -1f, isInLayer, childHitTest)
    }

	fun hitInMinimumTouchTarget(
        node: T,
        distanceFromEdge: Float,
        isInLayer: Boolean,
        childHitTest: () -> Unit
    ) {
        val startDepth = hitDepth
        hitDepth++
        ensureContainerSize()
        values[hitDepth] = node
        distanceFromEdgeAndInLayer[hitDepth] =
            DistanceAndInLayer(distanceFromEdge, isInLayer).packedValue
        resizeToHitDepth()
        childHitTest()
        hitDepth = startDepth
    }

node 就是 PointerInputEntity 链表头节点,hitDepth 累加说明 node 是按照顺序被存入 values 数组中的。放入数组后,执行参数的 childHitTest 函数,向上倒,找到给它传值的位置是在调用 PointerInputEntity 链表头节点的 hit() 的 else 情况:

    private fun <T : LayoutNodeEntity<T, M>, C, M : Modifier> T?.hit(
        hitTestSource: HitTestSource<T, C, M>,
        pointerPosition: Offset,
        hitTestResult: HitTestResult<C>,
        isTouchEvent: Boolean,
        isInLayer: Boolean
    ) {
        // this 指代调用者/接收者,在当前流程中就是 PointerInputEntity 链表头节点
        if (this == null) {
            hitTestChild(hitTestSource, pointerPosition, hitTestResult, isTouchEvent, isInLayer)
        } else {
            hitTestResult.hit(hitTestSource.contentFrom(this), isInLayer) {
                next.hit(hitTestSource, pointerPosition, hitTestResult, isTouchEvent, isInLayer)
            }
        }
    }

调用 next.hit(),next 是链表的下一个节点,调用 hit() 就形成了递归,不断地做下面两件事:

// 将 PointerInputEntity 链表的节点存入 values 数组
values[hitDepth] = node
// 调用下一个节点的 hit()
childHitTest()

这样一来,我们就能明确,PointerInputModifier 是按照从左至右的顺序被存储的,自然执行时也是按照从左至右的先后顺序执行的。至于执行的源码分析,暂时先不做,看看后续安排。

2、ParentDataModifier

ParentDataModifier 与 DrawModifier、PointerInputModifier 的底层存储方式相同,它是测量与布局过程中起到辅助作用的 Modifier。接下来我们会从它的作用、使用与原理三个方面来讲解这个 Modifier。

2.1 作用

我们会通过讲解 ParentDataModifier 接口的两个实现类,来说明 ParentDataModifier 的作用。

weight()

我们先来看一段代码:

@Composable
fun ParentDataModifierSample() {
    Row {
        Box(Modifier.size(40.dp).background(Color.Blue).weight(1f))
        Box(Modifier.size(40.dp).background(Color.Red))
        Box(Modifier.size(40.dp).background(Color.Green))
    }
}

当没有给蓝色 Box 添加 weight() 之前,红绿蓝三色 Box 是相同大小,都是 40dp。在给蓝色 Box 添加 weight() 之后,它会占满这一行剩余的空间:

在这里插入图片描述

说明 Compose 中的 weight() 与原生 View 体系中的 layout_weight 属性的作用是一样的。那么现在来思考,weight() 内部是使用哪一个 Modifier 实现的?看起来像是 LayoutModifier,但点进 weight() 的源码,你会先看到它是 RowScope 接口内定义的抽象函数:

@LayoutScopeMarker
@Immutable
interface RowScope {
    /**
     * 根据元素的[weight]相对于[Row]中其他带权重的兄弟元素的比例调整元素的宽度。父元素将在测量
     * 无权重子元素后,将水平剩余空间分配给子元素,比例由该权重决定。
     * 当[fill]为 true 时,元素将被强制占用分配给它的整个宽度。否则,允许元素更小 - 这将导致[Row]更小,
     * 因为未使用的分配宽度将不会重新分配给其他兄弟元素。
     * @param weight 给予该元素的比例宽度,相对于所有带权重兄弟元素的总和。必须为正数。
     * @param fill 当为 true 时,元素将占用分配的整个宽度。
     */
    @Stable
    fun Modifier.weight(
        /*@FloatRange(from = 0.0, fromInclusive = false)*/
        weight: Float,
        fill: Boolean = true
    ): Modifier
}

找到 RowScope 唯一的实现类 RowScopeInstance,发现 weight() 内部使用的是 LayoutWeightImpl:

internal object RowScopeInstance : RowScope {
    @Stable
    override fun Modifier.weight(weight: Float, fill: Boolean): Modifier {
        require(weight > 0.0) { "invalid weight $weight; must be greater than zero" }
        return this.then(
            LayoutWeightImpl(
                weight = weight,
                fill = fill,
                inspectorInfo = debugInspectorInfo {
                    name = "weight"
                    value = weight
                    properties["weight"] = weight
                    properties["fill"] = fill
                }
            )
        )
    }
}

而 LayoutWeightImpl 实现的是 ParentDataModifier 而不是我们预计的 LayoutModifier:

internal class LayoutWeightImpl(
    val weight: Float,
    val fill: Boolean,
    inspectorInfo: InspectorInfo.() -> Unit
) : ParentDataModifier, InspectorValueInfo(inspectorInfo)

并且 ParentDataModifier 不是 LayoutModifier 的子接口,而是一个直接继承自 Modifier.Element 的独立接口:

/**
* 一个修饰符,向父布局提供数据。这可以在测量和定位期间通过 IntrinsicMeasurable.parentData
* 从布局内部读取。Parent data 通常用于告知父级子布局应如何测量和定位。
*/
@JvmDefaultWithCompatibility
interface ParentDataModifier : Modifier.Element {
    /**
     * 通过 Modifier 链提供的 [parentData] 向外提供 parentData
     */
    fun Density.modifyParentData(parentData: Any?): Any?
}

通过 ParentDataModifier 接口上的注释,你应该了解了 ParentDataModifier 的用途了,就是向父布局提供子组件的数据,以便让父布局知道子组件应该如何测量和布局。比如我们举例的 weight(),虽然是设置在 Box 上的,但这个数据是会被父布局的 Row 获取到,从而让 Row 知道应该如何对其内部的 Box 进行测量与布局。所以这个 ParentData 可以理解为给父布局使用的数据。

相比于 LayoutModifier 这种直接影响组件的测量与布局过程的 Modifier,比如例子中的 Box 使用 size(40.dp),那么 LayoutModifier 就会让该 Box 的首选尺寸是 40dp。而 ParentDataModifier 并不是直接影响其所设置的组件,而是把该组件的相关“诉求”同步给它的父组件,父组件会收集其内部所有子组件的“诉求”,根据父组件的规则来处理每一个子组件的“诉求”。因此 ParentDataModifier 是一种间接影响组件测量与布局的 Modifier。

那为什么不使用 LayoutModifier 来做这件事?因为做不了,或者说,很难做。就以我们举得 Row 中有三个 Box 的例子,假如每个 Box 都设置了各自的 weight(),那么 LayoutModifier 这种专注于某个单一组件的 Modifier,就势必要获取该组件的所有兄弟组件的 Modifier 内设置的 weight() 值,这样才能计算出总的 weight 值,再计算出自己的 weight 所占的比例,最后再根据总的宽度计算出自己的宽度值。如果这样做的话,LayoutModifier 就有两个明显越权的地方:

  1. 额外获取了兄弟组件的 weight 值
  2. 获取了父组件的宽度

这样做会使得 LayoutModifier 的功能不再单一的只针对它所修饰的组件了,并且每个 LayoutModifier 都要掌握所有兄弟组件的 weight 使得 LayoutModifier 的功能更加混乱,并且这样实现也很麻烦。而像 Compose 设计的那样,让父组件来获取 ParentData 信息并进行计算测量,不仅可以在职权内获取到自己的内部宽度属性,也可以方便地计算出每个子组件的 weight(不用每个 LayoutModifier 内都保存一份 weight 数据,只需保存一份数据即可算出所有子组件的 weight),这就是不用 LayoutModifier 而使用独立的 ParentDataModifier 的原因。

实际上,weight 属性交由父级组件计算其实是一种通用设计,原生 View 体系下的权重 layout_weight 实际上也是给父布局使用的。包括 layout_width 与 layout_height 这些 layout 开头的属性都是用于父布局测量和布局使用的。

layoutId()

再看第二个例子,Modifier.layoutId():

@Stable
fun Modifier.layoutId(layoutId: Any) = this.then(
    LayoutId(
        layoutId = layoutId,
        inspectorInfo = debugInspectorInfo {
            name = "layoutId"
            value = layoutId
        }
    )
)

使用它可以为组件指定一个标签(tag),便于父组件在测量和布局时针对该组件做一些特殊处理。比如自定义一个 CustomLayout,在内部通过 Layout() 进行测量过程时,可以通过 Measurable 获取到这个 layoutId,然后根据规格做出相应的处理:

@Composable
fun CustomLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
    Layout(content, modifier) { measurables, constraints ->
        // 对每个组件进行测量                       
        measurables.forEach { // it:Measurable
            // 获取 Measurable 的 layoutId 做出对应的测量
            when (it.layoutId) {
                "big" -> it.measure(constraints.xxx)
                "small" -> it.measure(constraints.yyy)
                else -> it.measure(constraints)
            }
        }
        // 布局的模拟代码                       
        layout(100, 100) {
            ...
        }
    }
}

这里为了让代码更直观一些,就对 layoutId 的值做了硬编码,实际项目中一定不会直接像 “big”、“small” 这样写的,一旦写错单词错误会很难排查。

使用时在 CustomLayout 的子组件的 modifier 上调用 layoutId():

CustomLayout(Modifier.size(40.dp)) {
    Text("Jetpack", Modifier.layoutId("big"))
    Text("Compose", Modifier.layoutId("small"))
}

这样父布局 CustomLayout 在测量时,就会根据 Text 指定的 layoutId 内容做出不同的测量动作。这个例子也能看出,layoutId 背后的 ParentDataModifier 对父布局的测量起到了辅助作用。

综上,我们通过 weight() 与 layoutId() 两个例子,说明了 ParentDataModifier 是一个辅助测量与布局过程的 Modifier。

2.2 用法

本节介绍如果想自定义一个 ParentDataModifier 应该怎么写。

基本用法

根据以往的经验,最直接的使用方式就是在 Modifier 链中,用 then() 连接一个 ParentDataModifier 的实现类的对象即可:

Row {
    Box(
        Modifier
            .size(40.dp)
            .background(Color.Red)
            .then(object : ParentDataModifier {
                override fun Density.modifyParentData(parentData: Any?): Any? {
                    TODO("Not yet implemented")
                }
            })
    )
}

但是这样的话,在实现 modifyParentData() 的具体内容时,你需要去查阅 Box 的父组件 Row 的内部都用到了哪些 parentData,这是一件很麻烦的事情。因此,这种根据“以往经验”推测出的使用方法是错误的。

那正确的使用方法是什么呢?参考现成的 weight() 即可:

internal object RowScopeInstance : RowScope {
    @Stable
    override fun Modifier.weight(weight: Float, fill: Boolean): Modifier {
        require(weight > 0.0) { "invalid weight $weight; must be greater than zero" }
        return this.then(
            LayoutWeightImpl(
                weight = weight,
                fill = fill,
                inspectorInfo = debugInspectorInfo {
                    name = "weight"
                    value = weight
                    properties["weight"] = weight
                    properties["fill"] = fill
                }
            )
        )
    }
}

weight() 是父组件 RowScope 提供给子组件使用的 Modifier,因此子组件在使用 ParentDataModifier 时,直接调用 weight() 即可,没必要写成 then() + 匿名实现对象的形式。因为父组件能供你使用的 ParentDataModifier 已经通过weight() 这样的便捷函数给你了,如果你想增加某种父组件没提供的 ParentDataModifier 功能,只通过写一个匿名子类用 then() 连接上是没用的,因为父组件内部没有对于该 ParentDataModifier 实现类的相应处理,因此相当于你白写了。

上面说的是,如何使用父组件提供的已经实现好的 ParentDataModifier 的功能。假如,你想自定义一个提供 ParentDataModifier 功能的父组件,应该做如下三件事:

  1. 组件内部要使用 Layout(),因为只有使用它才能通过它的第三个参数 MeasurePolicy 接口内 measure() 的参数获得 measurables: List<Measurable> 参数,进而遍历 measurables 进行测量
  2. 在 Layout() 内遍历 measurables 时,去拿到并使用每一个子组件提供的 ParentDataModifier 信息
  3. 在组件内写一个提供 ParentDataModifier 的函数,类似 weight() 与 layoutId() 那样,方便开发者使用

基于以上三点,可以写个例子:

@Composable
fun CustomLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
    Layout(content, modifier) { measurables, constraints ->
        measurables.forEach {
            // 这个强转的类型需要根据你的业务需求做相应的转换,这里只是举例转成 String
            val data = it.parentData as? String
            ... // 自定义布局的后续代码
        }
        layout(100, 100) {
            // 布局代码...
        }
    }
}

通过 forEach 遍历 measurables,Measurable 的 parentData 就是该组件获取到的 ParentDataModifier 数据,类型是 Any?,因此在实现具体的自定义 Composable 函数时,需要将它强转为你所需要的类型,比如上面的 String。转换时需要注意使用 as? 而不是 as,因为并不是所有的组件都会设置 ParentDataModifier,对于这样的组件,它的 parentData 就为空,如果使用 as 强转会引发 NPE。

以上就完成了前两点,最后一点可以直接实现一个 ParentDataModifier 填入 then() 中即可:

fun Modifier.stringData() = then(object : ParentDataModifier {
    override fun Density.modifyParentData(parentData: Any?): Any? {
        // 带上右侧 ParentDataModifier 的 parentData 数据
        return "Compose: $parentData"
    }
})

modifyParentData() 的参数 parentData 是 Modifier 链上位于当前 Modifier 右侧的 ParentDataModifier 提供的 parentData 数据,之所以出现在参数上,是为了不遗弃右侧 ParentDataModifier 的数据,融合在一起使用的。当然,它提供给你的初衷是希望你不要遗弃数据,但你也需要结合实际应用场景,比如对于 weight() 这样的修饰符,如果连用了两个:

Modifier.weight(1f).weight(2f)

这种情况你对它做融合就没有任何意义,直接用后处理的 1f 覆盖先处理的 2f 即可:

// weight() 内 LayoutWeightImpl 的实现
override fun Density.modifyParentData(parentData: Any?) =
    ((parentData as? RowColumnParentData) ?: RowColumnParentData()).also {
        it.weight = weight
        it.fill = fill
    }

由于我们还没有到自定义布局的部分,所以现在举的例子只是为了说明 ParentDataModifier 的使用方式,完整的使用示例,会在后续介绍自定义布局时展示。

多 ParentDataModifier 的处理

以上我们说的是子组件只使用了一种 ParentDataModifier 的情况,假如像下面这样,子组件使用了一个以上的 ParentDataModifier:

setContent {
    CustomLayout(Modifier.size(40.dp)) {
        Text("Jetpack",
             Modifier
                 .weightData(1f)
                 .bigData(true))
    }
}

fun Modifier.bigData(big: Boolean) = then(object : ParentDataModifier {
    override fun Density.modifyParentData(parentData: Any?): Any? {
        return big
    }
})

fun Modifier.weightData(weight: Float) = then(object : ParentDataModifier {
    override fun Density.modifyParentData(parentData: Any?): Any? {
        return weight
    }
})

那么 CustomLayout 内获取 parentData 数据的方式就需要重新考量:

@Composable
fun CustomLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
    Layout(content, modifier) { measurables, constraints ->
        measurables.forEach {
            val data = it.parentData as? Float
            ... // 自定义布局的后续代码
        }
        layout(100, 100) {
            // 布局代码...
        }
    }
}

it.parentData 只能拿到 Modifier 链靠左侧的那个 ParentDataModifier 提供的 parentData 数据,也就是上例中 weightData() 的 Float 数据,但是没法拿到右侧 bigData() 的 Boolean 数据。这时候需要创建一个综合数据类承载这两个维度的 ParentDataModifier 的数据:

class LayoutData(var weight: Float = 0f, var big: Boolean = false)

bigData() 与 weightData() 在实现 modifyParentData() 时,结果也要融合到 LayoutData 中:

fun Modifier.bigData(big: Boolean) = then(object : ParentDataModifier {
    override fun Density.modifyParentData(parentData: Any?): Any? {
        // 如果 parentData 说明 bigData() 在最右侧,就创建 LayoutData 填入 big 属性,
        // 否则就在右侧传入的现有的 LayoutData 中添加 big 属性
        return if (parentData == null) {
            LayoutData(big = big)
        } else {
            (parentData as LayoutData).apply { this.big = big }
        }
    }
})

fun Modifier.weightData(weight: Float) = then(object : ParentDataModifier {
    override fun Density.modifyParentData(parentData: Any?): Any? {
        // 如果 parentData 为空就创建 LayoutData,否则就沿用参数传入的 LayoutData,添加 weight 属性
        return ((parentData as? LayoutData) ?: LayoutData()).also { it.weight = weight }
    }
})

CustomLayout 中获取到的 parentData 就可以转成 LayoutData,再分开对 big 与 weight 属性进行处理:

@Composable
fun CustomLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
    Layout(content, modifier) { measurables, constraints ->
        measurables.forEach {
            val data = it.parentData as? LayoutData
            val big = data.big
            val weight = data.weight
            ... // 自定义布局的后续代码
        }
        layout(100, 100) {
            // 布局代码...
        }
    }
}

解决 API 污染问题

上一小节创建出的 bigData() 与 weightData() 是给 CustomLayout 这个组件使用的,这两个函数只有在 CustomLayout() 的内部被调用才有意义,因此它不应该“随处可用”,只能在 CustomLayout 的环境中使用。

但按照当前代码,在 CustomLayout 之外,设置 Modifier 时会出现 bigData() 与 weightData() 的代码提示,并且可以在外部使用:

在这里插入图片描述

这就产生了 API 污染的问题。解决问题的方案可以参考 Row 中的 weight(),该函数是做了抗污染的。

首先,weight() 是被定义在 RowScope 接口中的:

@LayoutScopeMarker
@Immutable
interface RowScope {
    @Stable
    fun Modifier.weight(
        /*@FloatRange(from = 0.0, fromInclusive = false)*/
        weight: Float,
        fill: Boolean = true
    ): Modifier
}

这样一来,就只有 RowScope 的实现类对象,或者在 RowScope 的环境下才能调用 weight()。而 Row 这个 Composable 函数的最后一个参数 content 指定了函数的接收者类型为 RowScope,因此可以在 Row 的尾随 lambda 表达式中使用 weight():

@Composable
inline fun Row(
    modifier: Modifier = Modifier,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    verticalAlignment: Alignment.Vertical = Alignment.Top,
    content: @Composable RowScope.() -> Unit
)

这样我们效仿上面的做法应用到 CustomLayout 上即可。

首先创建一个 CustomLayoutScope 接口:

@LayoutScopeMarker
@Immutable
interface CustomLayoutScope {
    fun Modifier.bigData(big: Boolean): Modifier
    fun Modifier.weightData(weight: Float): Modifier
}

@LayoutScopeMarker 注解通常有两个作用:

  1. 限制 API 的可见性,被标记的接口或类的扩展函数与成员函数只在特定作用域内可见并调用
  2. DSL 作用域隔离,被标记的函数只能在特定代码块内访问

我们这里用到的是第一种,只能在直接的特定作用域内使用,在作用域之外不可见,在作用域的间接(嵌套的)内部不可调用。

@Immutable 注解可以减少重组过程中的不必要重组,一般用在接口上面。

然后实现 CustomLayoutScope 接口,把两个接口函数的具体实现拿进来:

// 使用 internal 限制 CustomLayoutScopeInstance 只在当前模块内获取
internal object CustomLayoutScopeInstance : CustomLayoutScope {
    override fun Modifier.bigData(big: Boolean) = then(object : ParentDataModifier {
        override fun Density.modifyParentData(parentData: Any?): Any? {
            // 如果 parentData 说明 bigData() 在最右侧,就创建 LayoutData 填入 big 属性,
            // 否则就在右侧传入的现有的 LayoutData 中添加 big 属性
            return if (parentData == null) {
                LayoutData(big = big)
            } else {
                (parentData as LayoutData).apply { this.big = big }
            }
        }
    })

    override fun Modifier.weightData(weight: Float) = then(object : ParentDataModifier {
        override fun Density.modifyParentData(parentData: Any?): Any? {
            // 如果 parentData 为空就创建 LayoutData,否则就沿用参数传入的 LayoutData,添加 weight 属性
            return ((parentData as? LayoutData) ?: LayoutData()).also { it.weight = weight }
        }
    })
}

最后在 CustomLayout() 中给它的 content 参数加上 CustomLayoutScope 类型的接收者:

@Composable
fun CustomLayout(
    modifier: Modifier = Modifier,
    content: @Composable CustomLayoutScope.() -> Unit
) {
    // Layout() 的第一个参数要的是 () -> Unit,因此要修改为下面的形式
    Layout({ CustomLayoutScopeInstance.content() }, modifier) { measurables, constraints ->
        measurables.forEach {
            val data = it.parentData as? LayoutData
            ...
        }
        layout(100, 100) {
            // 布局代码...
        }
    }
}

这样一来,就只能在 CustomLayout 的 content 的直接内部使用 CustomLayoutScope 内的函数了:

setContent {
    // Cannot access 'CustomLayoutScopeInstance': it is internal in 
    // 'com.jetpack.compose.scope'
    Modifier.weightData(1f)
    CustomLayout {
        // 这个位置可用
        Text("Jetpack", Modifier.weightData(1f))
        Box(
            Modifier
            	.size(20.dp)
            	.background(Color.Red)) {
            // 'fun Modifier.weightData(weight: Float): Modifier' can't be called in this 
            // context by implicit receiver. Use the explicit one if necessary
            Text("Compose", Modifier.weightData(2f))
        }
    }
}

上面演示了三处使用 weightData() 的代码,只有第二处是可以使用的:

  1. 示例代码在 CustomLayoutScopeInstance 所定义的模块之外,由于 CustomLayoutScopeInstance 是 internal 的,模块之外访问不到它,也就无法进一步使用它的 weightData() 了。实际上不能在 CustomLayoutScope 之外使用其内部的接口函数,是通过限定 CustomLayoutScope 的实现类的可见性(比如 RowScope 与 CustomLayoutScope 都是限定其在模块内可见)导致使用者拿不到 CustomLayoutScope 的实现类对象实现编译报错的
  2. 可以正常使用,因为 weightData() 是 CustomLayoutScope 的接口函数,且 CustomLayout 的尾随 lambda 函数提供了 CustomLayoutScope 这个接收者,因此在尾随 lambda 的直接内部可以使用 weightData()
  3. 在 CustomLayout 内又嵌套了一个 Box,在 Box 的直接内部,CustomLayout 的间接内部不能使用 weightData(),CustomLayoutScope 接口上用 @LayoutScopeMarker 做了限制,使得只能在 CustomLayout 的直接内部使用,不能穿透到 Box 这样的嵌套的内部。如果没加,就可以穿透到嵌套的内部

当然,你也可以直接用 object 来做这个 Scope 省去实现接口的麻烦:

@LayoutScopeMarker
object CustomLayoutScope {
    fun Modifier.bigData(big: Boolean) = then(object : ParentDataModifier {
        override fun Density.modifyParentData(parentData: Any?): Any? {
            // 如果 parentData 说明 bigData() 在最右侧,就创建 LayoutData 填入 big 属性,
            // 否则就在右侧传入的现有的 LayoutData 中添加 big 属性
            return if (parentData == null) {
                LayoutData(big = big)
            } else {
                (parentData as LayoutData).apply { this.big = big }
            }
        }
    })

    fun Modifier.weightData(weight: Float) = then(object : ParentDataModifier {
        override fun Density.modifyParentData(parentData: Any?): Any? {
            // 如果 parentData 为空就创建 LayoutData,否则就沿用参数传入的 LayoutData,添加 weight 属性
            return ((parentData as? LayoutData) ?: LayoutData()).also { it.weight = weight }
        }
    })
}

2.3 实现原理

所有的 Modifier 第一步都是在 LayoutNode 处理的,并且 ParentDataModifier 与 DrawModifier、PointerInputModifier 一样,都是先被添加到它右侧距离它最近的 LayoutNodeWrapper 的 entities 这个数组的对应类型的链表中:

@kotlin.jvm.JvmInline
internal value class EntityList(
    val entities: Array<LayoutNodeEntity<*, *>?> = arrayOfNulls(TypeCount)
) {

    fun addBeforeLayoutModifier(layoutNodeWrapper: LayoutNodeWrapper, modifier: Modifier) {
        if (modifier is DrawModifier) {
            add(DrawEntity(layoutNodeWrapper, modifier), DrawEntityType.index)
        }
        if (modifier is PointerInputModifier) {
            add(PointerInputEntity(layoutNodeWrapper, modifier), PointerInputEntityType.index)
        }
        if (modifier is SemanticsModifier) {
            add(SemanticsEntity(layoutNodeWrapper, modifier), SemanticsEntityType.index)
        }
        // 注意 ParentDataModifier 添加的链表类型是 SimpleEntity
        if (modifier is ParentDataModifier) {
            add(SimpleEntity(layoutNodeWrapper, modifier), ParentDataEntityType.index)
        }
    }
}

与其他几个类型稍有不同的是,ParentDataModifier 对应的链表类型为 SimpleEntity,不像其他三种都是 XxxModifier 对应 XxxEntitity。

然后我们来看一下,ParentDataModifier 提供的数据 parentData 是被如何使用的。前面我们讲 CustomLayout 这个示例时,是通过 Layout() 参数的 MeasurePolicy 接口的唯一抽象函数 measure() 的参数拿到 measurables: List<Measurable>,然后遍历 measurables 通过 Measurable 拿到 parentData 这个数据。

实际上 parentData 是 IntrinsicMeasurable 接口中的属性:

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

LayoutNodeWrapper 实现的是 IntrinsicMeasurable 的子接口 Measurable,因此 LayoutNodeWrapper 的内部有 parentData 的实现:

    override val parentData: Any?
        get() = entities.head(EntityList.ParentDataEntityType).parentData

    private val SimpleEntity<ParentDataModifier>?.parentData: Any?
        get() = if (this == null) {
            wrapped?.parentData
        } else {
            with(modifier) {
                /**
                 * ParentData provided through the parentData node will override the data provided
                 * through a modifier.
                 */
                measureScope.modifyParentData(next.parentData)
            }
        }

对接口内 parentData 属性的实现就是 SimpleEntity 的链表头中的 parentData,而链表头的 parentData 是 SimpleEntity 的扩展属性,它根据 SimpleEntity 是否为空,有两种处理方式:

  1. 当 SimpleEntity 为空时,返回 wrapped 的 parentData。前面已经说过很多次 LayoutNodeWrapper 的 wrapped 就是它内部包含的另一个 LayoutNodeWrapper,因此 wrapped?.parentData 就是返回其内部包含的 LayoutNodeWrapper 的 parentData。意思就是当前 LayoutNodeWrapper 内的 SimpleEntity 链表已经遍历完尾节点或者链表干脆就是空的,该去遍历下一个 LayoutNodeWrapper 的 SimpleEntity 链表了
  2. 当 SimpleEntity 不为空时,调用该 SimpleEntity 内封装的 modifier 的 modifyParentData() 获取 parentData,该函数参数上的 next.parentData 是 SimpleEntity 链表的下一个节点的 parentData 属性,这里就形成了属性的递归调用,即当前节点在调用 modifyParentData() 前,需要先获取下一个节点的 parentData,而获取下一个节点的 parentData 时,假如下一个节点不为空则继续调用下下一个节点的 get()(如为空则去获取下一个 LayoutNodeWrapper 内的 parentData)

举一个例子帮助理解,假如有如下形式的 Modifier 链:

Modifier.then(ParentDataModifier1).then(ParentDataModifier2).then(LayoutModifier1)
        .then(ParentDataModifier3).then(ParentDataModifier4).then(LayoutModifier2)
        .then(ParentDataModifier5).then(ParentDataModifier6)

那么最外层的 LayoutNodeWrapper 的结构应该是:

ModifiedLayoutNode(
	entities = [null,null,null,ParentDataModifier1 -> ParentDataModifier2,null,null,null],
	modifier = LayoutModifier1,
	wrapped = ModifiedLayoutNode(
		entities = [null,null,null,ParentDataModifier3 -> ParentDataModifier4,null,null,null],
		modifier = LayoutModifier2,
		wrapped = InnerPlaceable(
			entities = [null,null,null,ParentDataModifier5 -> ParentDataModifier6,null,null,null]
		)
	)
)

当访问外层 LayoutNodeWrapper 的 parentData 属性时,它会取 entities 数组中索引为 3 的链表 SimpleEntity<ParentDataModifier> 的表头,再访问表头的 parentData 属性。

访问表头 parentData 属性的过程是一个递归过程,递归访问下一个 SimpleEntity<ParentDataModifier> 节点的 parentData 属性,如果到了链表尾部就或链表本身就是空的,就访问当前 ModifiedLayoutNode 的 wrapped 包装的下一个 ModifiedLayoutNode,这样一直递归下去直到 InnerPlaceable 的 SimpleEntity<ParentDataModifier> 链表的尾部。

递归返回时带着下一个节点的 parentData 作为参数调用 modifyParentData(),也就是 ParentDataModifier 的接口函数,在这个函数中对下一个节点获取到的 parentData 作为参数进行数据融合,返回的结果将作为它上一个节点的 modifyParentData() 的参数,直到最外层 ModifiedLayoutNode 的 parentData 得出一个计算结果。

2.4 总结

对于 ParentDataModifier 可以总结以下几点:

  • 作用:ParentDataModifier 用于给子组件附加一些属性,让父组件可以利用
  • 用法:只有在通过 Layout() 写自定义函数时才会用到 ParentDataModifier,在测量与布局的算法中通过 Measurable 的 parentData 属性拿到这个数据后根据具体需求使用即可,最后要提供一个 Modifier 函数实现 ParentDataModifier,在 modifyParentData() 中根据需求提供附加数据
  • 在同一个组件上交换调用 ParentDataModifier 函数的位置,UI 显示效果不会发生变化,因为使用 ParentDataModifier 提供的数据的是该组件的父组件

3、SemanticsModifier

SemanticsModifier 用于提供 SemanticsTree —— 语义树。本节将解释什么是语义树,SemanticsModifier 的使用与原理。

3.1 语义树

在 Jetpack Compose 中,语义树(Semantics Tree) 是组合树(Composition Tree)的“增强版”,用于描述 UI 元素的含义交互属性

语义树是对组合树进行修剪(对节点进行删除或合并)简化为实际有意义的节点,实际有意义是指可以供用户查看和操作的独立组件。比如一个 LinearLayout 在用户的角度看,它不像一个图片那样可以查看或一个按钮那样可以点击,它只是负责布局,对用户而言不可见也没有意义,不被用户关注,这也可被称为没有语义。同理,Compose 中的 Column 如果本身没有设置监听器,那么它所创建出的 LayoutNode 也就是无语义的,这样的节点会被合并或删除,最终形成一个有语义的树,也就是 Semantics Tree —— 语义树。

语义树的核心作用是为无障碍服务(如 TalkBack)、自动化测试框架(如 Espresso)和 UI 分析工具提供结构化信息。

在进行传统的 UI 开发时,一些组件,比如 ImageView、TextView 等,都会有一个 contentDescription 属性,这个属性就是在开启 TalkBack 功能后,视障用户点击到某个组件后,会选中该组件(可以被选中的组件就是语义树中的节点)并以语音方式读出 contentDescription 设置的内容,以帮助视障用户方便地使用 Android 设备。

3.2 基本使用

setContent {
    Column {
        Text("Jetpack Compose")
        Box(
            Modifier
                .width(100.dp)
                .height(60.dp)
                .background(Color.Magenta)
        )
    }
}

上面是一个 Text 和一个 Box,开启 TalkBack 功能后,Text 组件是可被选中的,选中时语音会读出 Text 的文字内容,但 Box 不可被选中:

请添加图片描述

现在用 semantics() 为 Box 增加 contentDescription 属性:

setContent {
    Column {
        Text("Jetpack Compose")
        Box(
            Modifier
                .width(100.dp)
                .height(60.dp)
                .background(Color.Magenta)
                .semantics {
                    contentDescription = "品红色方块"
                }
        )
    }
}

这样 Box 也可以在点击时被选中了,并且 TalkBack 会读出 contentDescription 设置的内容:

请添加图片描述

semantics() 有两个参数:

/**
* 向布局节点添加语义键值对(key/value pairs),用于测试、无障碍服务等场景。
* 在提供的 lambda 接收者作用域(SemanticsPropertyReceiver)中,可以通过 key = value 的形式
* 为任何 SemanticsPropertyKey 赋值。此外,也支持链式调用多个 semantics 修饰符的写法。
* 最终会生成两棵语义树:
* 未合并的树(Unmerged Tree): 根节点为 SemanticsOwner.unmergedRootSemanticsNode,每个带有 
* SemanticsModifier 的布局节点会生成一个对应的 SemanticsNode,该 SemanticsNode 包含该节点上
* 所有 SemanticsModifier 设置的属性
* 合并的树(Merged Tree):根节点为 SemanticsOwner.rootSemanticsNode,节点数量更少
*(基于 mergeDescendants 和 clearAndSetSemantics 进行简化)。大多数场景(尤其是无障
* 碍服务或无障碍测试)应使用合并后的语义树。
*
* 参数说明:
* mergeDescendants(合并子节点语义):是否将当前组件及其子组件的语义信息合并为一个逻辑实体。
* 通常用于可被屏幕阅读器聚焦的组件(如按钮、表单字段)。在合并树中:所有子节点(除非子节点自身也标记了
* mergeDescendants)会从树中移除。子节点的属性会通过特定合并算法合并到父节点(例如,文本属性用逗号拼接)。
* 在未合并树中:仅标记 SemanticsConfiguration.isMergingSemanticsOfDescendants。
* properties(语义属性):通过 SemanticsPropertyReceiver 作用域添加语义属性,支持访问常用属性及
* 其值(如 contentDescription、role 等)。
*/
fun Modifier.semantics(
    mergeDescendants: Boolean = false,
    properties: (SemanticsPropertyReceiver.() -> Unit)
): Modifier

我们需要了解第一个参数 mergeDescendants 的含义与用法。从名字上能看出,它表示是否合并后代,也就是它的子组件。我们来看个例子:

Button(onClick = { /*TODO*/ }) {
    Text("测试文字")
}

上面这个按钮,在开启 TalkBack 点击它后,会语音播放“测试文字,按钮”,它会把 Text 内的文字作为按钮内容被读出,并且你想点击到 Text 上也不行,TalkBack 的选中框只能选中 Button:

请添加图片描述

发生这样情况的原因是,对于 Button 这种可点击的组件,Compose 在底层实现时,会自动给该组件调用 Modifier.semantics() 并给第一个参数传 true 使得其子组件被合并到该组件中。那假如我先在就想选中 Button 中的 Text,只读取 Text 的内容,那么可以给 Text 设置 Modifier.semantics(true):

Button(onClick = { /*TODO*/ }) {
    Text("测试文字", Modifier.semantics(true) { })
}

这样 TalkBack 就可以选中 Text 并读出它的内容了:

请添加图片描述

与 semantics() 类似的还有一个 clearAndSetSemantics(),它会清除当前组件的所有后代节点的语义,并设置新的语义。比如说:

setContent {
    Box(
        Modifier
            .width(100.dp)
            .background(Color.Magenta)
            .semantics(true) {
                contentDescription = "Jetpack"
            }
    ) {
        Text("Compose")
    }
}

当前,点击按钮,TalkBack 会读出 “Jetpack Compose”,而不设置 semantics() 的第一个参数为 true 时,即使用默认的 false 时,Box 与 Text 可以分开点击,TalkBack 会读出它们各自的内容,这是我们讲 semantics() 时已经说过的。

现在,将 semantics() 替换成 clearAndSetSemantics():

setContent {
    Box(
        Modifier
            .width(100.dp)
            .background(Color.Magenta)
            .clearAndSetSemantics {
                contentDescription = "Jetpack"
            }
    ) {
        Text("Compose")
    }
}

那么你就只能选中按钮,无法通过点击选中 Text,并且,选中时只读出 Box 的 contentDescription 属性的内容 “Jetpack”,这意味着 Box 内的后代节点都因为 clearAndSetSemantics() 而从语义树中被移除了,现在语义树中只有 Box 节点本身,语义内容为它设置的 contentDescription 的内容,相当于后代组件的语义被父组件设置的语义吞掉了。

3.3 原理简析

我们前面在讲 PointerInputModifier 的时候,其实有提到过 SemanticsModifier,说它们两个用的是一套结构,如果 PointerInputModifier 的原理你看明白了,那就意味着 SemanticsModifier 的原理也基本拿下了。

还是从两个角度来分析:SemanticsModifier 的底层是如何存储的,以及取出 SemanticsModifier 后如何工作的。

底层的存储又要再看一遍 EntityList 的 addBeforeLayoutModifier():

	fun addBeforeLayoutModifier(layoutNodeWrapper: LayoutNodeWrapper, modifier: Modifier) {
        if (modifier is DrawModifier) {
            add(DrawEntity(layoutNodeWrapper, modifier), DrawEntityType.index)
        }
        if (modifier is PointerInputModifier) {
            add(PointerInputEntity(layoutNodeWrapper, modifier), PointerInputEntityType.index)
        }
        if (modifier is SemanticsModifier) {
            add(SemanticsEntity(layoutNodeWrapper, modifier), SemanticsEntityType.index)
        }
        if (modifier is ParentDataModifier) {
            add(SimpleEntity(layoutNodeWrapper, modifier), ParentDataEntityType.index)
        }
    }

就是将 SemanticsModifier 封装进 SemanticsEntity,然后把 SemanticsEntity 存入到 EntityList 的成员属性 entities 数组的 SemanticsEntity 链表的头部。

使用是在 LayoutNodeWrapper 的 SemanticsSource 中:

		val SemanticsSource =
            object : HitTestSource<SemanticsEntity, SemanticsEntity, SemanticsModifier> {
                override fun entityType() = EntityList.SemanticsEntityType

                override fun contentFrom(entity: SemanticsEntity) = entity

                override fun interceptOutOfBoundsChildEvents(entity: SemanticsEntity) = false

                override fun shouldHitTestChildren(parentLayoutNode: LayoutNode) =
                    parentLayoutNode.outerSemantics?.collapsedSemanticsConfiguration()
                        ?.isClearingSemantics != true

                override fun childHitTest(
                    layoutNode: LayoutNode,
                    pointerPosition: Offset,
                    hitTestResult: HitTestResult<SemanticsEntity>,
                    isTouchEvent: Boolean,
                    isInLayer: Boolean
                ) = layoutNode.hitTestSemantics(
                    pointerPosition,
                    hitTestResult,
                    isTouchEvent,
                    isInLayer
                )
            }

SemanticsSource 与 PointerInputSource 都用了 HitTestSource,也就是 HitTest 进行触摸点测试,通过触摸的点判断触摸到的是哪一个组件。

SemanticsSource 在 LayoutNode 的 hitTestSemantics() 被使用:

	@Suppress("UNUSED_PARAMETER")
    internal fun hitTestSemantics(
        pointerPosition: Offset,
        hitSemanticsEntities: HitTestResult<SemanticsEntity>,
        isTouchEvent: Boolean = true,
        isInLayer: Boolean = true
    ) {
        val positionInWrapped = outerLayoutNodeWrapper.fromParentPosition(pointerPosition)
        // hitTest() 用于判断摸到了哪一个组件
        outerLayoutNodeWrapper.hitTest(
            LayoutNodeWrapper.SemanticsSource,
            positionInWrapped,
            hitSemanticsEntities,
            isTouchEvent = true,
            isInLayer = isInLayer
        )
    }

再向上,AndroidComposeViewAccessibilityDelegateCompat 的 hitTestSemanticsAt() 用到了 hitTestSemantics():

	@OptIn(ExperimentalComposeUiApi::class)
    @VisibleForTesting
    internal fun hitTestSemanticsAt(x: Float, y: Float): Int {
        view.measureAndLayout()

        val hitSemanticsEntities = HitTestResult<SemanticsEntity>()
        view.root.hitTestSemantics(
            pointerPosition = Offset(x, y),
            hitSemanticsEntities = hitSemanticsEntities
        )

        val wrapper = hitSemanticsEntities.lastOrNull()?.layoutNode?.outerSemantics
        var virtualViewId = InvalidId
        if (wrapper != null) {

            // The node below is not added to the tree; it's a wrapper around outer semantics to
            // use the methods available to the SemanticsNode
            val semanticsNode = SemanticsNode(wrapper, false)
            val wrapperToCheckAlpha = semanticsNode.findWrapperToGetBounds()

            // Do not 'find' invisible nodes when exploring by touch. This will prevent us from
            // sending events for invisible nodes
            if (!semanticsNode.unmergedConfig.contains(SemanticsProperties.InvisibleToUser) &&
                !wrapperToCheckAlpha.isTransparent()
            ) {
                val androidView = view.androidViewsHandler.layoutNodeToHolder[wrapper.layoutNode]
                if (androidView == null) {
                    virtualViewId = semanticsNodeIdToAccessibilityVirtualNodeId(wrapper.modifier.id)
                }
            }
        }
        return virtualViewId
    }

hitTestSemanticsAt() 在同一个类的 dispatchHoverEvent() 中被调用,而后者在原生的 AndroidComposeView 中被调用:

	public override fun dispatchHoverEvent(event: MotionEvent): Boolean {
        if (hoverExitReceived) {
            // Go ahead and send it now
            removeCallbacks(sendHoverExitEvent)
            sendHoverExitEvent.run()
        }
        if (isBadMotionEvent(event) || !isAttachedToWindow) {
            return false // Bad MotionEvent. Don't handle it.
        }
        if (event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN) &&
            event.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER
        ) {
            // Accessibility touch exploration
            return accessibilityDelegate.dispatchHoverEvent(event)
        }
        ...
    }

dispatchHoverEvent() 主要用于处理悬浮事件的,无障碍的触摸也在这个地方进行处理。


http://www.kler.cn/a/586896.html

相关文章:

  • Java面试八股—Redis篇
  • C# WPF 串口通信
  • 【fNIRS可视化学习1】基于NIRS-SPM进行光极可视化并计算通道坐标
  • 【Git学习笔记】Git结构原理及其分支管理模型分析
  • Ubuntu下管理多个GCC版本
  • 【数据分享】2000—2024年我国省市县三级逐月归一化植被指数(NDVI)数据(Shp/Excel格式)
  • 深入解析java Socket通信中的粘包与拆包问题及解决方案(中)
  • python 实现 A* 算法
  • 某大厂自动化工程师面试题
  • C语言每日一练——day_8
  • Qwen2.5的注意力秘籍:解锁高效模型的钥匙,分组查询注意力机制
  • .NET 9 中 OpenAPI 替代 Swagger 文档生成
  • apt-get update命令与apt update命令的区别
  • Assembly语言的安全开发
  • AI日报 - 2025年3月16日
  • 深入理解C/C++堆数据结构:从原理到实战
  • netsh实现TCP端口转发
  • 【Mapbox】介绍及基本使用
  • prompt提示词
  • 算法模型全解析:优缺点、场景适配与选择逻辑