一文读懂 Compose 支持 Accessibility 无障碍的原理

Compose-base-accessibility.png

前言

众所周知,Compose 作为一种 UI 工具包,向开发者提供了实现 UI 的基本功能。但其实它还默默提供了很多其他能力,其中之一便是今天需要讨论的:Android 特色的 Accessibility 功能。

采用 Compose 搭建的界面,完美地支持了 Accessibility 功能:它的 UI 变化能正确地发出无障碍事件 AccessibilityEvent 并响应来自无障碍服务的操作 AccessibilityAction

那 Compose 是如何做到完美兼容传统的 Accessibility 机制的,本文将按照无障碍事件、无障碍节点、无障碍操作等几个方向为你剖析 Compose 默默做了哪些事情。

目录:

  1. 为 Compose 适配 contentDescription
  2. Compose 收集 Accessibility 语义信息
  3. Compose 特殊的 Accessibility 代理
  4. Compose 中 AccessibilityEvent 的产生和发送
  5. Compose 中 AccessibilityNode 的生成和提供
  6. Compose 中 AccessibilityAction 的响应和执行

1. 为 Compose 后面适配 contentDescription

对采用 Compose 开发的 App 来说,几乎不需要做什么适配,就可以支持 Accessibility 功能。

但为了给使用障碍人士更好的体验,最好给使用到的 Compose 控件明确它们的 contentDescription 属性。这便于使用 AccessibilityService 的 App 拿到清晰的控件描述。

Image 控件为例,使用它的时候,通过 contentDescription 描述清楚它具体的作用。

 Image(
     ...
     contentDescription = "This is a image for artist",
     ...
 )

这便于比如 Talkback 之类的 App 可以利用该信息进行明确的提示:“This is a image for road”。不至于因为信息不够,只能对 user 进行“Image”的无用播报。

如何适配 Accessibility、适配得更好,详细的细节可以参考官方文档:使用 Jetpack Compose 改进应用的无障碍功能

当然,contentDescription 可不是 Accessibility 唯一关心的属性,还有很多控件所特有的属性,比如 click、text、progress 等等。

那这些属性信息是如何被通知到 Accessibility 系统的呢?

2. Compose 收集 Accessibility 语义信息

首先 Compose 专门设计了供 LayoutInspector、test 和 Accessibility 等场景读取和使用的语义系统 SemanticsConfiguration

在各 UI 控件进行初始化的时候,LayoutNode 会去收集各语义节点 SemanticsNode 提供的具体信息,综合到上述 SemanticsConfiguration中。

     internal val collapsedSemantics: SemanticsConfiguration?
         get() {
   
   
             ...
             var config = SemanticsConfiguration()
             requireOwner().snapshotObserver.observeSemanticsReads(this) {
   
   
                 nodes.tailToHead(Nodes.Semantics) {
   
   
                     ...
                     with(config) {
   
    with(it) {
   
    applySemantics() } }
                 }
             }
             _collapsedSemantics = config
             return config
         }

SemanticsNode 需要复写各自的 applySemantics() 方法,此后便被按照类型进行收集。比如负责提供核心语义的 CoreSemanticsModifierNode、提供点击相关语义的 ClickableSemanticsNode 等等。

事实上,SemanticsConfiguration 本质上是 Map,各类型语义在收集的时候,会按照对应的 key 进行存储。

接下来,我们以 contentDescription 和 click 两种语义信息为例,阐述 Compose 是如何收集它们到 SemanticsConfiguration 中以供 Accessibility 系统调用的。

2-1. for contentDescription

先来看下 Image 控件的源码,跟一下设置的 contentDescription 会如何传递。

 @Composable
 fun Image(
     ...
     contentDescription: String?,
     ...
 ) {
   
   
     val semantics = if (contentDescription != null) {
   
   
         Modifier.semantics {
   
   
             this.contentDescription = contentDescription
             this.role = Role.Image
         }
     }
     ...
 }

Modifier 的 semantics() 扩展函数直接交给了 AppendedSemanticsElement()。

 fun Modifier.semantics(
     mergeDescendants: Boolean = false,
     properties: (SemanticsPropertyReceiver.() -> Unit)
 ): Modifier = this then AppendedSemanticsElement(
     mergeDescendants = mergeDescendants,
     properties = properties
 )

