objc系列译文(12.4):View-Layer 协作

761 查看

在 iOS 中,所有的 view 都是由一个底层的 layer 来驱动的。view 和它的 layer 之间有着紧密的联系,view 其实直接从 layer 对象中获取了绝大多数它所需要的数据。在 iOS 中也有一些单独的 layer,比如 AVCaptureVideoPreviewLayerCAShapeLayer,它们不需要附加到 view 上就可以在屏幕上显示内容。两种情况下其实都是 layer 在起决定作用。当然了,附加到 view 上的 layer 和单独的 layer 在行为上还是稍有不同的。

基本上你改变一个单独的 layer 的任何属性的时候,都会触发一个从旧的值过渡到新值的简单动画(这就是所谓的可动画 animatable)。然而,如果你改变的是 view 中 layer 的同一个属性,它只会从这一帧直接跳变到下一帧。尽管两种情况中都有 layer,但是当 layer 附加在 view 上时,它的默认的隐式动画的 layer 行为就不起作用了。

animatable;几乎所有的层的属性都是隐性可动画的。你可以在文档中看到它们的简介是以 ‘animatable’ 结尾的。这不仅包括了比如位置,尺寸,颜色或者透明度这样的绝大多数的数值属性,甚至也囊括了像 isHidden 和 doubleSided 这样的布尔值。 像 paths 这样的属性也是 animatable 的,但是它不支持隐式动画。

在 Core Animation 编程指南的 “How to Animate Layer-Backed Views” 中,对为什么会这样做出了一个解释:

UIView 默认情况下禁止了 layer 动画,但是在 animation block 中又重新启用了它们

这正是我们所看到的行为;当一个属性在动画 block 之外被改变时,没有动画,但是当属性在动画 block 内被改变时,就带上了动画。对于这是如何发生的这一问题的答案十分简单和优雅,它优美地阐明和揭示了 view 和 layer 之间是如何协同工作和被精心设计的。

无论何时一个可动画的 layer 属性改变时,layer 都会寻找并运行合适的 ‘action’ 来实行这个改变。在 Core Animation 的专业术语中就把这样的动画统称为动作 (action,或者 CAAction)。

CAAction:技术上来说,这是一个接口,并可以用来做各种事情。但是实际中,某种程度上你可以只把它理解为用来处理动画。

layer 将像文档中所写的的那样去寻找动作,整个过程分为五个步骤。第一步中的在 view 和 layer 中交互的部分是最有意思的:

layer 通过向它的 delegate 发送 actionForLayer:forKey: 消息来询问提供一个对应属性变化的 action。delegate 可以通过返回以下三者之一来进行响应:

  1. 它可以返回一个动作对象,这种情况下 layer 将使用这个动作。
  2. 它可以返回一个 nil, 这样 layer 就会到其他地方继续寻找。
  3. 它可以返回一个 NSNull 对象,告诉 layer 这里不需要执行一个动作,搜索也会就此停止。

而让这一切变得有趣的是,当 layer 在背后支持一个 view 的时候,view 就是它的 delegate;

在 iOS 中,如果 layer 与一个 UIView 对象关联时,这个属性必须被设置为持有这个 layer 的那个 view。

理解这些之后,前一分钟解释起来还复杂无比的现象瞬间就易如反掌了:属性改变时 layer 会向 view 请求一个动作,而一般情况下 view 将返回一个 NSNull,只有当属性改变发生在动画 block 中时,view 才会返回实际的动作。哈,但是请别轻信我的这些话,你可以非常容易地验证到底是不是这样。只要对一个一般来说可以动画的 layer 属性向 view 询问动作就可以了,比如对于 ‘position’:

运行上面的代码,可以看到在 block 外 view 返回的是 NSNull 对象,而在 block 中时返回的是一个 CABasicAnimation。很优雅,对吧?值得注意的是打印出的 NSNull 是带着一对尖括号的 (“<null>“),这和其他对象一样,而打印 nil 的时候我们得到的是普通括号((null)):

对于 view 中的 layer 来说,对动作的搜索只会到第一步为止(至少我没有见过 view 返回一个 nil 然后导致继续搜索动作的情况)。对于单独的 layer 来说,剩余的四个步骤可以在 CALayer 的 actionForKey: 文档中找到。

从 UIKit 中学习

我很确定我们都会同意 UIView 动画是一组非常优秀的 API,它简洁明确。实际上,它使用了 Core Animation 来执行动画,这给了我们一个绝佳的机会来深入研究 UIKit 是如何使用 Core Animation 的。在这里甚至还有很多非常棒的实践和技巧可以让我们借鉴。:)

当属性在动画 block 中改变时,view 将向 layer 返回一个基本的动画,然后动画通过通常的 addAnimation:forKey: 方法被添加到 layer 中,就像显式地添加动画那样。再一次,别直接信我,让我们实践检验一下。

归功于 UIView 的 +layerClass 类方法,view 和 layer 之间的交互很容易被观测到。通过这个方法我们可以在为 view 创建 layer 时为其指定要使用的类。通过子类一个 UIView,以及用这个方法返回一个自定义的 layer 类,我们就可以重写 layer 子类中的 addAnimation:forKey: 并输出一些东西来验证它是否确实被调用。唯一要记住的是我们需要调用 super 方法,不然的话我们就把要观测的行为完全改变了:

通过输出动画的 debug 信息,我们不仅可以验证它确实如预期一样被调用了,还可以看到动画是如何组织构建的:

当动画刚被添加到 layer 时,属性的新值还没有被改变。在构建动画时,只有 fromValue (也就是当前值) 被显式地指定了。CABasicAnimation 的文档向我们简单介绍了这么做对于动画的插值来说的的行为应该是:

只有 fromValue 不是 nil 时,在 fromValue 和属性当前显示层的值之间进行插值。

这也是我在处理显式动画时选择的做法,将一个属性改变为新的值,然后将动画对象添加到 layer 上: