网站轮播图教程,百度网站的设计风格,dedecms转换wordpress方法,云南旅行社网站建设前言
众所周知#xff0c;Compose 作为一种 UI 工具包#xff0c;向开发者提供了实现 UI 的基本功能。但其实它还默默提供了很多其他能力#xff0c;其中之一便是今天需要讨论的#xff1a;Android 特色的 Accessibility 功能。
采用 Compose 搭建的界面#xff0c;完美…
前言
众所周知Compose 作为一种 UI 工具包向开发者提供了实现 UI 的基本功能。但其实它还默默提供了很多其他能力其中之一便是今天需要讨论的Android 特色的 Accessibility 功能。
采用 Compose 搭建的界面完美地支持了 Accessibility 功能它的 UI 变化能正确地发出无障碍事件 AccessibilityEvent 并响应来自无障碍服务的操作 AccessibilityAction。
那 Compose 是如何做到完美兼容传统的 Accessibility 机制的本文将按照无障碍事件、无障碍节点、无障碍操作等几个方向为你剖析 Compose 默默做了哪些事情。
目录
为 Compose 适配 contentDescriptionCompose 收集 Accessibility 语义信息Compose 特殊的 Accessibility 代理Compose 中 AccessibilityEvent 的产生和发送Compose 中 AccessibilityNode 的生成和提供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 configreturn config}SemanticsNode 需要复写各自的 applySemantics() 方法此后便被按照类型进行收集。比如负责提供核心语义的 CoreSemanticsModifierNode、提供点击相关语义的 ClickableSemanticsNode 等等。
事实上SemanticsConfiguration 本质上是 Map各类型语义在收集的时候会按照对应的 key 进行存储。
接下来我们以 contentDescription 和 click 两种语义信息为例阐述 Compose 是如何收集它们到 SemanticsConfiguration 中以供 Accessibility 系统调用的。
2-1. for contentDescription
先来看下 Image 控件的源码跟一下设置的 contentDescription 会如何传递。 Composablefun Image(...contentDescription: String?,...) {val semantics if (contentDescription ! null) {Modifier.semantics {this.contentDescription contentDescriptionthis.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)) : ModifierNodeElementCoreSemanticsModifierNode(), 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: Stringget() throwSemanticsGetNotSupported()set(value) {set(SemanticsProperties.ContentDescription, listOf(value))}class SemanticsConfiguration :SemanticsPropertyReceiver,IterableMap.EntrySemanticsPropertyKey*, Any? {...override fun T set(key: SemanticsPropertyKeyT, 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}}...}2-2. for click
我们知道通过 Modifier 可以设置 click Unit供执行 UI 上的单击操作。对于 Accessibility 功能来说也需要能够支持通过 AccessibilityService 输入触发点击操作。
我们以设置组合 click 的 CombinedClickableNode() 方式为例查看其 click 信息是如何和 Accessibility 交互的。
如下代码可以看到 CombinedClickableNode() 对外提供了点击语义节点 ClickableSemanticsNode其复写了 applySemantics() 方法而该方法则调用 SemanticsPropertyReceiver 的 onClick() 传递了 click Unit。 private class CombinedClickableNode( ...): ... {override val clickableSemanticsNode delegate(ClickableSemanticsNode(...))}private class ClickableSemanticsNode(...) : SemanticsModifierNode, Modifier.Node() {...override fun SemanticsPropertyReceiver.applySemantics() {...onClick(action { onClick(); true },label onClickLabel)...}}onClick() 则是将 label 和 click Unit 封装成 AccessibilityAction 实例并以 SemanticsActions.OnClick 为 key 存放在实现了 SemanticsConfiguration map 里。 fun SemanticsPropertyReceiver.onClick(label: String? null, action: (() - Boolean)?) {this[SemanticsActions.OnClick] AccessibilityAction(label, action)}至此click Unit 通过 AccessibilityAction 的形式收集好了。
3. Compose 特殊的 Accessibility 代理 收集到了 SemanticsConfiguration 后如何被 Accessibility 使用的呢 首先我们得了解一下 AccessibilityDelegate它是 Android 传统 View 提供的允许给某个 View 自定义 Accessibility 处理逻辑的机制。
本质上仍属于 ViewGroup 的 AndroidComposeView 在 init 的时候利用原生提供的 setAccessibilityDelegate() 接口设置了 AccessibilityDelegate 为 AndroidComposeViewAccessibilityDelegateCompat。 private val accessibilityDelegate AndroidComposeViewAccessibilityDelegateCompat(this)init {...ViewCompat.setAccessibilityDelegate(this, accessibilityDelegate)...}该代理类需要处理的逻辑非常多包括
处理 Compose 下 AccessibilityEvent 的产生和发送处理 Compose 下所有 Node 对应的 AccessibilityNodeInfo 实例的生成和提供处理 Compose 下 AccessibilityAction 的响应和执行
下面我们按照这 3 点逐步展开。
4. Compose 中 AccessibilityEvent 的产生和发送
我们以最常见的 window 内容变化 TYPE_WINDOW_CONTENT_CHANGED 的 AccessibilityEvent为例阐述 Compose 如何产生和发送它们。
首先当 AndroidComposeView 添加到 ViewGroup 之后会启动 LaunchedEffect 监听该 Compose 下所有 Node 在 bounds 上的变化。 private class WrappedComposition(...) : Composition, LifecycleEventObserver {...override fun setContent(content: Composable () - Unit) {owner.setOnViewTreeOwnersAvailable {if (!disposed) {...if (addedToLifecycle null) {...} else if (lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {original.setContent {...// 监听 bounds 变化LaunchedEffect(owner) { owner.boundsUpdatesEventLoop() }...}}}}}...}监听的具体逻辑由上述设置的 AccessibilityDelegate 完成。 suspend fun boundsUpdatesEventLoop() {accessibilityDelegate.boundsUpdatesEventLoop()}AccessibilityDelegate 会判断系统的 Accessibility 开关是否开启并在 ON 的时候 post 一个叫 semanticsChangeChecker 的 runnable。 suspend fun boundsUpdatesEventLoop() {try {val subtreeChangedSemanticsNodesIds ArraySetInt()for (notification in boundsUpdateChannel) {...if (isEnabledForAccessibility) {...if (!checkingForSemanticsChanges) {checkingForSemanticsChanges truehandler.post(semanticsChangeChecker)}}...}} finally {subtreeChangedLayoutNodes.clear()}}semanticsChangeChecker runnable 会将 Node 相关的 property change 下发交给 sendSemanticsPropertyChangeEvents() 统一处理。 private val semanticsChangeChecker Runnable {...checkForSemanticsChanges()...}private fun checkForSemanticsChanges() {...// Property changesendSemanticsPropertyChangeEvents(currentSemanticsNodes)updateSemanticsNodesCopyAndPanes()}sendSemanticsPropertyChangeEvents() 会遍历新的 Node 里发生变化的 property并产生对应 type 的 AccessibilityEvent。
比如
发现是 Progress 控件的 range 信息发生了变化则产生 TYPE_WINDOW_CONTENT_CHANGED发现是通用的 ContentDescription 发生了变化也产生 TYPE_WINDOW_CONTENT_CHANGED
还有很多其他的 property 变化会产生 AccessibilityEvent类型也各不相同这里不再一一展开。 internal fun sendSemanticsPropertyChangeEvents(newSemanticsNodes: MapInt, SemanticsNodeWithAdjustedBounds) {...for (id in newSemanticsNodes.keys) {val oldNode previousSemanticsNodes[id] ?: continueval newNode newSemanticsNodes[id]?.semanticsNodevar propertyChanged falsefor (entry in newNode!!.unmergedConfig) {...when (entry.key) {...SemanticsProperties.ProgressBarRangeInfo - {sendEventForVirtualView(semanticsNodeIdToAccessibilityVirtualNodeId(id),AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,AccessibilityEventCompat.CONTENT_CHANGE_TYPE_STATE_DESCRIPTION)...}SemanticsProperties.ContentDescription - {sendEventForVirtualView(semanticsNodeIdToAccessibilityVirtualNodeId(id),AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION,entry.value as ListString)}...}}...}}事件的初始化和发出还需要 sendEventForVirtualView() 具体完成。
检查下目标 View ID 是否合法以及 Accessibility 系统是否开启调用 createEvent() 构建 AccessibilityEvent 实例调用 sendEvent() 发送给 Accessibility 系统 private fun sendEventForVirtualView(...): Boolean {if (virtualViewId InvalidId || !isEnabled) {return false}val event: AccessibilityEvent createEvent(virtualViewId, eventType)if (contentChangeType ! null) {event.contentChangeTypes contentChangeType}if (contentDescription ! null) {event.contentDescription contentDescription.fastJoinToString(,)}return sendEvent(event)}createEvent() 通过 obtain() 拿到 AccessibilityEvent 新实例之后进行 className 等属性的初始化。并进行最重要的一步将该事件的 source 和 View ID 绑定便于后续从该事件中查找发生变化的 AccessibilityNodeInfo。 internal fun createEvent(virtualViewId: Int, eventType: Int): AccessibilityEvent {val event: AccessibilityEvent AccessibilityEvent.obtain(eventType)event.isEnabled trueevent.className ClassNameevent.packageName view.context.packageNameevent.setSource(view, virtualViewId)...return event}sendEvent() 将再次确保 Accessibility 系统的开启通过之后通过 AndroidComposeView 的 parent一般来说是 id 为 content 的 ViewGroup向 Accessibility 系统请求该 AccessibilityEvent 的最终发出。 private fun sendEvent(event: AccessibilityEvent): Boolean {if (!isEnabledForAccessibility) {return false}return view.parent.requestSendAccessibilityEvent(view, event)}后续便是利用 Android 传统 View 的链路向 AccessibilityManagerService 请求并经过 AccessibilityServiceConnection 的调度向活跃的 AccessibilityService 发出 AccessibilityEvent 变化的 callback。
因其不属于 Compose 的处理范畴了就不再具体展开了。
5. Compose 中 AccessibilityNode 的生成和提供
AccessibilityEvent 抵达 AccessibilityService App 之后它们需要从 AccessibilityEvent 里获取 source 对应的 AccessibilityNodeInfo 实例。
所以和 Android 传统 View 一样Compose 需要为目标 View ID 提供各层级所对应的 AccessibilityNodeInfo 实例。
当获取到 AndroidComposeView 的时候就会调度到上述设置的代理 AndroidComposeViewAccessibilityDelegateCompat。该代理通过 MyNodeProvider 类具体负责 AccessibilityNodeInfo 的构建。 private var nodeProvider: AccessibilityNodeProviderCompat AccessibilityNodeProviderCompat(MyNodeProvider())override fun getAccessibilityNodeProvider(host: View): AccessibilityNodeProviderCompat {return nodeProvider}inner class MyNodeProvider : AccessibilityNodeProvider() {}MyNodeProvider 构建 AccessibilityNodeInfo 的入口是 createAccessibilityNodeInfo()其会交给内部的 createNodeInfo() 继续。
createNodeInfo() 在进行 AccessibilityNodeInfo 实例的初始化、边界 Rect 赋值等基本操作之后执行最核心的信息填充populateAccessibilityNodeInfoProperties()。 inner class MyNodeProvider : AccessibilityNodeProvider() {override fun createAccessibilityNodeInfo(virtualViewId: Int):AccessibilityNodeInfo? {return createNodeInfo(virtualViewId)}...}private fun createNodeInfo(virtualViewId: Int): AccessibilityNodeInfo? {...val info: AccessibilityNodeInfoCompat AccessibilityNodeInfoCompat.obtain()...val semanticsNode: SemanticsNode semanticsNodeWithAdjustedBounds.semanticsNode...info.setSource(view, virtualViewId)val boundsInRoot semanticsNodeWithAdjustedBounds.adjustedBoundsval topLeftInScreen view.localToScreen(Offset(boundsInRoot.left.toFloat(), boundsInRoot.top.toFloat()))val bottomRightInScreen view.localToScreen(Offset(boundsInRoot.right.toFloat(), boundsInRoot.bottom.toFloat()))// 设置该 info 在 UI 上的范围 Rectinfo.setBoundsInScreen(android.graphics.Rect(floor(topLeftInScreen.x).toInt(),floor(topLeftInScreen.y).toInt(),ceil(bottomRightInScreen.x).toInt(),ceil(bottomRightInScreen.y).toInt()))// 将 Compose Node 和目标 Info 传入进行进一步的信息填充populateAccessibilityNodeInfoProperties(virtualViewId, info, semanticsNode)return info.unwrap()}我们知道AccessibilityNodeInfo 通常要明确它代表的 UI 控件类型当 App 通过 AccessibilityService 拿到该 info 时便于他们通过该类型准确理解其目标控件的作用和特点。该类型以 className 属性的形式存在于 AccessibilityNodeInfo 中。
在为 Compose UI 创建 AccessibilityNodeInfo 的时候一样需要进行这样的类型赋值。可是 Compose 内部的控件并不是传统的 View 控件所以需要依据 SemanticsNode 内记录的 Property 情况去差异化赋值。同时为了兼容传统 View 控件的命名方式具体赋值的内容还得借用和遵照 Android 传统 View 的类名。
所以populateAccessibilityNodeInfoProperties() 会执行如下处理 先统一地设置 className 为 “android.view.View” 如果对应的 SemanticsNode 是支持 setText Action 的类型则将 className 设置为 “android.widget.EditText” 反之如果是支持 Text Action 的类型设置为 “android.widget.TextView” packageName 则统一地被设置为 ComposeView 持有的 context 包名 后面则是各种各样的属性填充和所支持的 AccessibilityAction 的声明比如 检查当前的 View 和已 focus 的 View 是否一致以决定让该 info 支持清除还是添加无障碍的 focus action通过 x、y 方向上是否支持滚动以决定是否支持上下、左右滑动的 action 等等从 SemanticsNode 中取出 text 信息填充到 text 属性中这个属性是无障碍机制里最常用的检查 SemanticsNode 是否是 check 类型并赋值 Checkable 属性其他的还有 checked、selected 等常用属性 这里需要强调的 1 个非常重要的属性即 contentDescription它来自于代码里给控件指定的 contentDescription 信息。第三方的无障碍服务 App 非常依赖该属性进行朗读提示 还有 1 个重要的 Action 即 ACTION_CLICK当发现 SemanticsNode 里支持 OnClick 的时候需要标记该 info 可以处理 ACTION_CLICK 的 action 操作 internal class AndroidComposeViewAccessibilityDelegateCompat ... {...fun populateAccessibilityNodeInfoProperties( ... ) {// 先默认赋值一个 className后面依据具体类型再替换为对应的 View 包名info.className ClassName...if (semanticsNode.isTextField) {info.className TextFieldClassName}if (semanticsNode.config.contains(SemanticsProperties.Text)) {info.className TextClassName}// packageName 统一用一份 context 包名即可info.packageName view.context.packageName...// 依据 focus 的 View ID 决定支持清除还是添加 ACCESSIBILITY_FOCUSif (focusedVirtualViewId virtualViewId) {info.isAccessibilityFocused trueinfo.addAction(AccessibilityActionCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS)} else {info.isAccessibilityFocused falseinfo.addAction(AccessibilityActionCompat.ACTION_ACCESSIBILITY_FOCUS)}// 从 SemanticsNode 中取出 text 信息填充到 text 属性中setText(semanticsNode, info)...// 检查 SemanticsNode 是否是 check 类型并赋值 Checkable 属性setIsCheckable(semanticsNode, info)// 后面还有 checked、selected 等属性val toggleState semanticsNode.unmergedConfig.getOrNull(semanticsNode.unmergedConfig.getOrNull(SemanticsProperties.Selected)?.let {...// 赋值最重要的一个属性 contentDescriptionif (!semanticsNode.unmergedConfig.isMergingSemanticsOfDescendants ||semanticsNode.replacedChildren.isEmpty()) {info.contentDescription semanticsNode.infoContentDescriptionOrNull}// 其他的还有 isPassword、isEditable、isEnabled、isFocusable 等属性的填充info.isPassword semanticsNode.isPasswordinfo.isEditable semanticsNode.isTextFieldinfo.isEnabled semanticsNode.enabled()...// 同样的检查是否支持 OnClick Action// YES 的话结合 enabled 和 isSelected 状态// 决定 info 的 isClickable 属性以及是否支持 ACTION_CLICK 操作info.isClickable falsesemanticsNode.unmergedConfig.getOrNull(SemanticsActions.OnClick)?.let {val isSelected semanticsNode.unmergedConfig.getOrNull(SemanticsProperties.Selected) trueinfo.isClickable !isSelectedif (semanticsNode.enabled() !isSelected) {info.addAction(AccessibilityActionCompat(AccessibilityNodeInfoCompat.ACTION_CLICK,it.label))}}...// 其他的还有很多属性的填充和 Action 的是否支持...if (xScrollState ! null scrollAction ! null) {...if (semanticsNode.enabled()) {if (xScrollState.canScrollForward()) {info.addAction(AccessibilityActionCompat.ACTION_SCROLL_FORWARD)info.addAction(if (!semanticsNode.isRtl) {AccessibilityActionCompat.ACTION_SCROLL_RIGHT} else {AccessibilityActionCompat.ACTION_SCROLL_LEFT})}...}}...// 以及很多为了 OS 兼容性的处理if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) {Api29Impl.addPageActions(info, semanticsNode)}...}}6. Compose 中 AccessibilityAction 的响应和执行
除了负责 Compose 自己的 AccessibilityEvent 和 AccessibilityNodeInfo 以外Compose 还得为各控件去响应来自于 View 系统的、Accessibility 系统的 AccessibilityAction 操作。
省去 AccessibilityAction 发送到 AccessibilityDelegate 的通用流程我们直接看 Compose 收到该 Action 的入口仍然是上面提及的 MyNodeProvider 类对应的方法是 performAction()。 inner class MyNodeProvider : AccessibilityNodeProvider() {...override fun performAction(virtualViewId: Int,action: Int,arguments: Bundle?): Boolean {return performActionHelper(virtualViewId, action, arguments)}}performAction() 直接调用 performActionHelper() 进行内部的 Action 分发。
可以看到它需要处理的 AccessibilityAction 非常多这也是预料之中的毕竟原生的 Accessibility Action 太多了。 private fun performActionHelper(...): Boolean {val node currentSemanticsNodes[virtualViewId]?.semanticsNode ?: return false...if (!node.enabled()) {return false}when (action) {AccessibilityNodeInfoCompat.ACTION_CLICK - { ... }AccessibilityNodeInfoCompat.ACTION_LONG_CLICK - { ... }AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD,AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD,android.R.id.accessibilityActionScrollDown,android.R.id.accessibilityActionScrollUp,android.R.id.accessibilityActionScrollRight,android.R.id.accessibilityActionScrollLeft - {...}android.R.id.accessibilityActionPageUp - { ... }android.R.id.accessibilityActionPageDown - { ... }android.R.id.accessibilityActionPageLeft - { ... }android.R.id.accessibilityActionPageRight - { ... }android.R.id.accessibilityActionSetProgress - { ... }AccessibilityNodeInfoCompat.ACTION_FOCUS - { ... }AccessibilityNodeInfoCompat.ACTION_CLEAR_FOCUS - { ... }AccessibilityNodeInfoCompat.ACTION_SET_TEXT - { ... }android.R.id.accessibilityActionImeEnter - { ... }AccessibilityNodeInfoCompat.ACTION_PASTE - { ... }AccessibilityNodeInfoCompat.ACTION_CUT - { ... }AccessibilityNodeInfoCompat.ACTION_EXPAND - { ... }AccessibilityNodeInfoCompat.ACTION_COLLAPSE - { ... }AccessibilityNodeInfoCompat.ACTION_DISMISS - { ... }android.R.id.accessibilityActionShowOnScreen - { ... }...}}我们以最常见的 ACTION_CLICK 操作为例看下后续逻辑。
先从 View ID 对应的 SemanticsNode 里拿到存放各种语义信息的 SemanticsConfiguration map然后拿负责点击的 OnClick 为 key 进行查找。 private fun performActionHelper(...): Boolean {...when (action) {AccessibilityNodeInfoCompat.ACTION_CLICK - {val result node.unmergedConfig.getOrNull(SemanticsActions.OnClick)?.action?.invoke()sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_CLICKED)return result ?: false}...}...}可以得到在控件初始化时存入的 AccessibilityAction 实例之后直接 invoke 封装在其 action 属性里的 onClick Unit即可完成 click 操作的执行。 class AccessibilityActionT : FunctionBoolean(val label: String?, val action: T?) {...}结语
最后我们用一张图把整个流程串起来。 首先AndroidComposeView 的各 LayoutNode 初始化的时候通过 applySemantics() 将各 SemanticsNode 语义节点收集必要的信息并按照类型OnClick、ContentDescription 等的 key 存放到 SemanticsConfiguration 中 AndroidComposeView 初始化的时候设置特殊的 AccessibilityDelegate 代理类以告知 View 系统 Compose 下所有的 Accessibility 相关逻辑由该代理完成 接着监听 SemanticsConfiguration 里各信息的变化 Property Change 当某项信息变更的时候通过上述代理构建相应类型的 AccessibilityEvent 并发送到 Accessibility 系统 当 AccessibilityEvent 经过 AccessibilityManagerService 抵达 AccessibilityService 之后 AccessibilityService 从 Event 里获取目标的 AccessibilityNode 时上述代理会依据 View ID 从 SemanticsNode 里获取该控件的信息以创建合适的 AccessibilityNodeInfo 实例 AccessibilityService 对 AccessibilityNodeInfo 数据进行分析之后可以按需发送 AccessibilityAction。经过 AccessibilityManagerService 抵达 AndroidComposeView 后依据通过上述代理进行 performAction() 此后会依据 Action 的类型 key 去 SemanticsConfiguration 里查找到对应的 Action Unit 和执行
简单来说Compose 布局上所有的 Accessibility 逻辑都是通过特殊的 AccessibilityDelegate 完成包括
监听语义信息的变化发送无障碍事件 AccessibilityEvent接收无障碍节点 AccessibilityNode 的查找封装节点并返回响应无障碍操作 AccessibilityAction 的请求去找到对应的 Compose Node 执行 Click、Focus 等操作