《iOS 三问》 -- iOS 动画概述

1 Animation 概述

移动 App 内的动画,虽然看起来很炫酷,但其实花样并不多,无非就是可视元素大小变化、位置变化、颜色透明度变化等。在 iOS 中,将动画的本质视为 一个视图的可动画属性随着时间的改变

我们现在用计算机的思维去思考一次 app 中的一个动画 – “一个 view 从屏幕的一个位置移动到另一个位置”。

动画就是一个视图的可动画属性随着时间的改变,在这个动画中,无非就是 view 的位置随着时间而改变,然后在屏幕上实时地刷新其当前位置。我们现在来剖析这个动画,它其实就是 view 从一个位置 positionA,移动到屏幕上另一个位置 positionB,为了实现这个动画,我们要在 app 界面上自动生成一个小电影 – 每隔一段时间计算 view 当前应该要在的位置,然后发出信号刷新 UI,让 view 显示在新的位置上。如果这个计算与刷新的速度足够快的话,这个动画看起来就是连续的,也就是真是成为一个动画了。

如果我们要自己去实现一个动画的细节,这将是一个浩大的工程,我们要自己去管理动画实现的这些细节:

  1. 计算 - 对属性的计算,比如某时刻下可动画属性的值;
  2. 计时 - 对象属性随时间是个怎样的变化;
  3. UI 刷新 - 属性变化后怎样刷新 UI;
  4. 线程管理 - 当然,总不可以在主线程绘制动画吧,驱动动画的子线程我们还要加以管理。

幸运的是,iOS 早已想我们所想,为我们提供了一系列方便的框架及 API。这样就使得我们不需要花费精力在动画的具体实现上,而可以把精力集中放在动画的表现上

2 动画是怎样实现的

上面我所说的是计算机中一般动画的实现,那么具体到 iOS 中,动画是怎样实现的呢?

2.1 重绘时机 (redraw monment)

在搞清楚动画细节前,我们需要了解一下一个很重要的概念 – 重绘 (redraw)。

之前我们介绍 view 与 layer 时,介绍过 UI 的绘制原理与重绘原理,但是,当 view 或 layer 属性发生改变,会在什么时候触发重绘机制来刷新 UI 显示呢?

我们说,当我们改变一个 View 的可动画属性时,UI 的变化并不是马上呈现到屏幕中的。事实上系统会把这个修改记下来,然后对这个 View 进行一次标记,标记它即将重绘 (也就是我们之前介绍过的 setNeedsDisplay 做的事情)。如果有多个修改,系统会将之构造在一起,在你这段代码运行结束时或系统空闲时对屏幕进行重绘 (这一重绘时刻,也称之为 redraw monment)。

所以,我们可以说,重绘时机主要在下面两个:

  1. 当 view 可动画属性修改时,触发重绘
  2. 手动调用 setNeedsDisplay

这解释了下面的语句为什么不会造成 view 闪烁,因为在真正发生重绘时,只有最后一个修改会最终生效,绘制于屏幕上。

1
2
3
view.backgroundColor = UIColor.redColor;
view.backgroundColor = UIColor.yellowColor;
view.backgroundColor = UIColor.greenColor;

2.2 动画发生的时机

与重绘一样,当你想开始一段动画时,也得等到重绘时刻 (redraw monment) 动画才会真正开始。

当你把 view 从 position1 移动 position2 时,会发生如下事情 :

  1. View 的 center 这个可视的位置属性,被设置成 position2;但是还没有到重绘时刻,所以 view 现在看起来还在 position1 位置;
  2. 设置完 position 属性后的代码继续运行;
  3. 重绘时刻终于来临,如果没有动画的话,view 会马上被重绘到 position2 的位置。如果有动画,则会开始 view 从 position1 到 position2 的动画。动画结束,把 view 放在 position2 位置上。
  4. 如果有动画的话,动画播完后会被移除,现在 view 的属性与看起来的位置都在 position2 上。

做动画时,要确保的一点就是,动画放完之后,view 当前的属性与动画结束时的属性应该是一致的

