自己做的笔记,过段时间却又忘记了,为了尽量避免这种情况,同时治服拖延种种,打算开始多写写技术向的博客。

在 Ulysses 写的,转到 Bitcron 简直不能看,代码部分基本都重排了一遍…以后还是在 Typora 写完,发布后再用 Ulysses 做修改好了。

UIKit Animation or Core Animation?

在 iOS 中,动画可以分为两个类别:UIKit,和底层的 Core Animation。

通过最基本的 UIView.animate(withDuration:) 方法建立如下的 UIKit animation,能胜任复杂程度不高的动画:

UIView.animate(withDuration: 0.8) {
      self.orangeBlock.frame = CGRect(x: 38, y: 70, width: 300, height: 60)
}

但是当要处理不单一的属性时,往往会出现一些不合预期的效果,这是因为,用 UIKit 接口创建的动画是基于视图(view)的,它并不能直接对图层(layer)属性作出更改。

为了制作出能符合预期的动画效果,我们就需要直接对图层进行控制,这就是 Core Animation 了。

基于 Core Animation,我们可以创建两步的 CABasicAnimation,或者用 CAKeyframAnimation 绘制路径等。

Layer v.s. View

图层不是视图的替代,而是后者的底层支持。在能满足需求的情况下,对视图而不是图层进行操作——即使用 UIKit 动画——是被建议的做法。

在 iOS 开发中,通常是每一个视图有一个对应的图层(称为 layer-backed view),图层对象会保证两者保持同步。然而在一些特殊情况下,这种一对一的关系不是必须的(比如多个图层支持一个单独 UIImageView 来节省因单张图片重复出现的内存占用)。

基本过渡动画 - CABasicAnimation

想要对哪一组值进行设置,知道对应的 keyPath 就好了。

let radiusAnimation = CABasicAnimation(keyPath: "cornerRadius")
radiusAnimation.fromValue = 50
radiusAnimation.toValue = 30
radiusAnimation.duration = 3

purpleBlock.layer.add(radiusAnimation, forKey: "cornerRadius")
// 为模型图层设置新的半径
purpleBlock.layer.cornerRadius = 30

需要注意的一点是,add(_: CAAnimation, forKey:) 设置的动画针对的是模型图层(model layer)而不是显示图层(presentation layer),当动画显示完,它就被移除了,显示图层属性将会恢复成初始的模型图层属性。所以,为了保持对象视图变化后的状态,要记得手动更新。

Core Animation 控制的图层中包含有两套平行的继承树:模型图层树(model layer tree)和显示图层树(presentation layer tree)。前者反映的是图层状态,后者是图层在动画中动态的值。如果要实时跟踪图层的属性变化,要检测的是显示图层的值。

关键帧动画 - CAKeyframeAnimation

现在,试想我们要实现的动画超过了两步(而这是非常常见的情况),那么以 fromValuetoValue 简单两组值就显然过于局限了。这时候我们用 CAKeyframeAnimation 就能任意地定义和设置帧。

let shakeAnimation = CAKeyframeAnimation(keyPath: "position.x")
shakeAnimation.values = [0, 10, -10, 10, 0]
shakeAnimation.keyTimes = [0, 0.2, 0.6, 0.8, 1]
shakeAnimation.duration = 0.4
shakeAnimation.isAdditive = true

blueBlock.layer.add(shakeAnimation, forKey: "position.x")

values 数组代表的是对象的位置,keyTimes 数组代表划分的时间段。

除了使用状态数组的方式,设定路径(CGPath)也是可以的:

let boundingRect = CGRect(x: -150, y: -150, width: 300, height: 300)
let orbitAnimation = CAKeyframeAnimation(keyPath: "position")
orbitAnimation.path = CGPath(ellipseIn: boundingRect, transform: nil)
orbitAnimation.duration = 4
orbitAnimation.isAdditive = true
orbitAnimation.repeatCount = Float.infinity
orbitAnimation.calculationMode = kCAAnimationPaced
orbitAnimation.rotationMode = kCAAnimationRotateAuto

greyBlock.layer.add(orbitAnimation, forKey: "position")

CAAnimation 的可重用性

一个动画实例由 keyPath 指定其所定义的图层属性类型,因此对于该属性(如 cornerRadius)不必再重复创建新实例,也即是能被多个对象所共用的。

为了提高其可重用性,我们还可以用 byValue 来替代 CABasicAnimation 中的 toValue,前者设定的是变化量。

let radiusAnimation = CABasicAnimation(keyPath: "cornerRadius")
radiusAnimation.fromValue = 50
// radiusAnimation.toValue = 30
// equals:
radiusAnimation.byValue = -20

如果同时创建了多个动画,可以使用 CAAnimationGroup 打包成一个动画组:

let aniGroup = CAAnimationGroup()
aniGroup.animations = [shakeAnimation, radiusAnimation]
aniGroup.duration = 3

magentaBlock.layer.add(aniGroup, forKey: "whatsoever")

Timing Functions 使动画流畅

为了让动画看起来更自然,添加一个 CAMediaTimingFunction 对象实例。可以直接对动画实例设置:

// 现有的「淡入淡出」效果
let timingFunc1 = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
ANI_INSTANCE.timingFunction = timingFunc

也可以为 CATransaction 设置:

// 自定义函数,通过 controlPoints 设置贝塞尔曲线
let timingFunc2 = CAMediaTimingFunction(controlPoints: 0.65, -0.55, 0.27, 1.55)
CATransaction.setAnimationTimingFunction(timingFunc)

CATransaction 事务机制

当我们要进行多组图层操作时,一个很好的习惯是把它们显式(explicitly)合并到一个事务中。

CATransaction Definition: A mechanism for grouping multiple layer-tree operations into atomic updates to the render tree.

事实上,即使我们不主动创建 CATransaction 事务,系统也会默认给我们创建一个隐式(implicit)事务。显式声明还有个好处是,可以为事务内部的代码统一设置一些默认值(如 duration)。嵌套的 CATransaction 是允许的。

CATransaction.begin()
CATransaction.setAnimationDuration(3)
        
let positionAnimation = CABasicAnimation(keyPath: "position")
positionAnimation.fromValue = [38, 484]
positionAnimation.toValue = [112, 484]
greenBlockLayer.add(positionAnimation, forKey: "position")
greenBlockLayer.position = CGPoint(x: 112, y: 484)
        
CATransaction.begin()
CATransaction.setAnimationDuration(1)

UIView.animate(withDuration: b_Interval) {
    self.greenBlock.alpha = 0.5
}

CATransaction.commit()
CATransaction.commit()

Swift Xcode 项目地址
4.0 9.2 Github

ref:

  1. Better iOS Animations with CATransaction - Medium
  2. Core Animation Basics - Apple
  3. When is it appropriate to use Core Animation over UIView animation… - Stack Overflow
  4. Animations Explained - objc.io
  5. CATransaction - Apple
Comments
Write a Comment