VoiceOver是苹果“读屏”技术的名称,属于辅助功能的一部分。VoiceOver可以读出屏幕上的信息,以帮助盲人进行人机交互。 这项技术在苹果的各个系统中都可以看到,OS X,iOS,watchOS,甚至tvOS。 苹果公司的VoiceOver在2015年6月18日获得了美国盲人基金会(American Foundation for the Blind, AFB)颁发的海伦凯勒成就奖,成为全球首家获得此殊荣的科技公司。 单从iOS来说,iOS的VoiceOver功能可以毫不夸张的说是三大移动平台中做的最好的。

虽然说苹果默认的UI组件都已经默认支持VoiceOver功能了,但是通常情况下App还是需要对VoiceOver进行适配和优化的,比如说一些自定义复杂UI组件。

基本使用

iPhone上开启VoiceOver功能后,就可以通过单指左右轻扫来遍历当前界面中的所有的AccessibilityElement(可以被VoiceOver访问的UI元素),当一个AccessibilityElement被选中后,VoiceOver会将AccessibilityElement的信息读出来。单指轻点两次能够激活当前元素对应的操作,比如当前AccessibilityElement是一个按钮,那么对应的就是按钮的Action事件。

简单点来说在App开发过程中关于VoiceOver我们需要关注如下几点:

  • 界面上的AccessibilityElement有哪些

  • AccessibilityElement的位置和形状

  • AccessibilityElement的信息是什么(就是Element被选中后,被读出来内容)

  • AccessibilityElement所能响应的的事件有哪些

UIKit中的控件基本都是 VoiceOver Ready的,即使是UIView,你也可以通过简单的设置其实变成AccessibilityElement。所以这一小节中所讲的AccessibilityElement其实都是UIView或其子类的实例。

相关属性和方法基本都在UIAccessibility.h这个头文件中进行了声明。

所以简单的情况下通过UIView的isAccessibilityElement属性就可以控制某个View是否是AccessibilityElement,在UIKit的控件中,像UILabel,UIButton 这些控件的isAccessibilityElement属性默认就是true的,UIView这个属性默认是false。

一般情况下AccessibilityElement的位置和形状是通过accessibilityFrame进行设置的,默认值是View在屏幕中的位置,形状就是View的矩形形状。如果你想自己设置accessibilityFrame的值,那么得注意下,这边的frame值是相对于设备Screen的坐标系的,当然可以通过UIAccessibilityConvertFrameToScreenCoordinates函数来帮助转换。此函数有两个参数,一个rect,一个是view, 其含义就是将相对于view这个坐标系的rect转换成相对于screen坐标系的值并返回。所以一般情况下 rect可以是目标Element在父View中的frame,view就为其父view。

public func UIAccessibilityConvertFrameToScreenCoordinates(rect: CGRect, _ view: UIView) -> CGRect

如果你想设置非矩形的形状,你也可以通过给accessibilityPath属性指定一个UIBezierPath类型的值来自定义AccessibilityElement的形状。

至于AccessibilityElement的信息可以通过下面几个UIAccessibility的属性来决定

  • accessibilityLabel 这是什么

  • accessibilityHint 这个有什么用,会产生什么样的结果

  • accessibilityValue 这个的值是什么

  • accessibilityTraits 这个的类型以及状态,就是通过traits来表征这个Element的特质,数据类型是一个枚举类型,可以通过按位或的方式合并多个特性。

这里有个需要注意的就是,当某个View的是AccessibilityElement的时候 ,其subviews都会被屏蔽掉,这个特性有时候还是有用的,比如一个View中包含多个Label,那么你希望每一个下面的Label不要单独可以访问到,那么你可以将这个View设置成可以访问的,然后将其accessibilityLabel设置为所有子Label的accessibilityLabel的合并值。

至于AccessibilityElement的事件,最简单的莫过于上面提到单指轻点两次能够激活当前元素对应的操作了,如果当前AccessibilityElement实现的public func accessibilityActivate() -> Bool这个方法返回true,那边此逻辑将被调用,否则相当于在AccessibilityElement的accessibilityActivationPoint这个位置点上进行了一次Tap操作。

高级特性

#### Accessibility Container 设想下这样的一个场景,一个UIView,内部包含一组用户可以进行交互的内容,每一个内容之间是独立的,但是这些内容不是以子View的形式存在,而是通过Quarz 2D或者Core Text渲染而成,所以这部分内容无法通过上面的方式变成AccessibilityElement。这种情况UIView需要按照UIAccessibilityContainer的方式,来将内部的每一个独立的内容都描述成UIAccessibilityElement的实例,这个时候这个UIView我们称之为Accessibility Container。