2.3 呈现与模型

动画是在一个独立的线程中发生的。

动画不仅使用了多线程技术,还使用了 “多图层技术”。我们平时看到的 layer 其实不是 layer 本身,是它的presentation layer,你可以通过 presentationLayer这个属性获得它;

当你通过修改一个 view 或 layer 的可动画属性来开始一个动画时,layer 的这个可动画属性也跟着立马就变了,但是它的presentation layer却没变。直到我们所说的重绘时刻发生时,presentation layer才会随着时间改变,这就形成了动画!

独立线程还有个好处就是,在动画的整个生命周期,可以对动画的不同阶段进行回调。

而且如果这个线程被打断也不要紧,因为动画线程只是用于播放动画,实际上动画被打断,view 的相关属性早就已设置成最终的值,界面恢复时 view 也会出现在它所要在的正确位置。

2.3.1 呈现与模型的设计思想

这里值得再说一说,我们说 CALayer 的设计其实使用了典型的 MVC 模式。CALayer 是一个连接用户界面的模型类,存储了视图应该如何显示和动画的数据模型,而具体的展现,用的是 view 视图 – presentation layer

当我们修改一个视图的可视属性,数据模型 CALayer 的属性会马上更改,而视图层 presentation layer 的属性不会马上变化,而是会根据 redraw monment 时的状态再更新其显示或由动画线程动态地改变。而如果你要从视图层 presentation layer 获取当前视图真正的属性值的话,可以通过其 modelLayer 属性,其实就是指回了其数据模型 CALayer

2.3.2 什么时候会用到 presentation layer

  • 当你要自己实现基于自己的定时器的动画时,可能要用到 presentation layer 去自行地改变视图层当前的可视状态。

  • 当你想在图层动画过程中响应用户手势操作时,也有可能会用动。因为当前图层的真正响应区域其实是动画结束时的区域,如果你想在图层动画之中的位置响应用户手势的话,就得用 presentation layer 去判断 hitTest 等。

3 小结

通过这些分析,我们对动画实现的细节是不是加深了了解呢,动画的实质是 一个视图的可动画属性随着时间的改变。当可动画属性改变后,差不会马上进行重绘或动画,而是等在重绘时刻中进行。在动画进行时,会在自己的 独立线程 里计算该属性在当前时刻的插值,然后通过更新 presentation layer 来显示动画。

4 我们的收获

4.1 UI 系统中动画的实现与 iOS 中的优化

其实,在不同的 UI 系统中,如 Windows、Android 等也和 iOS 的动画实现一样,基本都是使用 一个视图的可动画属性随着时间的改变 这样的思想去设计的。架构上也差不多都是有个动画的独立线程,然后在线程里对动画进行插值计算,异步 UI 刷新等工作来实现动画。而 iOS 对这个基本的动画框架做了上述的一些优化:

  • redraw monment

比如 redraw monment 的设计,iOS 并不是每次修改可动画属性都触发 UI 的重绘,而是在属性的修改都进行完毕后在一个时机统一来进行重绘或动画,从而减少重绘的次数。而且这个重绘是自动进行的,比某些 UI 框架修改完后必须手动调用或发消息触发 UI 刷新要更简单和方便。

  • 动画生命周期的回调

动画生拿周期的回调也让 API 使用者用起来更加的方便了,比如你可以在动画开始时禁止用户对界面的操作,然后在动画结束回调中打开界面的可操作,从而保证你的这次动画不受用户操作的影响,等等,这让使用者使用动画与实现逻辑可以更加紧密和方便了。

4.2 呈现与模型的设计思想

当我们设计 UI 组件时,可以学习此设计思想,将组件的业务与显示分开。这样做可以降低控件实现逻辑与展示逻辑之间的耦合,特别在实现复杂功能的 UI 组件时,会让整个设计结构更加清晰,功能边界明了。

5 引用

【1】[Matt Neuburg - 《Programming iOS deep into views,view controllers and frameworks》]
【2】[Nick LockWood - 《iOS Core Animation: Advanced Techniques》]