本文由CocoaChina译者呆萌院长(博客)翻译自苹果开发者中心
hyhSuper(GitHub)
本文档文章包括
- Apple TV 编程指南:介绍
- Apple TV 编程指南:创建Client-Server App
- Apple TV 编程指南:使用Apple TV遥控器控制屏幕上的用户界面
- Apple TV 编程指南:检测手势和按钮按压
- Apple TV 编程指南:设计键盘输入体验
- Apple TV 编程指南:使用游戏控制器
- Apple TV 编程指南:创造视差美术资源
- Apple TV 编程指南:资源管理
使用Apple TV遥控器控制屏幕上的用户界面
在iOS设备上,用户通过触屏直接进行交互。在苹果电视上,用户使用遥控器进行间接交互。用户浏览到一个特定的选项上,然后按下遥控器上一个按钮来选择这个选项。当用户选中屏幕上的选项时,该选项就会变成焦点。焦点是指用户间接地通过远程的或者其他的输入设备进行的输入行为引起的屏幕的外部效果。在一个以焦点为基础的交互模型中,在屏幕上的单一视图可以得到焦点,并且用户可以通过浏览屏幕上不同的UI选项将焦点移动到其他视图,从而引起焦点更新。得到焦点的视图被用作任何用户操作的目标事件。例如,如果一个屏幕上的按钮被选中,当由遥控器发送按钮选择事件时,目标事件将被触发。
UIKit框架只支持以焦点为基础的交互,在大多数情况下,这种行为只在有意义的地方自动提供。你可以要求以编程形式焦点更新,但不能将焦点往某个方向设置或者移动。例如,UIButton对象有焦点,但UILabel对象都没有。对于自定义用户界面组件的应用程序,你需要实现自定义的焦点目标事件,正如Supporting Focus Within Your App中解释一样。UIKit库中,如UIButton,UITextField,UITableView,UICollectionView,UITextView,UIsearchbar 以及UISegmentedControl有默认焦点。
焦点引擎控制焦点
包含可控制焦点的UIKit的系统和焦点移动称为焦点引擎。有许多用户可以控制焦点的输入设备,例如(不同类型)遥控器,游戏控制器,模拟器等等。在你的应用程序中,焦点引擎监听来自这些不同输入设备的焦点移动事件。当一个事件发生时,焦点引擎会自动判断更新焦点并通知你的应用程序。该系统有助于创造一个持续性的用户体验,即提供了每一个应用程序中所有当前和未来的输入方法的自动支持,并帮助开发人员专注于实现他们的应用程序的独特行为而不是定义或重塑基本的导航。
只有焦点引擎才能显式更新焦点,这意味着没有一个可直接设置焦点的视图或移动焦点往一个特定方向的接口。如果用户发送一个移动事件,如果焦点引擎系统要求更新,或者如果应用程序请求更新,该焦点引擎只更新焦点。要了解更多关于如何手动更新的焦点,请参看 Updating Focus Programmatically 一节。
焦点引擎控制主要是确保焦点不在屏幕上意外地移动,并且它的行为在不同的应用程序中是相似的。这有助于防止用户对不同的行为感到困惑,同时意味着开发人员不必自己定制适当的焦点管理解决方案。
UIFocusEnvironment协议
焦点引擎通过UIFocusEnvironment协议,与你的应用程序进行通信。 UIFocusEnvironment 协议定义了视图层次结构的一个分支的焦点行为。UIKit库中遵循这个协议的类有UIView、UIViewController、UIWindow 和 UIPresentationController,换句话说,这些类都直接或间接地控制在屏幕上的视图。在视图和视图控制器中重写UIFocusEnvironment协议方法,你可以在应用程序中控制焦点行为。
用户生成的焦点移动
焦点引擎可以根据遥控器或者其他输入设备的响应事件自动决定焦点应该移动到哪里。用户可以将焦点转移到任何二维方向:上下左右或对角线(如果硬件支持的话)。例如,如果用户左滑,焦点引擎试图直接找到在当前获得焦点的视图左边的一个视图,这个视图必须具有可以获得焦点的特性。如果一个新的视图被发现,则焦点移动到该视图;否则,焦点停留在当前激活的视图。
如果输入设备支持,焦点引擎也自动处理复杂的行为。例如,基于运动的远程滑动,根据焦点速度调节焦点相关的动画速度,播放导航的声音,和当焦点移出屏幕,时更新滚动视图偏移。要了解更多关于焦点相关的动画,请参看 UIFocusAnimationCoordinator类参考。
决定在哪里移动焦点
当决定如何按照用户动作移动焦点时,焦点引擎需要应用程序内部的用户界面和所有的具有可获得焦点特性的所有视图的可视区域。这意味着,如果一个可获得焦点的视图在完全在另一个可获得焦点视图的下面,那么它将会被忽略,不能得到焦点。也就是说部分区域隐藏的视图,只有可见部分才能获得焦点。利用这一技术,焦点引擎从当前获得焦点的视图开始,在运动路径上直接找到可以获得焦点的区域。搜索区域的大小与当前焦点视图的大小直接相关。
图 3-1焦点移动
如果焦点引擎找到一个新的视图去移动焦点,那么它会在移动焦点发生之前给你的应用程序一个验证移动的机会。操作系统在每个包含上一个和下一个焦点视图的焦点环境上都会调用 shouldUpdateFocusInContext:方法。先通知先前的焦点视图,然后是焦点视图,最后通知父视图。如果所有焦点环境 shouldupdatefocusincontext:的返回值为NO,那么这个焦点移动将会被取消。要了解更多关于焦点环境的知识,参见 UIFocusEnvironment Protocol Reference.
初始焦点和首选焦点链
当一个应用程序启动时,焦点引擎决定一个默认的焦点视图获得焦点。这个视图通常是距屏幕左上角最近的一个可获得焦点的视图。
图 3-2分层搜索的初始视图,以获得焦点。
但是,使用 UIFocusEnvironment 协议的首选焦点视图,可以为你的应用程序提供默认情况下焦点该去哪的线索。当设置初始化焦点时,焦点引擎首先询问 window 的首选的焦点视图,这个视图就是根视图控制器的 preferredFocusedView 对象。因为返回值是一个符合 UIFocusEnvironment 协议的 UIView 对象,所以焦点引擎询问其首选的焦点视图,以次类推。这个由一系列作为返回值的首选焦点视图组成的链表是首选焦点链。焦点引擎遵循这个首选焦点链,直到得到的焦点视图是它本身或者是nil。从首选焦点链中的最后一个列表中选择位置最深处的视图作为下一个可获得焦点的视图。
下面这个例子展示什么是首选焦点链:
- 焦点引擎向根窗口查询 preferredFocusedView,并返回它的根视图控制器下的 preferredFocusedView。
- 根视图控制器是选项卡视图器的时候,返回值是它的被选者的视图控制器的 preferredFocusedView 对象。
- 被选择视图控制器重写其 preferredFocusedView 方法,返回一个特定的 UIButton 实例对象。
- 这个 UIButton 实例对象返回 self(默认值),并可获得焦点,所以它被焦点引擎选择作为下一个焦点视图。
正如首选焦点链所定义的那样,每当焦点在一个指定的视图更新时,新的焦点视图都会被设定为首选焦点视图。焦点链的另一个例子,当有一个视图控制器呈现在当前焦点视图的上面时,焦点引擎会使用它的首选焦点链更新焦点到新的视图控制器上。
焦点更新
当用户行为引起了焦点运动时就会引起焦点更新(例如,在遥控器上滑动),这时应用将会请求更新焦点或者系统自定触发更新焦点。
焦点更新的解析
当焦点更新或者焦点移动到一个新的视图的时候,无论焦点视图是子视图或者不同于这个视图层级的部分,都将会有下列事情发生:
- focusedView 属性被更新,并且更新的效果会反应到最新的到焦点的视图或者首选焦点视图上。
- 焦点引擎使用didUpdateFocusInContext:withAnimationCoordinator:方法通知每一个焦点环境,包含先前焦点视图或下一个焦点视图(焦点更新后的视图)的焦点环境。焦点更新时,系统会使用动画协调员安排焦点更新相关的动画。详见 UIFocusAnimationCoordinator。
- 在所有相关的焦点环境得到通知后,所有的协调动画都在同一时间运行。
- 如果下一个焦点视图是在一个滚动视图上,并且在屏幕以外,那么滚动视图将会滚动到该视图所在的位置,使它移进屏幕内。
系统生成的焦点更新
在许多常见的情况下,必要的时候UIKit会自动更新焦点。下面的几个例子展示了什么情况下系统会自动的更新焦点:
- 从视图层次结构中移除一个焦点视图。
- UITableView 或 UICollectionView 重新加载数据。
- 在当前获得焦点的视图上展示一个新的视图控制器。
- 用户按下遥控器上的菜单回退。
以编程方式更新焦点
通常情况下,焦点会在必要的时候自动更新,但是有时候,你需要在应用程序内部,用代码主动引起焦点更新。任何焦点环境通过调用 setNeedsFocusUpdate 方法请求焦点更新,这个方法重置了焦点环境的 preferredFocusedView。下面是一些何时可以在你的应用内部主动的更新焦点的例子:
- 如果应用程序的内容发生变化,那么为了留在用户所期望的地方,则需要改变焦点。例如:音乐类应用程序总是希望专注当前播放的歌曲。当一首歌曲结束时,应用程序应该请求更新的焦点,并移动到播放列表中得下一首歌曲。
- 用户期望焦点转移到某个新的地方的操作行为。例如:一个应用程序的界面由两个部分组成,左边是一个菜单,右边是对应菜单项的内容集合。当用户在菜单项之间移动时,右边的内容更改。当用户选中一个菜单项时,他们可能会期待焦点自动移动到选定的菜单项集合中的第一项。
- 得到焦点的自定义的焦点控件,更新它的内部状态时需要以某种方式改变焦点。例如: 当用户在遥控器上选定选取器件时,焦点会移动到这个控件上,让用户选取选取器上的一系列选项。按下遥控器上的菜单又会导致改控件获得焦点。在这种场景下,通过 preferredFocusedView,选择控件会发出请求更新焦点移动到它的一个子视图上。
让你的APP支持焦点
如果你的应用程序使用内置 UIKit 控件,你的应用程序什么都不用做就可以支持焦点:在应用程序启动时,屏幕上的一个可获得焦点的视图会被选择作为最初的焦点视图,在应用程序的运行周期中,焦点引擎负责管理焦点。
然而,对于大多数应用程序来说,都希望实现某些自定义的焦点行为,例如,当焦点改变时,更新应用程序状态,创建新的用户交互界面元素类型,并实现自定义的焦点动画。下面的章节概述了在你的应用程序中实现支持自定义焦点行为需要做什么。
对一个视图层级结构的分支来说 UIFocusEnvironment 类控制焦点相关行为。这意味着控制器控制着它的根视图和子节点视图的焦点相关行为,并且 UIView 类型对象控制它自己和它子节点的焦点行为。因此,多个焦点环境,可以控制同一个视图层级结构的分支的焦点相关行为。视图层级结构分支是指视图不但包含其他视图,而且还在视图控制器中。控制焦点相关行为并不是意味着某个视图获得焦点,而是意味着焦点环境可以控制焦点在它的视图层级上如何移动,在焦点环境下的视图层级上焦点改变后,UI如何应对。
在视图控制器中支持焦点
因为 UIViewController 遵循 UIFocusEnvironment 协议,所以在你的应用程序中,自定义视图控制器可以重写 UIFocusEnvironment 委托方法来实现自定义的焦点行为。自定义视图控制器可以实现如下功能:
- 重写 preferredFocusedView 方法指明焦点默认开始的视图。
- 重写 shouldUpdateFocusInContext: 方法来定义焦点允许移动的位置。
- 在更新你的应用程序的内部状态或者焦点更新发生时,重写 didUpdateFocusInContext:withAnimationCoordinator: 方法来对焦点更新。
通过调用 setNeedsFocusUpdate 方法,你的视图控制器可以请求焦点引擎为当前的 preferredFocusedView 重置焦点。注意,只有视图控制器包含当前焦点视图,调用setneedsfocusupdate 才有效果。
在Collection视图和Table视图中支持焦点
当使用 collection 视图和 table 视图工作时,你可以使用委托对象来定义任何自定义行为。这种模式也可以基于焦点交互界面实现时使用。UITableViewDelegate 和UICollectionViewDelegate 协议声明的方法和属性和 UIFocusEnvironment 协议提供的属性和方法相似,但是 tableView 和 collectionView 的协议是规范他们自己的行为的。
在collection视图和table视图中支持焦点的提示:
- 使用 UICollectionViewDelegate 的 collectionView:canFocusItemAtIndexPath: 方法或者 UITableViewDelegate 的 tableView:canFocusRowAtIndexPath: 方法去检查某个特殊的 cell 是否需要焦点。这一行动的工作类似于在一个自定义视图中重写 UIView 的 canBecomeFocused 方法。
- 当焦点离开当前视图时,使用在 UICollectionView 和 UITableView 中都有定义的 remembersLastFocusedIndexPath 属性,检查焦点是否应该返回上一个焦点索引, 指定焦点是否应返回焦点离开时的最后一个焦点索引路径,然后焦点重新进入 collectionView 或者 tableView 视图。
在自定义视图中支持焦点
像 UIViewController 和 UIView 类也遵循 UIFocusEnvironment 协议,这意味着 Supporting Focus in View Controllers文档中概述的所有的功能都可以应用到自定义的视图中。但是,因为视图可以获得焦点,所以在你实现自定义的焦点行为的时候,需要考虑下面的几个因素:
- 如果你的自定义视图需要焦点,重写 canBecomeFocused 方法并设置返回值为 YES(默认情况下,它的返回值为NO)。你的视图可能经常得到焦点或在某种情况下才能得到焦点。例如,UIButton 对象的 enable 属性为 NO 的时候,它不能得到焦点。
- 如果聚焦这一观点必须重新聚焦到另一个视图(例如,一个子视图),选择重写 preferredFocusedView。
- 更新你的应用程序的内部状态或者焦点更新发生时,重写 didUpdateFocusInContext:withAnimationCoordinator:方法作为焦点更新后的反应行为。
协调焦点相关动画
当焦点发生更新时,当前视图以某种动画的形式过度到焦点状态,之前的焦点视图以某种动画形式过度到非焦点状态,焦点更新后获得焦点的视图将以某种动画形式过度到焦点状态。然而,这些动画和你应用程序中定义的常见的动画不同,为了实现某种系统级别的行为,UIKit 使与焦点相关的动画的曲线轨迹和运动时间相适应。例如,当焦点正在快速移动时,动画的时间会缩短,以跟上用户的移动。
UIKit提供系统定义的有关可以获得焦点的视图类的焦点动画。使用 UIKit 的内置 UIFocusAnimationCoordinator 类和 addCoordinatedAnimations:completion: 方法,可以创建带有系统级行为的自定义动画。
根据协调器提供了的焦点环境,被添加到协调器中的动画可以作为正在获取焦点的动画中的边缘动画,或者失去焦点中得动画,或者两者都不是。一般情况下,获得焦点和失去焦点的父视图,和特殊的焦点视图的父视图一样,将会沿着获得焦点的方向做动画。非焦点视图独有的父视图将会做失去焦点的动画。
图 3-3自定义焦点动画
通常,视图拥有不同的动画,取决于他们正在得到焦点或失去焦点。通过重写视图的 didUpdateFocusInContext:withAnimationCoordinator: 方法和检查视图的当前焦点的状态的上下文,你可以指定视图需要哪种动画。下面是重写 didUpdateFocusInContext:withAnimationCoordinator: 方法的例子。
- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator
{
[super didUpdateFocusInContext:context withAnimationCoordinator:coordinator];
if (self == context.nextFocusedView) {
[coordinator addCoordinatedAnimations:^{
// focusing animations
} completion:^{
// completion
}];
} else if (self == context.previouslyFocusedView) {
[coordinator addCoordinatedAnimations:^{
// unfocusing animations
} completion:^{
// completion
}];
}
}
非焦点和焦点视图共同的父视图,可以为焦点视图和非焦点视图一起关联相关动画。例如,一个 UICollectionView 对象可以将之前获得焦点的cell以及将要获得焦点的 cell 一起做动画。在对 UICollectionViewCell 的子类情况下,推荐使用上面代码片段的相似逻辑,并在子类中实现动画代码。
调试焦点问题
当应用程序运行时,UIKit帮助你调试的焦点问题。
为什么这个视图不能获得焦点?
一个被期望获得焦点的视图却不能获得焦点的原因有很多种,下面列举了几种可能的原因但并不是全部原因:
- 视图的 canBecomeFocused 方法返会值为 NO 。
- 视图的 hidden 属性值设置为 YES 。
- 视图的透明度属性值为 0 。
- 视图的用户交互被禁用。
- 这个视图被它上面的视图完全遮盖。
UIKit 提供的 UIView 类的隐藏方法——_whyIsThisViewNotFocusable,目的是为了帮助测试所有前面提到的常见情况。此方法仅是在调试一个特别的视图引用时调用,并打印出一个具有可读性并且可能引起问题的如下所示的列表:
为什么焦点移动到你没有想到的某个地方?
有时候,焦点并没有移动到你所期待的地方,或者它根本就没有移动。焦点引擎提供了很多便利,有时候你最好了解关于它是如何移动焦点的更多信息。
UIKit发送了一个可视化的关于焦点引擎在 Quick Look 中如何搜寻下一个焦点视图的 UIFocusUpdateContext 实例对象。为了看到这个场景,可以在shouldUpdateFocusInContext: 或者 didUpdateFocusInContext:withAnimationCoordinator: 方法中设置断点,当程序运行到这个断点时,在调试台中选中 context 参数并代开 Quick Look 菜单选项。
图 3-4在调试器中选择上下文参数
如果断点在焦点运动的过程中被执行(不是焦点更新时), Quick Look会展示一张图片来表达搜索引擎找到下一个焦点视图的搜索路径。例子如下3-5图。
图 3-5快速寻找一个图像显示
下面是 Quick Look 中图片可能展示的几种信息
- 以前的焦点视图(搜索的开始视图),显示在红色区域。
- 搜索路径轮廓,用虚线标注。
- 任何在搜索路径中可获得焦点的 UIView 对象区域,显示紫色。
- 任何在搜索路径中可获得焦点的 UIFocusGuide 类区域,显示蓝色。