接下来讲解具体的实现步骤了。 先说说iOS 8之后如何实现,首先Accessibility Container的isAccessibilityElement值必须设置为false,另外我们需要创建出所有的UIAccessibilityElement的实例,然后赋给accessibilityElements属性(iOS 8.0+) 假设在一个UIView的子类中,通过叫做updateAccessibleElements的方法来更新维护所有的UIAccessibilityElement实例,当界面上的内容发生变化,或者VoiceOver开启关闭状态发生变化时,调用此方法以更新accessibilityElements,相关伪代码如下:

    internal func updateAccessibleElements() {
       guard UIAccessibilityIsVoiceOverRunning() else {
           self.accessibilityElements = nil
           return
       }
       
       self.isAccessibilityElement = false
       var elements = [AnyObject]()        let element1 = UIAccessibilityElement(accessibilityContainer: self)
       element1.accessibilityLabel = "element1"
       element1.accessibilityTraits = UIAccessibilityTraitStaticText
       element1.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(element1FrameInSelf, self)
       elements.append(element1)
       ...
       self.accessibilityElements = elements
   }

如果是iOS 8以下,那么就需要实现下面的几个方法来实现了,比较繁琐:

// accessibilityElement的个数
public func accessibilityElementCount() -> Int // 返回指定Index的accessibilityElement
public func accessibilityElementAtIndex(index: Int) -> AnyObject? // 返回指定accessibilityElement的Index
public func indexOfAccessibilityElement(element: AnyObject) -> Int

使用上面第二种方式的时候,往往需要自己维护一个包含所有accessibilityElements的数组,然后通过数组来完成上面的这三个方法的返回。这种方法比较累赘,而且当界面更新需要刷新内容的时候,还需要发送通知系统,告诉系统界面上的accessibilityElements有变动需要更新。所以最小版本定位于iOS 8的情况下还是直接设置accessibilityElements属性的方式比较科学。

关于UIAccessibilityElement这个类的设计还是存在一些疑惑的,既然NSObject的UIAccessibility扩展已经包含了诸如accessibilityLabel,accessibilityHint这些属性,为何UIAccessibilityElement类中还需要重复声明这些属性,有点重复的感觉。

Actions

之前只讲到了最简单的事件,就是单指轻点两下,其实常见的Actions有下面这些,每一个Action都会对应一个方法,可以通过覆盖方法的方式来自定义Action对应的逻辑:

  • Activate 单指轻点两次 
    public func accessibilityActivate() -> Bool

  • Escape. 单指 Z-shaped 手势一般用于退出模态界面或者返回导航的上一页界面 
    public func accessibilityPerformEscape() -> Bool

  • Magic Tap. 双指轻点两次触发 most-intended action. 
    public func accessibilityPerformMagicTap() -> Bool

  • Three-Finger Scroll. 三指滑动触发界面水平或者垂直的滚动 
    public func accessibilityScroll(direction: UIAccessibilityScrollDirection) -> Bool

  • Increment. 单指向上滑动,需要设置accessibilityTraits为UIAccessibilityTraitAdjustable,否则对应的方法不会被调用 
    public func accessibilityIncrement()

  • Decrement. 单指向下滑动,需要设置accessibilityTraits为UIAccessibilityTraitAdjustable,否则对应的方法不会被调用 
    public func accessibilityDecrement()

这些方法中,其中Escape,Magic Tap,Three-Finger Scroll这几种手势支持在响应链中向上寻找对应的Action方法,首先用户在屏幕上进行相应的手势,系统检查当前VoiceOver的Focus的Element有无实现对应的方法,没有实现的话,则向响应链的上一级寻找。比如有些全局性的操作,对应的方法写在上层View或者ViewController中比较合适。 其实很多系统提供的组件都默认实现了一些VoiceOver的手势Action,比如UINavigationController, UIAlertController 都提供了对Escape手势的支持。

Accessibility Notification

Accessibility提供了一系列的通知,可以完成一些特定的需求。比如你可以监听UIAccessibilityVoiceOverStatusChanged通知,来监控Voice Over功能开启关闭的实时通知 

或者是你在App中主动发送一些通知,来让系统做出一些变化,比如当你界面上的AccessibilityElement有变动的时候你可以发送UIAccessibilityLayoutChangedNotification通知,通知发送时使用UIAccessibility的专用的函数,UIAccessibilityPostNotification函数有两个参数,第一个是通知名,第二个是你想让VoiceOver读出来的字符串或者是新的VoiceOver的焦点对应的元素。

UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, self.myFirstElement)

