其实很少用户知道,ios系统其实有一套完整的轻松访问机制,很多盲人或者说视障用户都喜欢使用iphone。
所以说,作为一名开发者,我觉得不论是从产品销售面还是作为责任,都应该做好完善的轻松访问支持。
不过好在,得益于苹果严格的开发规范,所以一般只要你的app已经通过审核能够上架,那么基本上 VoiceOver 就已经能够很好的识别你 app 中的大部分内容了,比较通用的,比如 tabView,比如 navigationController,这些通用的框架都已经自动支持了的。现在,我们就一起来看看,如何让你的 app 利用好 VoiceOver 的所有高级功能,这些功能能够让你在一定程度上控制 VoiceOver,让它更好地兼容你的app,让用户体验更佳完美。
主要目标
VoiceOver 除了能够读出屏幕内容外,其实还可以做更多的内容,比如与界面交互,比如内置的魔法轻拍,魔法返回,要让我们自定义的内容支持它,就需要自己去重写对应的方法,还要让我们自己定制的按钮,按照希望的方式来运作——毕竟使用 VoiceOver 的用户是靠听的,所以有时候我们还需要为按钮等加上更多解释,甚至,在一些动作之后,还要让 VoiceOver 读出特定的内容来告知用户都发生了什么,把这些都规范了,那么视障用户也能轻松地使用你开发的 app 了。
VoiceOver 运行状态
要对 VoiceOver 进行支持,那么得先要能发现它的状态——虽然其实大部分情况下你不需要这么做,因为你在对 VoiceOver 做多配置,如果 VoiceOver 不开启的话,就不会有任何影响,但如果你希望检测一下的话,在 UIKit 里有对应的全局函数:
1 |
var isVoiceOverOn:Bool {return UIAccessibilityIsVoiceOverRunning()} |
当然,你也可以不用像我这样在封装一层,我是为了自己整体代码看起来更一致罢了。
设定可读
对于一个自定义的控件,你需要手动地来为它指定是否要作为一个 VoiceOver 控件:
1 |
someButton.isAccessibilityElement = true |
控件角色
ios 为控件提供了一些常见的角色可以配置,比如按钮,比如音量键,为你的控件设定合适恰当的控件角色,这有助于 VoiceOver 为你的控件提供对应的额外功能。
举例来说,对于键盘按钮,就要给按钮去掉按钮角色而使用键盘按钮角色,只有这样,用户才能实现用手指在键盘上滑动,然后找到需要的按钮时,松开手指立即执行按下的操作。
1 |
accessibilityTraits = UIAccessibilityTraitKeyboardKey |
当然,其实这些角色可以是多个,我们通过最后的声明可以看到这个角色类型其实就是一个大的整数: public typealias UIAccessibilityTraits = UInt64 所以我们可以这样:
1 |
accessibilityTraits = UIAccessibilityTraitKeyboardKey | UIAccessibilityTraitButton |
这里我顺便为大家翻译一下声明里的翻译,具体的特性需要大家自己去尝试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
// 不使用角色,在某些情况下很有用,比如你想自己处理它的时候,落格输入法的候选字,既想让它有按钮的动作,又不想让它读出来“按钮”这个烦人的提示,那么就用这个了。 public var UIAccessibilityTraitNone: UIAccessibilityTraits // 如果是系统拖的按钮,那么默认肯定就是这个。如果你给按钮的 VoiceOver title 设定的名字是xx按钮,那么VO就不会给你追加“按钮”俩字。 public var UIAccessibilityTraitButton: UIAccessibilityTraits // 控件被用于链接的时候使用 public var UIAccessibilityTraitLink: UIAccessibilityTraits // 控件作为内容的标题的时候使用,比如说导航栏的标题。 @available(iOS 6.0, *) public var UIAccessibilityTraitHeader: UIAccessibilityTraits // 搜索栏 public var UIAccessibilityTraitSearchField: UIAccessibilityTraits // 图片,可以和按钮、链接之类的混合使用。 public var UIAccessibilityTraitImage: UIAccessibilityTraits /* 被选中的控件。 比如说tableview里被选中的那一行。 */ public var UIAccessibilityTraitSelected: UIAccessibilityTraits // 当控件自身可以播放声音的时候 public var UIAccessibilityTraitPlaysSound: UIAccessibilityTraits // 当控件是键盘按钮的时候 public var UIAccessibilityTraitKeyboardKey: UIAccessibilityTraits // 不能改变的静态文本 public var UIAccessibilityTraitStaticText: UIAccessibilityTraits /* 当作为提供当前情况快速总结的时候。 比如说,一个天气应用,第一次打开时当天的天气情况就可以标记为这个。 */ public var UIAccessibilityTraitSummaryElement: UIAccessibilityTraits // 控件不可用的时候 public var UIAccessibilityTraitNotEnabled: UIAccessibilityTraits /* 当控件要频繁地更新其label或者值的时候 比如说秒表。 */ public var UIAccessibilityTraitUpdatesFrequently: UIAccessibilityTraits /* 当控件启动不应该被VO干扰的媒体会话的时候(比如说播放电影,录音) */ @available(iOS 4.0, *) public var UIAccessibilityTraitStartsMediaSession: UIAccessibilityTraits /* 当控件可以被“调节”的时候,比如说滑动条。 控件也要实现 accessibilityIncrement 和 accessibilityDecrement. */ @available(iOS 4.0, *) public var UIAccessibilityTraitAdjustable: UIAccessibilityTraits // 当控件需要直接与用户交互的时候使用,比如说钢琴键盘。 @available(iOS 5.0, *) public var UIAccessibilityTraitAllowsDirectInteraction: UIAccessibilityTraits /* 通知VO完成阅读时要滚动到下一个页面。VO会调用 accessibilityScroll: with UIAccessibilityScrollDirectionNext 并且在检测到内容不变时停止。 */ @available(iOS 5.0, *) public var UIAccessibilityTraitCausesPageTurn: UIAccessibilityTraits /* Used when a view or accessibility container represents an ordered list of tabs. The object with this trait should return NO for isAccessibilityElement. */ @available(iOS 10.0, *) public var UIAccessibilityTraitTabBar: UIAccessibilityTraits |
魔法轻拍
这是一个很常用的手势,就是双指在屏幕上双击。比如说在落格输入法中,它用来快速返回第一个候选(当你往后翻了很久的时候)。如果你想监听这个动作,就在对应的 Controller 里重写 accessibilityPerformMagicTap :
1 2 3 |
override func accessibilityPerformMagicTap() -> Bool { ... } |
返回 true 来告诉系统,VoiceOver 接收了操作,返回 false 来说明跳过,将内容发送给其他接受者。
魔法返回
魔法返回的手势有点难了,但稍微练习一下应该也不是什么难事,总之,在能够返回的地方比如 navigationController 中双指左右来回一下就是执行了一次魔法返回。还是用落格输入法举例子,那么就是在键盘上执行魔法返回来收起键盘:
1 2 3 4 |
override func accessibilityPerformEscape() -> Bool { dismissKeyboard() return true } |
手动发出提示
那么,怎么才能主动给用户发出内容提示呢?比如“进入表情键盘”?那么你可以这样:
1 |
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, "进入表情键盘"); |
那么VO就会在你调用它发送通知后读出你的内容,所以,记得要做本地化。
同时,值得一提的是,如果你同时发送多条通知,那么后面的通知会把前边的通知给覆盖掉!比如这样:
1 2 3 |
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, "进入表情键盘"); UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, "进入符号键盘"); UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, "进入拼音键盘"); |
那么你得到的很可能是“进入表情键盘”或“进入拼音键盘”,其它的两个就丢失了。
还有比如说在落格输入法之前的版本中,一直存在一个bug,那就是VO里无法读出删除的字。这对于视障用户来说是一件很重要的问题,因为如果你不读出来的话他们就不能确定自己删除了啥,那可真是糟糕透了。而且比这还糟糕的是,我自己本地测试是完全正常的!
所以说,一定要注意这个问题,发送通知不需要一定要在主线程(如果你和我一样常用并发的话),但一定要注意间隔,经过分析,最终我才明白,当你在键盘上点击了删除,那么作为一款第三方键盘,我必须调用内置的api来删除,可是这个删除会触发UI改变(因为文字变了对吧?)VoiceOver就会去傻傻地报告光标位置,显然,一般输入错了肯定是最后一个字居多,那么自然bug就成了读“文本底部”。
这时候,只需要比系统的通知慢就好了,(系统通知比你发的要慢一点,所以它老是覆盖我的通知即使我把它放在了删除调用之后),所以,我这样处理了:
1 2 3 |
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, String(word.characters.last!)); }) |
总之,慢这么0.1秒对用户来说他们发现不了什么区别,但对于系统来说,那就是有了足够的时间让VO在收到系统的通知后,还来不及读出就被你的通知给覆盖掉了。
手动改变焦点
当用户点了键盘,或者说做了一些操作之后,我们需要让VO自动将焦点移动到上边,典型的情况就是你在点击了键盘按钮后,候选栏会更新新的候选词,这个时候讲焦点移动到第一个候选就会很好用。因为这样的话,用户就可以双击屏幕直接选中它了。所以,我这样做:
1 |
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, self); |
第二个实际参数就是要获得焦点的控件引用,这样收到通知后,VO会立即将焦点移动到这个空间上并读出它的内容。
总结
基本上来说,对于 VoiceOver 的测试,你是无法在 xcode 中完成的,它所附带的工具仅仅能够支持最简单的检查,想要完整地测试你的VO支持,你应该把app安装到你的 iphone 上,然后打开VoiceOver,闭上眼睛去亲自使用一番,如果不擅长 VoiceOver 的你都能顺利地使用你的app,那应该就没什么大问题了。
以上这些,就是我把落格输入法做成 ios 平台 VoiceOver 支持最好的中文输入法的人生经验了。?
本文由 落格博客 原创撰写:落格博客 » ios 为视障用户支持 VoiceOver
转载请保留出处和原文链接:https://www.logcg.com/archives/2322.html
Comments