Focus Order(焦点顺序)是指在使用键盘、Switch Control、VoiceOver 或 TalkBack 导航时,界面元素获取焦点的顺序。在移动应用中,焦点顺序决定了用户如何通过手势或按钮在控件之间移动。根据 W3C WCAG 2.2,成功标准 2.4.3,2023,焦点必须按照保持内容意义的逻辑顺序移动。违反此原则是无障碍审计不通过的常见原因之一。
要点
Focus Order(焦点顺序) 是指用户通过替代输入方式(键盘 Tab、Switch Control 逐步、VoiceOver 右/左滑动手势或 TalkBack)在交互元素之间移动的顺序。与鼠标或触摸屏(用户直接选择元素)不同,焦点导航是线性的——每步将焦点移动到下一个元素。
根据 Apple HIG, 2024,VoiceOver 使用基于视觉布局构建的无障碍树中的元素顺序:左上角 → 右下角。如果屏幕包含复杂布局(列、Grid、ZStack),树可能与视觉顺序不匹配。
WCAG 2.4.3 原则:“如果可以按顺序导航网页的部分且焦点顺序影响意义,则焦点必须按照保持意义和可操作性的顺序移动”。例外:动态内容中焦点可以跳跃以吸引注意力(警告、模态窗口)。
Switch Control 用户(行动障碍人士)会在元素之间自动移动——循环往复。如果顺序被打乱,用户完成表单需要多花 3 倍时间。根据 Deque University, 2024,正确的焦点顺序能为使用辅助技术的用户减少 60% 的表单填写时间。
特别注意——模态窗口。打开模态窗口后,焦点必须立即移动到模态框内的第一个交互元素(通常是“关闭”或“确认”按钮)。关闭后——返回到触发模态窗口的元素。这是 WCAG 2.4.3 的要求,同时也是常见错误。
在 iOS 中,VoiceOver 自动基于几何结构构建顺序:元素先按 Y 再按 X 排序。对于结构复杂的屏幕,此顺序可能不正确——开发者必须进行干预。
主要工具:
为商品卡片设置自定义顺序的示例:
class ProductCardView: UIView {
let titleLabel = UILabel()
let priceLabel = UILabel()
let buyButton = UIButton()
override var accessibilityElements: [Any]? {
get {
return [titleLabel!, priceLabel!, buyButton!]
}
set {}
}
}
在操作后程序化移动焦点:
UIAccessibility.post(
notification: .layoutChanged,
argument: newlyAddedItem
)
shouldGroupAccessibilityElement 属性对集合中的卡片非常有用。如果在父级卡片上设置为 true,VoiceOver 会将整个卡片视为一个元素。用户可以双击以激活整个卡片,或设置转子以在内部导航。推荐用于 UICollectionViewCell 和 UITableViewCell。
在 Android 中,TalkBack 也使用几何顺序,但优先考虑显式的 nextFocus* 属性。这些属性在 XML 中或以编程方式设置:
| 属性 | 用途 | 示例 |
|---|---|---|
| nextFocusDown | 向下导航时的元素 | @+id/field_email |
| nextFocusUp | 向上导航时的元素 | @+id/field_name |
| nextFocusLeft | 左侧的元素 | @+id/btn_back |
| nextFocusRight | 右侧的元素 | @+id/btn_next |
注册表单的示例:
<EditText
android:id="@+id/field_email"
android:nextFocusDown="@+id/field_password" />
<EditText
android:id="@+id/field_password"
android:nextFocusDown="@+id/btn_submit" />
对于 RecyclerView,焦点顺序是动态的——由适配器决定。如果单元格具有复杂结构,请设置 descendantFocusability = “beforeDescendants” 并在列表元素节点中指定顺序。对于 Jetpack Compose,焦点顺序通过 Modifier.focusOrder() 和 FocusOrder 设置。优先级:previous(子级)、next(下一个)、custom key。
如果元素太小(小于 44pt),请通过 iOS 的 TouchDelegate 或 Android 的 minWidth/minHeight 增加点击区域。根据 Google Material Design, 2024,最小触摸区域为 48×48dp。VoiceOver 和 TalkBack 聚焦于元素的边界框。小于 30pt 的元素可能无法通过手势焦点访问——用户无法用物理手指点击到它们。
跳跃焦点——当操作后(如删除元素)焦点跳到列表开头或系统“返回”按钮时。VoiceOver 用户会失去上下文。解决方案:程序化地将焦点移动到最接近已删除元素的元素。
隐形焦点——元素获得焦点但没有视觉指示器(键盘用户看不到自己的位置)。在 iOS 中检查 UIAccessibility.isVoiceOverRunning 以使用自定义指示器。根据 Deque University, 2024,隐形焦点是无障碍审计失败的第二大常见原因。
模态窗口——打开模态窗口后焦点仍留在背景内容上。在 iOS 中,如果设置了 modalPresentationStyle = .pageSheet,模态视图会自动捕获焦点。在 Android 中,在对话框容器上使用 setFocusable(true)。
相反的问题:焦点卡在模态窗口内无法退出(关闭除外)。这仅允许用于模态窗口——用户必须有意识地关闭窗口。对于普通屏幕,焦点陷阱是严重错误。解决方案:确保模态窗口的最后一个元素(“关闭”按钮)将焦点传递回去。
对于自定义屏幕(地图、画布、游戏),自动几何顺序不适用。开发者必须手动构建无障碍树。在 iOS 中,需要重写 UIAccessibilityContainer 方法。
自定义画布的示例:
class CanvasView: UIView {
var shapes: [ShapeView] = []
override var accessibilityElements: [Any]? {
get {
// 按 Z 索引排序形状,而不是按几何形状
return shapes.sorted { $0.zIndex < $1.zIndex }
}
set {}
}
}
在 Android 中,对于自定义 View 重写 onInitializeAccessibilityNodeInfo:
override fun onInitializeAccessibilityNodeInfo(
info: AccessibilityNodeInfo
) {
super.onInitializeAccessibilityNodeInfo(info)
info.addChild(firstElement)
info.addChild(secondElement)
info.isFocusable = true
}
对于动态列表(聊天、新闻流),在添加元素后调用焦点移动到第一个新元素。iOS 中:UIAccessibility.post(notification: .layoutChanged, argument: newMessage)。Android 中:sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)。
iOS 自动根据元素的 frame 确定焦点区域。如果元素有变换(transform、rotation),VoiceOver 可能会聚焦到错误区域。显式设置屏幕坐标中的 accessibilityFrame:element.accessibilityFrame = UIAccessibility.convertToScreenCoordinates(element.bounds, in: element)。这确保 VoiceOver 高亮正确的区域。
对于动画屏幕(UIKit Dynamics、Lottie、SpriteKit),程序化焦点尤为重要。VoiceOver 无法为动态移动的元素构建无障碍树。在动画容器上设置 isAccessibilityElement = false,仅对内部的交互元素设置为 true。
手动测试:启用 VoiceOver(iOS)或 TalkBack(Android),用手势向右划过整个序列。焦点应遵循视觉顺序——从左到右,从上到下。每个交互元素应该恰好获得一次焦点。
自动化测试较为困难但可行:
func testKeyboardFocusOrder() {
let app = XCUIApplication()
app.launch()
app.textFields["Email"].tap()
// Tab — 仅限外接键盘
}
对于 Android,使用 Accessibility Testing Framework:
@Test
fun testFocusOrder() {
onView(withId(R.id.fieldEmail))
.check(matches(isFocusable()))
onView(withId(R.id.fieldEmail))
.perform(focus())
onView(withId(R.id.fieldPassword))
.check(matches(isFocused()))
}
最可靠的方法——场景 UI 测试:逐步填写表单(电子邮件→密码→发送),检查每一步是否成功完成。如果焦点顺序被破坏,场景将在尝试与焦点外的元素交互时失败。
Xcode 中的 Accessibility Inspector 工具显示完整的无障碍树。您可以按 VoiceOver 顺序浏览元素,查看精确的焦点路径。使用“Audit”选项卡自动搜索 Focus Order 违规。
常见问题
WCAG 2.4.3(Focus Order)——A 级成功标准。要求焦点顺序在顺序导航中保持内容的意义。违规被视为严重问题,会阻止认证。
隐藏元素在 iOS 中应设置 isAccessibilityElement = false,在 Android 中设置 visibility = gone/invisible。出现时——通过 UIAccessibility.post(notification: .layoutChanged) 程序化移动焦点。
iOS 通过 accessibilityElements 和 shouldGroupAccessibilityElement 管理,Android 通过 nextFocus* 属性和 AccessibilityNodeInfo 管理。原理相同:默认几何顺序,可覆盖。
在根元素上设置 descendantFocusability = “beforeDescendants”,并通过每个单元格的 onInitializeAccessibilityNodeInfo 在适配器中配置顺序。
通过蓝牙或 USB 连接外接键盘。在 iOS 中按 Tab 移动焦点。在 Android 中启用 TalkBack 并使用 Tab 和方向键。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。