建议

  • AccessibilityElement的信息尽量简洁,accessibilityLabel不要包含提示性质的文案,避免信息干扰

  • TableView的每一个Cell的信息尽量合并,使得Cell变成一个整体的AccessibilityElement,避免无意义的冗余元素之间切换的操作。Cell中有多个按钮的时候,可以考虑使用Magic Tap的方式,Magic Tap的Action中弹出sheet样式的UIAlertController来供用户操作。

  • 自定义的模态页面注意设置accessibilityViewIsModal为true,最好支持Escape手势 的方式退出模态页面。

  • 将页面中装饰用的没有实际意义的元素的accessibilityElementsHidden设置成true,减少操作过程中的干扰。

iOS VoiceOver Programming Guide的更多相关文章

  1. View Controller Programming Guide for iOS---(一)---About View Controllers

    About View Controllers View controllers are a vital link between an app’s data and its visual appear ...

  2. 【IOS笔记】View Programming Guide for iOS -1

    原文:View Programming Guide for iOS View and Window Architecture Views and windows present your applic ...

  3. Table View Programming Guide for iOS---(一)---About Table Views in iOS Apps

    About Table Views in iOS Apps Table views are versatile user interface objects frequently found in i ...

  4. View Programming Guide for iOS ---- iOS 视图编程指南(五)---Animations

      Animations Animations provide fluid visual transitions between different states of your user inter ...

  5. View Programming Guide for iOS ---- iOS 视图编程指南(四)---Views

    Views Because view objects are the main way your application interacts with the user, they have many ...

  6. Collection View Programming Guide for iOS---(一)----About iOS Collection Views

    Next About iOS Collection Views 关于iOS Collection Views A collection view is a way to present an orde ...

  7. View Programming Guide for iOS ---- iOS 视图编程指南(三)---Windows

    Windows Every iOS application needs at least one window—an instance of the UIWindow class—and some m ...

  8. View Programming Guide for iOS ---- iOS 视图编程指南(二)---View and Window Architecture

    View and Window Architecture 视图和窗口架构 Views and windows present your application’s user interface and ...

  9. View Programming Guide for iOS ---- iOS 视图编程指南(一)

    Next About Windows and Views 关于窗口和视图 In iOS, you use windows and views to present your application’s ...

随机推荐

  1. OLEDB和ODBC的区别(优缺点)

    ODBC是一种连接数据库的开放标准,OLEDB(对象链接和嵌入数据库)位于ODBC层与应用程序之间. 在你的ASP页面里,ADO是位于OLEDB之上的应用程序. 你的ADO调用先被送到OLEDB,然后 ...

  2. Oracle EBS 预警系统管理

    本章主要讲述配置和设置Oracle EBS预警系统管理, 它比较方便和及时发用户或系统对数据库操作情况.下面讲一操作步聚: 1.预警系统管理-->系统-->选项 名称"Unix ...

  3. Java 数组在内存中的结构

    Java中的数组存储两类事物: 原始值(int,char,...),或者引用(对象指针). 当一个对象通过 new 创建,那么将在堆内存中分配一段空间,并且返回其引用(指针). 对于数组,也是同样的方 ...

  4. Cookie的前后台应用

    1.jquery.cookie.js的基本应用 这个是第三方js插件,可以更方便的设置和使用cookie $.cookie("UserName", "kingtiger& ...

  5. C# XML序列化操作菜单

    鉴于之前写的一篇博文没使用XML序列化来操作菜单,而且发现那还有一个问题,就是在XML菜单的某个菜单节点前加上一些注释代码的就不能读取,现在使用XML序列化后可以很方便的读取,故在此写一写.   XM ...

  6. 找出Java进程中大量消耗CPU

    原文:https://github.com/oldratlee/useful-shells useful-shells 把平时有用的手动操作做成脚本,这样可以便捷的使用. show-busy-java ...

  7. linux下的rbenv和rails安裝

    今天是周一,我到新公司的第14天,今天继续linux下ruby和rails环境变量的配置. 首先碰到的问题是 主要看ubuntu下安装rbenv,ruby,rails开发环境, http://blog ...

  8. HDU-4414 Finding crosses 水题

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4414 直接暴力判断即可. //STATUS:C++_AC_15MS_232KB #include &l ...

  9. json里的日期字符串 怎么 转换成 javascript 的 Date 对象?

    “/Date(1232035200000)/” 怎么转换成  javascript 的 Date 对象 做法:new Date(+/\d+/.exec(value)[1]); value就是json字 ...

  10. Xamarin开发Android时Visual Studio 2012没有智能提示解决办法

    Most of the people who work with Xamarin’s Mono for Android in Visual Studio 2012 face a bug where I ...