A thumb pressing a tactile accessibility switch with a scarlet-red rubber dome on a Scandinavian oak surface, headphones blurred beside — the visual marker for mobile-native accessibility-API comparison.
Image description: A thumb pressing a tactile accessibility switch with a scarlet-red rubber dome on a Scandinavian oak surface, headphones blurred beside — the visual marker for mobile-native accessibility-API comparison.

工程入门 · 移动无障碍API

2026年移动原生无障碍API:UIAccessibility、AccessibilityNode与网页端

iOS UIAccessibility、Android AccessibilityNodeInfo及跨平台桥接层的深度对比——哪些映射清晰,哪些存在问题,以及移动端网页无障碍的边界在哪里。

2026年移动原生无障碍API:
UIAccessibility、AccessibilityNode与网页端

iOS和Android各自向平台屏幕阅读器暴露了一棵功能完整的无障碍树——然而这两棵树除了标签和角色的基础层面,几乎没有任何共识。我们梳理了VoiceOver和TalkBack在2026年实际使用的每一个原语,分析了React Native、Flutter和Kotlin Multiplatform尝试桥接它们的方式,以及移动端WebView无障碍支持在哪里悄然跌落悬崖。

2
对比的原生API
3
跨平台桥接框架
28
已映射的原语
阅读约需13分钟
更新于2026年5月

1. iOS UIAccessibility — 标签、特征、提示与值

iOS屏幕上每一个可见元素,都拥有或可以拥有一个无障碍表示。Apple通过UIAccessibility非正式协议来暴露这一表示——该协议由UIView及所有系统控件实现,同时通过UIAccessibilityElement(一个轻量级类)来处理那些被绘制出来却本身不是视图的界面元素,例如自定义图表中的字符、Core Graphics场景中的字形、CALayer内部的区域。VoiceOver、Switch Control、Full Keyboard Access和Voice Control均消费同一套协议;掌握它,即同时获得四种辅助技术的支持。

该协议暴露了几乎适用于所有界面的四个核心原语。无障碍标签(label)是元素的简短人类可读名称——“发送”、“Asha的个人照片”、“返回”。无障碍特征(traits)是一个类似角色标志的位掩码——.button.header.image.selected.adjustable.staticText.updatesFrequently——用于告知VoiceOver如何处理该元素以及启用哪些手势。无障碍值(value)是当前状态的字符串表示(“开”、“75%”、“2026年5月22日,星期四”)。无障碍提示(hint)是一段较长的可选说明(“双击以打开照片查看器”),当用户没有对标签作出响应时,VoiceOver会延迟后播报。

这四个原语可以组合使用。一个开关的读法为:标签+特征+值,即”Wi-Fi,开关按钮,开”。一个滑块的读法为:标签+特征+值+提示,即”音量,可调节,60%,向上或向下滑动以调整”。一个自定义绘制的图表柱形读作一组UIAccessibilityElement,每个元素拥有标签、值和在容器内的frame。这条链就是API接口——VoiceOver在用户向右滑动时线性遍历它,并按照容器的accessibilityElements数组中元素的发布顺序进行播报。

SwiftUI使用同一套协议,只是提供了更友好的外观

SwiftUI中的.accessibilityLabel().accessibilityValue().accessibilityHint().accessibilityAddTraits()视图修饰符,在底层会编译为对应UIView上相同的UIAccessibility属性。SwiftUI还新增了.accessibilityElement(children:),以比UIKit方案更具声明式的方式解决”图表中字符”的问题——但VoiceOver所见的运行时契约是一致的。学习UIKit的命名依然值得,因为所有Apple示例、Stack Overflow答案以及无障碍审计都使用这套语言。


2. Android AccessibilityNodeInfo — 角色、动作与importantForAccessibility

Android走的是另一条路。iOS将无障碍挂载到每个视图的扁平协议上,而Android则将整棵无障碍树序列化为一组AccessibilityNodeInfo对象图,每个对象都是TalkBack查询到达那一刻视图的快照。框架按需懒构建这些快照;一个View通过重写onInitializeAccessibilityNodeInfo()(或在Compose中设置语义修饰符)来发布其节点,平台再将父子关系拼接成与视图层级对应的树。

这些原语与iOS在三个有意义的维度上存在差异。首先,Android通过字符串类型的className字段来暴露角色(role)——android.widget.Buttonandroid.widget.CheckBoxandroid.widget.EditText。TalkBack读取类名并决定如何播报(“按钮”、“复选框”、“编辑框”)。Compose将其Role.ButtonRole.CheckboxRole.RadioButton语义翻译至同一字段。角色比iOS的特征位掩码更细粒度,但也更为刚性——除非接受”view”这一播报结果,否则没有”完全自定义”的角色。

