从语义到语义树
SemanticsModifierNode 是用来提供 semantics tree 语义树的。
语义树是什么?其中语义又是什么?
别着急,往下看 🔻
什么是语义(Semantics)?
在编译原理中,语义指的是程序代码的实际含义和执行效果。而在 Jetpack Compose 中,语义的含义是:UI组件对于用户来说的实际意义和功能,尤其是对于使用辅助技术(如屏幕阅读器)的用户而言。
简单来说,语义描述了:🔍 这个组件是什么(按钮?文本?图片?),📊 它的状态是怎么样的(可用?禁用?选中?),🖐️ 用户怎么去操作它(点击?滑动?长按?)。
比如一个静音按钮,它的语义可能是:这是一个静音按钮,按钮当前是可以点击的,用户可以双击屏幕来激活它,使手机变为静音模式。
什么是语义树(Semantics Tree)?
既然知道了什么是语义,那语义树不就是描述了很多个组件的树形结构吗?
对,你可以这么想。
在 Compose 的组合过程中,会形成一个 UI 节点树,它负责渲染界面。语义树就是删除或合并了UI节点树中那些对用户交互没有实际意义的节点,最后得到的节点树。
语义树只包含了"有意义"的节点。有意义的节点就是可以供用户查看、操作的组件 ,用户需要了解它状态的组件。比如:查看一张图片、查看一段文字、点击一个按钮、点击一个输入框、了解进度条的进度。
无意义的节点就是用于纯布局的组件 、装饰性的组件 、用户不可见、不可交互的组件 。比如,Row
、Column
组件是用来布局的;有背景色的 Box()
组件;用于占位,提供间距的 Spacer()
组件,对用户来说看不到、点击无效果。
关键区别在于:用户是否会关注这个组件本身。
比如这样的UI节点树:
scss
UI节点树:
App
├── Column
│ ├── Row
│ │ ├── Image (用户头像)
│ │ ├── Spacer (空白占位)
│ │ └── Text (用户名)
│ └── Button (登录)
│ └── Text (登录)
生成的语义树是:
scss
语义树:
App
├── Image (用户头像)
├── Text (用户名)
└── Button (登录)
其中对用户没有实际交互意义的组件在语义树中会被移除。
语义树的作用与重要性
语义树的作用是:1. 用于系统的无障碍功能 2. 开发时进行测试
系统无障碍
无障碍功能(Accessibility)是指让所有人,包括残障用户,都能够顺畅地使用应用,旨在消除用户使用应用的障碍。
其中有一个非常重要的无障碍功能就是 TalkBack(屏幕阅读器),它可以为视力障碍的用户提供语音提示。
来演示一下:
它会用绿色框选中当前组件,并且语音播报组件的语义信息:"已开启TalkBack屏幕阅读,开关,点按两次即可切换"。
再解释一下就是你点击任意一个组件,它并不会触发点击逻辑,而是用绿框标记,并且说出这个组件的语义,然后在屏幕任意位置双击才可以触发点击。
你也可以自己试一试,体验一下效果,可以让你更好地理解语义树。
开发测试
语义树还可以给开发测试带来方便,我们可以快速定位到被我们设置了语义信息的UI节点(相当于拿到了这个组件)。
可以检查组件的状态,比如它是可见还是不可见,可用还是不可用;可以模拟用户与组件交互,对登录按钮进行点击,对输入文本到输入框。
实现无障碍
无障碍的实现可以被分为两点,第一组件要提供可被系统选中的逻辑,因为系统要给被选中的组件加上绿色框;第二组件需要提供可被系统获取的可读信息,这样系统才能获取到信息,将信息读出来给用户听到。
在 Compose 中要提供这两个东西,使用的是 SemanticsModifierNode,对应的修饰符函数是 Modifier.semantics()
和 Modifier.clearAndSetSemantics()
。
Modifier.semantics()
那怎么使用呢?
先来看这个示例代码:
kotlin
@Composable
fun SemanticsDemo(modifier: Modifier = Modifier) {
Column {
Text("曾经沧海难为水") // Text 组件已内置无障碍支持
Box(Modifier
.background(Color.Green)
.width(120.dp)
.height(80.dp))
// 默认情况下,这个绿色方块没有语义信息
}
}

