为什么 Kotlin 中的无障碍是超能力,而非打勾项
\n\n想象一下:你用 Kotlin 写了一个很酷的应用。漂亮的 Material Design、流畅的动画、响应式数据流。你为代码感到自豪。但后来发现,一位视力障碍的用户无法点击“购买”按钮,因为 TalkBack 将其读作“未标记按钮”。或者一位手部震颤的用户总是点不到那个微小的开关。
\n\n无障碍并非枯燥的官僚流程。它关乎让你的 Kotlin 代码为每个人服务。根据世界卫生组织的数据,超过十亿人患有某种残疾。忽视他们,就意味着失去用户群体,而且坦率地说,也不是一名优秀的开发者。
\n\n在本文中,我们将探讨如何毫无痛苦地编写无障碍的 Kotlin 代码。无需大量样板代码。提供可直接复制粘贴的生动示例。
\n\n基本原则:ContentDescription——你最好的朋友
\n\n无障碍中最常见的失败点就是缺少界面元素的描述。Android 上的屏幕阅读器 TalkBack 无法读取图片或图标。它需要文字。
\n\n在 Kotlin 中,这通过 contentDescription 属性实现。但不要简单写成“按钮”,而要描述其含义。
糟糕的示例(请勿这样做)
\nimageView.contentDescription = "按钮"\n\n良好的示例
\nbinding.fabAddTask.contentDescription = "添加新任务"\n\n如果图标是装饰性的(仅用于美化),请设置 contentDescription = null,以便 TalkBack 忽略它。
// 装饰性图标——不打扰用户\nbinding.decorativeIcon.contentDescription = null\n\n提示: 使用字符串资源(strings.xml)以支持本地化。不要在 Kotlin 文件中硬编码中文文本。
语义标记:告诉系统什么重要
\n\n标准的 View(如 Button、TextView)表现尚可,但自定义组件常常破坏无障碍。在 Kotlin 中,我们可以显式地设置语义。
为元素添加角色
\nbinding.customView.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES\nbinding.customView.accessibilityDelegate = object : View.AccessibilityDelegate() {\n override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {\n super.onInitializeAccessibilityNodeInfo(host, info)\n info.text = "当前值:${viewModel.value}"\n info.contentDescription = "音量滑块"\n }\n}\n\n这段代码告诉系统:“嘿,这不只是一个方块,这是一个滑块!”。TalkBack 会明白该元素是可交互的,并能正确地进行交互。
\n\n元素分组
\n如果你的商品卡片包含图片、价格和名称,请将它们合并为一个焦点。否则,用户将逐个听取三个独立元素。
\n\nbinding.cardProduct.apply {\n isFocusable = true\n contentDescription = "商品:${product.name},价格:${product.price} 元"\n}\n\n现在,TalkBack 只需一次点击即可读取所有信息。
\n\n操作导航:键盘与方向键
\n\n并非所有用户都通过触摸屏幕操作。有些人使用键盘(通过 OTG)或方向键(遥控器)。你的应用应支持无需触摸即可操作。
\n\n在 Kotlin 中,这通过 focusable 和焦点顺序(nextFocusDown、nextFocusForward)来解决。
binding.buttonNext.nextFocusForwardId = R.id.buttonConfirm\nbinding.buttonConfirm.nextFocusDownId = R.id.buttonCancel\n\n请确保所有可交互元素都能获得焦点。一个常见错误是带有 ClickableSpan(文本中的链接)的 TextView。默认情况下,它无法获得焦点。修复方法如下:
binding.textWithLink.movementMethod = LinkMovement