其次,Android将交互性表示为一组附加到节点的动作(actions)ACTION_CLICKACTION_LONG_CLICKACTION_SCROLL_FORWARDACTION_SET_TEXTACTION_FOCUS,以及一长串可通过AccessibilityNodeInfo.AccessibilityAction注册的自定义动作。TalkBack通过”动作”旋转菜单来呈现自定义动作——用户单指向上滑动,即可逐一听到每个自定义动作的名称。iOS有相同的概念(UIAccessibilityCustomAction),但在Android上动作列表就是界面;在iOS上则是手势词汇。

第三,Android拥有importantForAccessibility,一个per-view枚举(autoyesnonoHideDescendants),控制节点是否出现在树中。noHideDescendants是Android无障碍中最强大的工具,也是最常被遗忘的——它将整个子树从TalkBack的遍历中移除,相当于网页端的aria-hidden=“true”。iOS没有精确对应物;最接近的方式是将容器的accessibilityElements设置为空数组,但这只会移除容器的直接子元素,而非整个子树。

”实时区域”的不匹配

Android通过ViewCompat.setAccessibilityLiveRegion()暴露三个值:nonepoliteassertive。词汇与ARIA几乎一致。TalkBack也可靠地兼顾礼让级别。iOS在协议层面没有类似的机制:更新通知通过调用UIAccessibility.post(notification: .announcement, argument: “已保存”)来发送,这是一个命令式的一次性调用,不与视图绑定。跨平台桥接框架不得不在两者之上相互模拟,而这种阻抗失配在第3节评审的每个框架中都有所体现。


3. 跨平台桥接框架 — React Native、Flutter、Kotlin Multiplatform

每个跨平台移动框架都必须将上述两套API整合为一个统一的、框架化的接口。没有一个框架能完全做到这一点。2026年主导市场的三种方案——React Native、Flutter和搭配Compose Multiplatform的Kotlin Multiplatform——各自在泄漏与抽象之间做出了略微不同的取舍。

React Native 0.76
JS桥接原生UIKit和Android View
映射最明确——也最容易泄漏
iOS桥接PressableView上的accessibilityLabelaccessibilityHintaccessibilityRoleaccessibilityState几乎1:1映射到UIAccessibility——但角色名称是React Native的词汇,而非iOS的。
Android桥接相同的JS属性通过Yoga侧适配器映射到AccessibilityNodeInfo;accessibilityRole=“button”className设置为android.widget.Button
注意事项accessibilityLiveRegion属性仅适用于Android——在iOS上它静默无效,必须手动调用AccessibilityInfo.announceForAccessibility()
Flutter 3.27
自定义渲染 · 合成无障碍树
最统一——也最不透明
方式Flutter将所有内容渲染在Skia画布上,因此它构建自己的SemanticsNode树,并按需将其序列化到平台。
iOS路径SemanticsNode被转换为Flutter视图上的UIAccessibilityElement实例,特征从SemanticsActionSemanticsFlag集合映射而来。
Android路径同一棵SemanticsNode树由Flutter的Android视图序列化为AccessibilityNodeInfo节点;动作变为AccessibilityActions;实时区域变为SemanticsFlag.isLiveRegion
Kotlin Multiplatform · Compose Multiplatform
共享Compose运行时 · per-target无障碍
最新,且具有最多平台形态的接缝
方式Compose的Modifier.semantics { }一次性定义角色和动作;每个目标平台将同一语义块翻译为自身的原生无障碍API。
iOS目标Compose-for-iOS运行时遍历语义树并构造UIAccessibilityElements——但iOS实现比Android更年轻,仍缺少若干语义类型。
Android目标成熟路径:语义通过Android原生Compose所使用的同一compose-ui-semantics层转化为AccessibilityNodeInfo。

三者呈现出相同的模式:一侧是合成的、框架化的语义树,另一侧是两棵平台化的无障碍树,中间是一个翻译器——对简单情况处理良好,对复杂情况则存在明显的保真度损耗。简单情况——带标签的按钮、有alt文本的图片、标题——可以无损往返。复杂情况——需要双指滑动的自定义手势、元素应构成可聚焦分组的图表、必须在iOS上触发但没有视图绑定礼让设置的实时区域——要么将底层平台的词汇泄漏到跨平台代码中,要么根本无法翻译。

“移动无障碍的前80%在每个框架中是相同的。最后20%,才是每个框架暗中遵循哪套原生API的地方。”