当我们开启屏幕阅读后,它会自动选中文本组件,并语音播报:"曾经沧海难为水",这是因为 Compose 在内部对Text 组件做了无障碍的支持。
绿色方块就不行了,屏幕阅读对它没有任何效果。
添加语义信息
而 semantics()
修饰符是可以给绿色方块也加上这种效果的,比如:
diff
@Composable
fun SemanticsDemo(modifier: Modifier = Modifier) {
Column {
Text("曾经沧海难为水")
Box(Modifier
.background(Color.Green)
.width(120.dp)
.height(80.dp)
+ .semantics {
+ contentDescription = "绿色方块" // 添加内容描述
+ }
)
}
}

这样当点击绿色方块时,系统能够框选它并读出它的信息------"绿色方块"。
在 semantics()
的大括号中不止可以设置 contentDescription 语义属性,比如还有 stateDescription (描述组件的状态)、progressBarRangeInfo (描述进度条信息)等,这些属性都是 SemanticsPropertyReceiver
接口的扩展属性,文件是 SemanticsProperties.kt。
不过常用的组件已经具备了无障碍能力,比如刚刚的 Text 组件,还有 Image 组件。
语义合并
再来看这段代码:
kotlin
Button(onClick = {}) {
Text("除却巫山不是云")
}
Button 组件、Text组件都具有无障碍的能力,能够被读,那么当我点击了这个按钮(范围内的任意位置),会发生什么?
语言播报:"除却巫山不是云,按钮,点按两次即可激活"。
为什么是这样?
因为按钮和文本在语义树中被合并了,这是语义树的重要特性------语义合并。在用户的角度上,这就是一个有着文本的按钮,肯定不会读成:这是一个按钮,按钮里有一个文本,文本是...
控制语义合并
知道了这个,你也可以做这种合并需求,大多数情况下我们是不用考虑的,因为许多可触摸的组件,都有合并, Modifier.clickable()
内部就完成了合并,如果你真有合并的需求,只需要设置 semantics()
的 mergeDescendants
参数为true,就可以了,mergeDescendants 的意思是合并后代,这个参数默认是false。
这个 mergeDescendants
参数还有一个作用,防止自己被合并到外部组件中,换个角度来看,就是外部组件合并内部组件时,会跳过 mergeDescendants
参数设置为true的组件。
比如在上面的例子中,给 Text 组件设置这个参数为true:
kotlin
Button(onClick = {}) {
Text("除却巫山不是云", Modifier.semantics(mergeDescendants = true) {
})
}

这样 Text 组件和 Button 组件分开了,文本组件不会被合并到按钮中,并且只有文本被绿色框框起来了,语音也只读了"除却巫山不是云",并没有读"按钮,点按两次即可激活"。
这种合并的设置,往往添加在可交互的组件上面。
Modifier.clearAndSetSemantics()
它和 semantics()
的区别是:它不会和内部的组件合并,并把这些组件从语义树中清除。
简单来说:它会清除内部组件的所有语义信息,只保留自身设置的语义信息。
我们来对比一下,如果使用的是 semantics
,并且 mergeDescendants
参数为 true:
kotlin
Box(
Modifier.background(Color.Red).width(150.dp).height(90.dp).semantics(mergeDescendants = true) {
contentDescription = "一个红色块"
}
) {
Text("我很重要,你一定要读我啊")
}
TalkBack 会读出 "一个红色块,我很重要,你一定要读我啊"
换成使用 clearAndSetSemantics():
kotlin
Box(
Modifier.background(Color.Red).width(150.dp).height(90.dp).clearAndSetSemantics{
contentDescription = "一个红色块"
}
) {
Text("我很重要,你一定要读我啊")
}

只会语音播报:"一个红色块"。
这个函数的使用场景:组件不需要内部组件语义,比如将多个组件作为一个整体对外提供语义。
总结
- 语义(Semantics) 描述了UI组件对用户的实际意义和功能
- 语义树(Semantics Tree) 是删除或合并了无意义节点后的UI树
- 语义树的作用:支持无障碍功能、便于开发测试