AppendedSemanticsElement 的 create() 则创建了 CoreSemanticsModifierNode 类型,并将包裹了 contentDescription 的 Unit 继续下发。

 internal data class AppendedSemanticsElement(
     ...
     val properties: (SemanticsPropertyReceiver.() -> Unit)
 ) : ModifierNodeElement<CoreSemanticsModifierNode>(), SemanticsModifier {
   
   
     ...
     override fun create(): CoreSemanticsModifierNode {
   
   
         return CoreSemanticsModifierNode(
             mergeDescendants = mergeDescendants,
             isClearingSemantics = false,
             properties = properties
         )
     }
     ...
 }

CoreSemanticsModifierNode 复写了 applySemantics(),即此处将执行 contentDescription 的收集。

 internal class CoreSemanticsModifierNode(
     ...
     var properties: SemanticsPropertyReceiver.() -> Unit
 ) : Modifier.Node(), SemanticsModifierNode {
   
   
     ...
     override fun SemanticsPropertyReceiver.applySemantics() {
   
   
         properties()
     }
 }

收集的操作是将 contentDescription 的内容按照 SemanticsProperties.ContentDescription 为 key 存入实现了 SemanticsPropertyReceiver 接口的 SemanticsConfiguration map 里。

至此,contentDescription 信息就收集好了。

 var SemanticsPropertyReceiver.contentDescription: String
     get() = throwSemanticsGetNotSupported()
     set(value) {
   
   
         set(SemanticsProperties.ContentDescription, listOf(value))
     }class SemanticsConfiguration :
     SemanticsPropertyReceiver,
     Iterable<Map.Entry<SemanticsPropertyKey<*>, Any?>> {
   
   
     ...
     override fun <T> set(key: SemanticsPropertyKey<T>, value: T) {
   
   
         if (value is AccessibilityAction<*> && contains(key)) {
   
   
             val prev = props[key] as AccessibilityAction<*>
             props[key] = AccessibilityAction(
                 value.label ?: prev.label,
                 value.action ?: prev.action
             )
         } else {
   
   
             props[key] = value
         }
     }
     ..
### Jetpack Compose 原理 Jetpack Compose 是一种现代的声明式 UI 工具包,专为 Android 开发设计。其核心理念是通过函数式的编程风格来描述界面的状态变化,并自动更新视图以反映这些状态的变化。 #### 1. **声明式 UI** - 在传统的 Android UI 开发中,开发者通常需要手动操作 View 对象及其属性。而在 Jetpack Compose 中,UI 是由可组合函数(Composable Functions)定义的[^5]。 - 可组合函数是一种特殊的 Kotlin 函数,用于描述 UI 的结构和行为。当数据发生变化时,Compose 自动重新调用受影响的部分,从而实现高效的 UI 更新。 #### 2. **不可变性与重组** - Jetpack Compose 鼓励使用不可变的数据模型。这意味着一旦某个对象被创建,它的状态就不会改变。这种设计理念使得框架能够更高效地检测到哪些部分发生了变化,并仅对那些部分进行更新[^4]。 - 当底层数据发生变更时,Jetpack Compose 不会完全重建整个 UI 树,而是只对受更改影响的小范围区域执行“重组”操作。这大大提高了性能效率。 #### 3. **状态提升 (State Hoisting)** - 状态提升是 Jetpack Compose 中的一种重要模式,允许父组件管理子组件的状态。这种方式有助于集中控制逻辑流,减少重复计算并增强代码复用能力。 --- ### Docker Compose 工作机制 Docker Compose 提供了一种简单的方式来定义和运行多容器的应用程序。它的工作流程主要围绕 YAML 文件展开,该文件用来描述服务、网络以及卷等内容。 #### 1. **YAML 定义** - 用户可以通过 `docker-compose.yml` 文件指定多个服务之间的依赖关系和服务本身的配置参数,比如镜像名称、端口映射、环境变量等[^1]。 #### 2. **上下文隔离** - 每个服务都被视为独立的任务单元,在自己的容器内运行。即使它们共享某些资源(如数据库),仍然保持良好的边界划分[^1]。 #### 3. **自动化部署** - 执行命令后,Docker Compose 将按照预设顺序依次启动各个服务实例。如果某项服务失败,则其余部分不会受到影响;反之亦然——成功完成初始化之后即可正常运作。 ```yaml version: '3' services: web: image: nginx:latest ports: - "80:80" db: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: example ``` 以上展示了一个简单的 Nginx 和 MySQL 组合案例,其中分别设置了公开访问端口及必要的密码字段。 --- ### 总结对比 无论是 Jetpack Compose 还是 Docker Compose,两者都体现了现代化软件开发中的模块化思维。前者专注于简化移动应用前端布局过程,后者则致力于优化后台微服务体系架构搭建体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TechMerger

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值