— Disability World 工程编辑部,2026年5月

4. WebView的鸿沟 — 移动端网页无障碍在哪里悄然失效

iOS和Android均通过系统WebView渲染网页内容——iOS上是WKWebView,Android上是android.webkit.WebView(或Chrome Custom Tabs)。从宿主应用的角度看,WebView是一个黑盒:应用只看到一个视图,但屏幕阅读器能看到其中完整的DOM无障碍树。两棵树之间的桥接,正是大量移动端无障碍问题悄然出现的地方。

这一机制表面上相当直观。当屏幕阅读器的焦点进入WebView时,平台直接从浏览器引擎(iOS上是WebKit,Android上是Blink)读取文档的无障碍树,并将其作为宿主应用树的子树进行遍历。网页的角色、标签和ARIA属性被实时翻译为平台词汇。WebView内部没有显式角色的button元素在两个平台上均被读为按钮;aria-live=“polite”区域在两个平台上均能正确播报;链接上的aria-label会作为链接的无障碍标签浮现。在移动网页的最初三年里,这一切运作良好。

悬崖出现在三处。首先,在宿主应用中定义的自定义手势——双指滑动关闭、魔法点击播放暂停——对WebView内容是不可见的;当焦点位于文档内部时,它们会打到错误的目标上,或根本无法触发。其次,覆盖在WebView之上的宿主应用UIAccessibilityElement(悬浮动作按钮、自定义工具栏)会与WebView的树竞争遍历顺序,而最终的阅读顺序在iOS各版本之间是不确定的。第三——也是移动端网页无障碍最大的单一失效模式——iOS上的WebView对aria-live礼让级别的处理方式与Safari标签页中不同:WKWebView的播报管道丢弃了politeassertive的区分,无论标记如何,所有实时更新均被视为polite

同一DOM的两种视角
在Mobile Safari标签页中
<div role="alert" aria-live="assertive">
  连接已断开——正在重试。
</div>

在普通Safari标签页中,VoiceOver会打断当前播报并立即读出消息。assertive礼让级别通过WebKit端到端得到了保留。

在WKWebView中的同一DOM
<div role="alert" aria-live="assertive">
  连接已断开——正在重试。
</div>

相同的标记,相同的浏览器引擎——但WKWebView到UIKit的无障碍桥接将播报降级为延迟的polite消息。用户会延迟后才听到,有时已在损坏的表单中完成了输入。

真正有效的跨平台解决方案

对于WebView内部的播报,2026年唯一可靠的跨平台方案是向宿主应用暴露一个JavaScript桥接——一个微型postMessage处理器——将assertive播报从DOM路由到宿主应用,再通过iOS的UIAccessibility.post(notification: .announcement, …)或Android的announceForAccessibility()触发平台播报。网页的aria-live只适用于真正的polite消息——即接受几秒延迟的场景。


5. 映射对照表 — 什么对应什么

我们映射了VoiceOver和TalkBack在实际使用中消费的28个原语——iOS UIAccessibility协议接口、Android AccessibilityNodeInfo接口,以及最常用的React Native和Flutter跨平台属性的并集。下表仅列出有争议的行:即映射不完整、不对称或令人意外的原语。映射清晰的行(标签、按钮角色、图片角色、标题)已省略,以控制篇幅。

能力iOS UIAccessibilityAndroid AccessibilityNodeInfoReact Native 0.76Flutter 3.27
提示文本(较长说明)accessibilityHinttooltipText(API 28+)accessibilityHint(仅iOS)SemanticsProperties.hint
实时区域礼让级别N/A — 仅命令式postsetAccessibilityLiveRegion()accessibilityLiveRegion(仅Android)SemanticsFlag.isLiveRegion
从无障碍中隐藏子树accessibilityElementsHidden(仅子元素)importantForAccessibility=“noHideDescendants”accessibilityElementsHidden / importantForAccessibilityExcludeSemantics widget
自定义动作(旋转菜单)UIAccessibilityCustomActionAccessibilityNodeInfo.AccessibilityActionaccessibilityActions + onAccessibilityActionSemanticsAction带自定义标签
可调节/滑块语义UIAccessibilityTraitAdjustable + accessibilityIncrementRangeInfo + ACTION_SCROLL_FORWARDaccessibilityRole=“adjustable” + 处理器Slider暴露SemanticsAction.increase
标题级别UIAccessibilityTraitHeader(无级别)setHeading(true)(无级别)accessibilityRole=“header”(无级别)SemanticsProperties.headingLevel(1–6)
已选/已切换状态UIAccessibilityTraitSelectedsetSelected(true) + setCheckable()accessibilityState={selected, checked}SemanticsFlag.isSelected
分组/容器语义shouldGroupAccessibilityChildrensetScreenReaderFocusable(true)父元素上的accessible={true}MergeSemantics widget
播报一次性消息UIAccessibility.post(.announcement, …)view.announceForAccessibility()AccessibilityInfo.announceForAccessibility()SemanticsService.announce()

