大多数浏览器和
Developer App 均支持流媒体播放。
-
自定义转子的旁白效率
了解如何集成自定义转子,并帮助用户使用旁白功能在 app 中的复杂场景进行导航。了解自定义转子如何帮助用户操控无比复杂的界面、如何实现自定义转子,以及转子如何改善用户利用旁白功能进行导航。 要想充分利用本节内容,建议先熟悉常用的辅助功能原则以及 iOS 和 iPadOS上的旁白辅助功能 API。了解相关概述,请观看“通过自定义操作让访问 App 更方便”。
资源
相关视频
WWDC23
WWDC21
WWDC19
-
下载
大家好 欢迎参加全球开发者大会
大家好 我是亚历克斯沃尔扎克 今天我将为大家展示 (自定义转子的旁白效率) 如何添加自定义转子到旁白 从而使你的 App 更易于 被所有用户访问 Apple 的屏幕阅读器 旁白 让你能够随心交互所有 Apple 设备 无须观看屏幕 只需触摸屏幕 即可使用旁白听到你手指下方的内容 然后执行简单手势来导航用户界面 (TODAY AT APPLE 本地编辑) 不看屏幕的人可通过转子 来完成各种任务 今天我就为大家简单展示一下 通过在屏幕上转动两个手指 就像转动表盘一样 转子的力量就在你的指尖启用了
向下轻扫就会移动到屏幕的 下一个转子项目 向上轻扫就会移动到上一个项目 通过在 app 中 添加响应简单轻拂手势的自定义转子 就可改变用户的 app 体验 就连复杂的导航界面也变得更为简便 (导航复杂的界面 查找相关元素) 要在 App 中查找相关元素 只需直接向上或向下轻拂即可 先来看看几个示例 先从自定义转子 如何让导航复杂的界面变得更简单开始 首先 我为大家展示一下 在我 app 中的旁白体验 无须任何自定义转子 我正在开发的 app 可以在地图上显示用户所在位置 以及附近的 Apple Store 商店和公园 接着打开旁白 就能看到它是如何自上到下 穿越 app 的视图 遵循用户界面的布局方向 Apple 海湾大街 圣弗朗西斯科湾 海湾大桥 恶魔岛
这个坐标方格 代表在没有自定义转子的情况下 人们通过旁白来体验我的 app 的感受 就像在本演示当中 我们不但能看到旁白光标 在 Apple Store 商店和公园之间移动 而且也能看到它 在桥梁和其他景点之间的移动情况
注意 使用旁白的人 必须按顺序移动所有类别中的项 而看着屏幕的人 将使用图标及其颜色来关注 单个类别里的项目 要怎样才能让旁白用户 像其他任何人一样 来体验我的 app 呢? 首先要找出 有哪些界面项目在视觉上 吸引了人们的注意力 就我的 app 而言 它就是 Apple Store 商店和公园的标志
接下来 按类别对这些项目进行分组 然后创建自定义转子 以探索多个在其类别内的项目
开始吧
我们将为 Apple Store 商店添加转子 为公园添加另一个转子
这样我们就可以根据用户与各地点的距离 对每个转子项目进行排序
(0.1 英里 - 1.7 英里 - 2.5 英里 0.3 英里 - 1.0 英里 - 1.4 英里) 并将距离包含在每个项目的 辅助功能信息中 这样一来 使用转子与用户界面交互的人 就可以快速扫描出 距离最近的 Apple Store 商店 (Apple Store 商店) 以及扫描出最近的公园 (公园) 一般来说 人们在看屏幕时 很有可能只会留意 离他们位置最近的地点
一起看看实现这两个转子之后的操作 首先来看看 已完成的 Apple Store 商店转子 Apple Store 商店 Apple 栗树街 0.9 英里 Apple 联合广场 1.8 英里 Apple 斯通镇 4.6 英里 现在来看看公园的转子 公园
阿拉莫广场 0.8 英里 科罗纳高地公园 1.5 英里 嬉皮士山 1.5 英里 在这两个示例中 用户可以在每个自定义转子中 浏览经排序后的地点 从而可快速确定离他们最近的地点 要么是 Apple Store 商店 要么是公园 从这些示例中可看出自定义转子 可以为所有用户提供类似的体验 尤其是在导航复杂界面的时候 比如像这幅地图 怎样才能将这些转子添加到 app 中呢? 当旁白进入“地图视图”时 就开始在视图中的 accessibilityCustomRotors 属性 寻找任意一个自定义转子
我的 app 需要两个转子 一个用于Apple Store 另一个用于公园 所以我就将这个属性设置为 两个自定义转子
每个转子都会过滤相同的地图注释 因此我们只需要执行一种方法 就能创造两个转子
让我们开始构建这个方法 返回一个新的 UIAccessibilityCustomRotor 以代表某个类型的兴趣点 开发人员只需极少的额外工作 就可以实现自定义转子 采用区块语法即可实现 通过使用本地化名称初始化 UIAccessibilityCustomRotor 之后 我们将在闭包中执行基本逻辑 然后返回 UIAccessibilityCustomRotorItemResult 这样就会出现 代表下一个兴趣点的旁白
然后从区块参数中提取当前转子项 并准备用户可接触的可能项列表
“搜索方向”属性 会告知用户是否打开以转到上一项 或者向下转到下一项 此信息可用于递减或递增 可能项列表中的索引
返回 nil 值 则可告知用户 当前处于第一项还是最后一项 而旁白将继续关注该兴趣点 否则就通过返回新的 UIAccessibility CustomRotorItemResult 来完成 以前一项或下一项作为目标元素 虽然我提到了很多步骤 不过作为开发人员 请务必记住 想在你的 app 中利用自定义转子 API 只要实施闭包 并添加新的自定义转子 到视图的 accessibilityCustomRotors 列表中 自定义转子可以极大地影响 人们与 app 元素组之间的交互方式 但这些并不是 改善复杂界面辅助功能的唯一方法 (可访问界面) 想要了解另一种方法 请查看这篇文章 关于自定义操作如何增强 与 app 中单个元素的交互 (查找相关元素) 我们已经知道自定义转子可用来 查找相关元素 比如商店和公园 现在来了解如何在不同类型内容上 使用自定义转子:文本 (金门公园 计划你的行程) 当你点击某个地点时 我的 App 就会弹出小册子 既然我打算去金门公园野餐 我就打开了它的小册子
哇 有这么多内容 那就使用可自动显示文本的 内置字间转子吧 以便逐行浏览这本小册子
字间 金门公园 计划你的行程 荷兰风车 三月份出发 可观赏盛开的郁金香 这座高耸的地标矗立在公园东侧 东梅多 野餐区因维修而关闭 宠物家庭野餐的好去处 太棒了 我们能够听到内容解说 但看看要花多长时间才能得到 第一个警报 我们必须听完前面所有的内容 才发现绝对不应该选择东梅多 作为我的野餐地点 更别说后面还有好几个警报呢 所以如果不仔细倾听 这本小册子中的每句话 就不知道自己是否已经听完 所有的警报 对吧? 等等 其实 我们可以设计只浏览警报的 自定义转子 就可以迅速听到关键信息了 就像看着屏幕的人 马上会注意到警报图标一样 一起来看看“警报”转子会是什么样子 警报 东梅多 野餐区因维修而关闭 橡树林小径 道路泥泞 海洋沙滩 预计会有浓雾和大风 太棒了 不出所料 “警报”转子只在警报之间移动 这样就能更有效地计划公园之旅了
文本视图的唯一自定义转子 仅用于警报 所以在我们的实现中 这次 TextView 的 accessibilityCustomRotors 属性 只包含单个转子 跟之前一样 这次的实现方法 同样会返回到新的转子 不过这次输入的不是位置类型 比如商店或公园 我们将传入 我们希望放在转子中的文本属性类型
在本例中的输入 就是警报属性 正如我们预期的那样 这方法具有与地图示例类似的语法 同样 利用本地化名称初始化 UIAccessibilityCustomRotor 并实现返回到旁白将移动到的项闭包
我们可以在 TextView 的属性文本中 找到所有警报 因为它们都标记有自定义的警报属性 考虑到文本中当前转子项目的范围 我们的目标是 根据用户想前往的方向 确定上一个或下一个警报的范围 而此时 我们的做法是 根据用户手势 来确定搜索自定义警报属性的 文本范围和搜索方向
当发现与警报匹配时 就可以停止 此时返回新的 UIAccessibility CustomRotorItem 结果 输入新属性 targetRange 否则就会在 targetRange 空值内通过 以表明处于第一个 还是最后一个转子项目 不过必须仔细确保 targetRange 为 UITextRange 而且 targetElement 要符合 UITextInput 协议 这里面需要理解相当多的信息 但如果我们退一步 就可以看到 app 中 实现自定义转子 为返回上一个或下一个 自定义转子项目的结果 该结果来自用于初始化转子的区块 (用户可以在元素类别内移动) (改善文本内容的辅助功能) AccessibilityCustomRotors 允许我们 从你的 app 中过滤信息 并且只关注特定类别 之前我们看到自定义转子 如何改善一个复杂的基于地图的 app 现在则可看到自定义转子有助于增强 用户与基于文本内容的交互 若要更深入学习如何进一步改善 文本内容的辅助功能 请查看本期课程 创建可访问的阅读体验 (创建可访问的阅读体验 全球开发者大会 19) 我们刚刚学习了旁白 包括两种自定义转子的方法 我希望你利用所学的知识 来审核自己的 app 可访问性 欲了解更多细节 请看上期的 app 测试与旁白 旁白 可视化之外的应用测试 全球开发者大会 18 最后 我有几项建议可让你提高 app 中的旁白效率 首先 找到界面中最为复杂的视觉区域 打开旁白 然后感受一下访问该内容时 是否跟关闭旁白时一样简单自如 如果不是 那这就跟某个没在看屏幕的人 所体验到的感受不相上下 因此 可以考虑添加自定义转子 借助它们的帮助 (为相关元素创建自定义转子) 毕竟 你可花了很多时间 为人们设计了你的 app 所以要确保它对每个人都有效 谢谢大家 愿你们能好好享受 全球开发者大会 2020
-
-
4:04 - mapView.accessibilityCustomRotors = [customRotor(for: .stores), customRotor(for: .parks)]
mapView.accessibilityCustomRotors = [customRotor(for: .stores), customRotor(for: .parks)]
-
4:31 - map rotor 1
// Custom map rotors func customRotor(for poiType: POI) -> UIAccessibilityCustomRotor { UIAccessibilityCustomRotor(name: poiType.rotorName) { [unowned self] predicate in return UIAccessibilityCustomRotorItemResult( ) } }
-
4:56 - map rotor 2
// Custom map rotors func customRotor(for poiType: POI) -> UIAccessibilityCustomRotor { UIAccessibilityCustomRotor(name: poiType.rotorName) { [unowned self] predicate in let currentElement = predicate.currentItem.targetElement as? MKAnnotationView let annotations = self.annotationViews(for: poiType) let currentIndex = annotations.firstIndex { $0 == currentElement } return UIAccessibilityCustomRotorItemResult( ) } }
-
5:04 - map rotor 3
// Custom map rotors func customRotor(for poiType: POI) -> UIAccessibilityCustomRotor { UIAccessibilityCustomRotor(name: poiType.rotorName) { [unowned self] predicate in let currentElement = predicate.currentItem.targetElement as? MKAnnotationView let annotations = self.annotationViews(for: poiType) let currentIndex = annotations.firstIndex { $0 == currentElement } let targetIndex: Int switch predicate.searchDirection { case .previous: targetIndex = (currentIndex ?? 1) - 1 case .next: targetIndex = (currentIndex ?? -1) + 1 } return UIAccessibilityCustomRotorItemResult( ) } }
-
5:17 - Maps rotor 4
// Custom map rotors func customRotor(for poiType: POI) -> UIAccessibilityCustomRotor { UIAccessibilityCustomRotor(name: poiType.rotorName) { [unowned self] predicate in let currentElement = predicate.currentItem.targetElement as? MKAnnotationView let annotations = self.annotationViews(for: poiType) let currentIndex = annotations.firstIndex { $0 == currentElement } let targetIndex: Int switch predicate.searchDirection { case .previous: targetIndex = (currentIndex ?? 1) - 1 case .next: targetIndex = (currentIndex ?? -1) + 1 } guard 0..<annotations.count ~= targetIndex else { return nil } // Reached boundary return UIAccessibilityCustomRotorItemResult(targetElement: annotations[targetIndex], targetRange: nil) } }
-
8:07 - Text rotor 1
// Custom text rotor func customRotor(for attribute: NSAttributedString.Key) -> UIAccessibilityCustomRotor { UIAccessibilityCustomRotor(name: attribute.rotorName) { [unowned self] predicate in var targetRange: UITextRange? // Goal: find the range of following `attribute` let beginningRange = guard let currentRange = else { return nil } switch predicate.searchDirection { } return UIAccessibilityCustomRotorItemResult(targetElement: self, targetRange: targetRange) } }
-
8:20 - Text rotor 2
// Custom text rotor func customRotor(for attribute: NSAttributedString.Key) -> UIAccessibilityCustomRotor { UIAccessibilityCustomRotor(name: attribute.rotorName) { [unowned self] predicate in var targetRange: UITextRange? // Goal: find the range of following `attribute` let beginningRange = self.textRange(from: self.beginningOfDocument, to: self.beginningOfDocument) guard let currentRange = predicate.currentItem.targetRange ?? beginningRange else { return nil } let searchRange: NSRange, searchOptions: NSAttributedString.EnumerationOptions switch predicate.searchDirection { } return UIAccessibilityCustomRotorItemResult(targetElement: self, targetRange: targetRange) } }
-
8:37 - Text rotor 3
// Custom text rotor func customRotor(for attribute: NSAttributedString.Key) -> UIAccessibilityCustomRotor { UIAccessibilityCustomRotor(name: attribute.rotorName) { [unowned self] predicate in var targetRange: UITextRange? // Goal: find the range of following `attribute` let beginningRange = guard let currentRange = else { return nil } let searchRange: NSRange, searchOptions: NSAttributedString.EnumerationOptions switch predicate.searchDirection { case .previous: searchRange = self.rangeOfAttributedTextBefore(currentRange) searchOptions = [.reverse] case .next: searchRange = self.rangeOfAttributedTextAfter(currentRange) searchOptions = [] } return UIAccessibilityCustomRotorItemResult(targetElement: self, targetRange: targetRange) } }
-
9:06 - Text rotor 4 (end)
// Custom text rotor func customRotor(for attribute: NSAttributedString.Key) -> UIAccessibilityCustomRotor { UIAccessibilityCustomRotor(name: attribute.rotorName) { [unowned self] predicate in var targetRange: UITextRange? // Goal: find the range of following `attribute` let beginningRange = guard let currentRange = else { return nil } let searchRange: NSRange, searchOptions: NSAttributedString.EnumerationOptions switch predicate.searchDirection { } self.attributedText.enumerateAttribute( attribute, in: searchRange, options: searchOptions) { value, range, stop in guard value != nil else { return } targetRange = self.textRange(from: range) stop.pointee = true } return UIAccessibilityCustomRotorItemResult(targetElement: self, targetRange: targetRange) } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。