从表中可以发现三个规律。首先,实时区域的不对称是跨平台差异最大的来源——Android有per-view的礼让设置,iOS只有全局命令式post,每个框架都不得不在这一差异上撒谎。其次,标题级别是Flutter真正优于两个原生平台的唯一地方;iOS和Android的原语只知道”这是一个标题”,而不知道”这是H2下面的H3”。第三,“从无障碍中隐藏”的原语在Android上比在iOS上更灵活——noHideDescendants一步隐藏整个子树,而iOS要求逐一隐藏每个容器的子元素。


6. 移动原生实战手册

1

先学原生词汇,再学框架词汇

每个跨平台桥接框架——React Native、Flutter、Compose Multiplatform——都有自己的无障碍属性命名,而每一个名称都是对底层平台实际行为的轻微误导。当屏幕阅读器播报不正确时,问题几乎总在框架所翻译的原生API中,而不在你设置的框架属性上。至少通读一遍UIAccessibility文档和AccessibilityNodeInfo文档;之后再读框架文档才会豁然开朗。

2

专门在iOS上测试实时播报

第2节所述的实时区域不对称意味着,任何假设aria-live=“assertive”accessibilityLiveRegion=“assertive”有效的代码都会在iOS上静默降级。在发布任何用户体验依赖于状态变化播报的功能之前,先搭建一个小型测试工具,在真实设备上分别用VoiceOver和TalkBack触发polite和assertive两种播报,进行验证。

3

对assertive播报绕过WebView

WKWebView将assertive播报降级这一问题,苹果短期内不会修复——从iOS 14起的每个版本都是如此。如果你的混合应用允许用户在WebView内部遇到致命错误,请将播报通过JS桥接路由到宿主应用,让宿主触发平台播报。仅靠网页端是不够的。

4

使用框架的”合并”或”分组”语义,而非逐子元素处理

iOS(shouldGroupAccessibilityChildren)、Android(setScreenReaderFocusable)和Flutter(MergeSemantics)都提供了将视觉集群——一个图标加一个标签加一个值——折叠为单个无障碍元素的方式。请使用它。默认的”每个叶节点都是可聚焦元素”行为会将一个包含六个元素的导航芯片变成六次VoiceOver滑动。

5

使用Accessibility Inspector和TalkBack开发者设置进行审计

两个平台都提供了免费的官方工具来检查实时无障碍树——macOS上与连接的iOS模拟器或设备配对的Accessibility Inspector,以及Android上的”显示无障碍焦点”和”开发者设置”叠加层。使用它们以屏幕阅读器所见的方式读取自己应用的树;不要以为框架的调试日志和平台向TalkBack展示的内容是一样的。


结语:框架处于平台的下游

人们很容易相信——框架文档也助长了这一信念——跨平台无障碍API是对两套等效原生API的单一统一抽象。第5节的映射对照表推翻了这一统一论。两套原生API是由两个不同的团队、围绕屏幕阅读器应如何遍历文档的两种不同心智模型独立设计的;差异是真实存在的,它们渗透到每个框架中,并在用户体验中最关键的部分显现——实时更新、自定义手势、隐藏子树、标题层级。

好消息,在那段话之后:基础映射是有效的。带标签的按钮、有alt文本的图片、章节顶部的标题——这些在每个框架中都能无损往返,并在两个平台上正确播报。如果只使用这些原语,不需要考虑UIAccessibility或AccessibilityNodeInfo;框架的默认值是诚实的。麻烦始于用户界面开始做一些有趣的事情,而这也恰恰是无障碍最为重要的时候。

第6节的实战手册是让最多残障用户获得可用体验的最简论点:先以原生原语思考,在两个平台的真实设备上测试,在必要时绕过WebView,有意识地分组叶节点,并使用官方检查工具。你选择的框架帮你完成前80%,并在最后20%时让开。最后这20%,才是屏幕阅读器用户生活的地方。

“VoiceOver和TalkBack从同一份源代码读出了两份不同的文档。用户是否察觉到这一差异,衡量的是你对框架之下那个平台的理解程度。”

— Disability World 工程编辑部,2026年5月