前两篇文章已经介绍了 UIView 层面的动画内容已经足够应对大多数情况,但是面对更高的性能和更大的复杂度的场景则远远不够。 所以接下来我们将视角伸向更底层的 Core Animation,看一看 iOS 动画世界的基石。
Core Animation 的主体内容大致可分为两类:Layer、Animation。为了与前文更好的衔接,我们先来看 Animation 部分。下图是 Animation 的结构图:
CAAnimation 是整个动画的基础,他派生出了 CAAnimationGroup、CATransition、CAPropertyAnimation。而 CAPropertyAnimation 进一步派生了三个最常用的动画类:CABasicAnimation、CAKeyframeAnimation、CASpringAnimation。CAMediaTiming 协议中则包含了动画时间线相关的所有重要属性。CAMediaTimingFunction 负责时间线上的进出场效果处理。CAAnimationDelegate 则是作为所有动画的代理。
准备好了吗?一起接受动画洗礼吧。
CABasicAnimation
CABasicAnimation 作为基础动画使用频次最高,它通过更改 CALayer 中各种基础属性来进行动画实现。不过这里可用于动画的属性远比 UIView 层面要多得多。除了位置、大小、旋转角度等,还可以对边框、阴影、寄宿图进行设置。
例如最简单的位置改变动画,我们都不需要在 viewWillAppear 中提前设置状态,直接在 viewDidAppear 中添加代码:
override func viewDidAppear(_ animated: Bool) {
...
let flyRight = CABasicAnimation(keyPath: "position.x" )
flyRight.fromValue = -view.bounds.size.width / 2
flyRight.toValue = view.bounds.size.width / 2
flyRight.duration = 0.5
userAvator?.layer.add(flyRight, forKey: nil)
}
当然我们也可以像之前一样设置状态然后再动画:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
userAvator?.center.x = -view.bounds.size.width / 2
}
override func viewDidAppear(_ animated: Bool) {
...
let flyRight = CABasicAnimation(keyPath: "position.x" )
flyRight.toValue = view.bounds.size.width / 2
flyRight.duration = 0.5
userAvator?.layer.add(flyRight, forKey: nil)
}
但是此时你就会发现一个问题,userAvator 在动画介绍后并没有出现在屏幕中。 在之前我们提过,动画过程中我们所见的并不是 Layer Tree 而是 Presentation Tree 所代表的瞬时值,而 Presentation Tree 又是由私有的 Render Tree 进行渲染的。所以我们能看见不过是 Presentation Tree 呈现的动画过程,而对象本身的状态并没有事实上的改变, userAvator 还是处于预设状态的视图之外。为了解决这个问题,需要更细粒度的动画控制。
CAMediaTiming、CAMediaTimingFunction 进行更精确的控制。
CAMediaTiming 作为协议主要负责对动画的时间线进行刻画,主要包含以下属性:
beginTime:动画开始时间
timeOffset:动画播放时间的偏移量
repeatCount:动画的循环次数
repeatDuration 动画循环的持续时间
duration:动画时长
speed:动画播放的步长,默认为1。可以实现快速或者低速播放效果
autoreverses:是否以动画形式返回之前的状态。
fillMode 设置当前对象在非活动时间段的状态。
其中 fillMode 必须在动画的 isRemovedOnCompletion = false 的情况下才会生效。它共有四个可选值:
kCAFillModeRemoved 默认值 动画结束后 layer会恢复到之前的状态。
kCAFillModeBackwards 立即进入动画的初始状态并等待动画开始。
kCAFillModeForwards 当动画结束后,layer会一直保持着动画最后的状态 。
kCAFillModeBoth 动画加入后开始之前 layer处于动画初始状态 动画结束后layer保持动画最后的状态。
所以,上一个问题我们可以如此修复:
override func viewDidAppear(_ animated: Bool) {
...
flyRight.isRemovedOnCompletion = false
flyRight.fillMode = kCAFillModeForwards
// 另外可以进行延迟设置
flyRight.beginTime = CACurrentMediaTime() + 0.3
}
除了 CAMediaTiming ,我们也可以通过 CAMediaTimingFunction 对动画的 timingFunction 属性进出场效果进行设置:
kCAMediaTimingFunctionLinear:线性匀速
kCAMediaTimingFunctionEaseIn:加速
kCAMediaTimingFunctionEaseOut:减速
kCAMediaTimingFunctionEaseInEaseOut:先减速后减速
CASpringAnimation
CASpringAnimation 作为 CABasicAnimation 的子类,为了更好的模拟阻尼简谐运动的效果该类提供了很多与之相关的属性:
damping:阻尼系数,越大越容易让动画停止
mass:质量,影响图层运动时的弹簧惯性,质量越大,弹簧拉伸和压缩的幅度越大
stiffness:弹性系数,数值越大反弹幅度越大
initialVelocity:初始速度。小于 0 的时候表示速度方向与运动方向相反。
override func viewDidAppear(_ animated: Bool) {
let flyRight = CASpringAnimation(keyPath: "transform.scale" )
flyRight.fromValue = 1
flyRight.toValue = 1.25
flyRight.damping = 10
flyRight.mass = 5
flyRight.stiffness = 5
flyRight.initialVelocity = 10
flyRight.duration = flyRight.settlingDuration
userAvator?.layer.add(flyRight, forKey: nil)
}
CAKeyframeAnimation
与之前 UIView 中的关键帧动画一样,CAKeyframeAnimation 负责提取 Layer 中的动画关键帧。该动画中的属性值包括:
values:关键帧的所有具体数值,用于确定关键帧的动作。
path:动画路径,默认为 nil。当进行人为指定的时候上面的 values 中的值就会被忽略。
keyTimes:关键帧的所有节点,取值范围为 0.0 ~ 1.0,具体的时间点可自行根据 duration 换算。
timingFunctions:进出场的效果设置。
calculationMode:关键帧之间动画的拟合方式,默认为线性拟合。
rotationMode:默认为 nil ,用于决定动画过程中 Layer 是否沿着轨迹的切线旋转。
简单的示例:
override func viewDidAppear(_ animated: Bool) {
...
let flyRight = CAKeyframeAnimation(keyPath: "transform.rotation" )
flyRight.duration = 1.0
flyRight.values = [0,-M_PI_4,-M_PI_2,-M_PI_4 * 3,-M_PI,-M_PI_4 * 5,-M_PI_2 * 3, -M_PI * 2]
flyRight.keyTimes = [0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8]
userAvator?.layer.add(flyRight, forKey: nil)
}
CATransition
顾名思义 CATransition 就是为了实现 Layer 的转场过渡效果而设计的动画类。该动画让视图的添加过程显得更加自然,具体的动画属性:
startProgress:取值范围为 0.0 ~ 1.0,表示以 duration 时间线上那一刻动画作为起始状态。
endProgress:与 startProgress 一致,只不过表示的是时间线上那一刻动画作为结束状态。
type:动画效果。
subType:动画方向。
type 提供以下取值:
kCATransitionFade 渐变
kCATransitionMoveIn 覆盖
kCATransitionPush 推出
-
kCATransitionReveal 揭开
除此之外,还有以下私有类型:
cube 立方体旋转
suckEffect 收缩动画
oglFlip 翻转
rippleEffect 水波动画
pageCurl 页面揭开
pageUnCurl 放下页面
cemeraIrisHollowOpen 镜头打开
cameraIrisHollowClose 镜头关闭
subType 包含以下取值:
kCATransitionFromRight
kCATransitionFromLeft
kCATransitionFromTop
kCATransitionFromBottom
示例代码,与效果图:
override func viewDidAppear(_ animated: Bool) {
...
let flyRight = CATransition()
flyRight.duration = 1.0
flyRight.type = "rippleEffect"
userAvator?.layer.add(flyRight, forKey: nil)
}
CAAnimationGroup
如果你足够敏锐那么应该注意到上面所有的示例代码中,动画都是针对某一个特定属性的修改或者动作。那么对于同时进行多个动画怎么办呢?当然你也可以通过 add(_ anim: CAAnimation, forKey key: String?) 函数反复在 Layer 上添加动画来实现,但是理想的方式是使用 CAAnimationGroup 将多个动画组合起来再添加到 Layer 中。
我们只需要将那些单独的动画添加到动画数组 animations 中,其余的事情 CAAnimationGroup 会很好的完成。
示例代码与效果图如下:
override func viewDidAppear(_ animated: Bool) {
...
let spring = CASpringAnimation(keyPath: "transform.scale" )
spring.fromValue = 1
spring.toValue = 1.25
spring.damping = 5
spring.mass = 1
spring.stiffness = 2
spring.initialVelocity = 10
spring.duration = spring.settlingDuration
let keyframe = CAKeyframeAnimation(keyPath: "transform.rotation" )
keyframe.duration = spring.settlingDuration
keyframe.values = [0,-M_PI_4,-M_PI_2,-M_PI_4 * 3,-M_PI,-M_PI_4 * 5,-M_PI_2 * 3, -M_PI * 2]
keyframe.keyTimes = [0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8]
let flyRight = CAAnimationGroup()
flyRight.animations = [spring,keyframe]
flyRight.duration = spring.settlingDuration
userAvator?.layer.add(flyRight, forKey: nil)
}
总结
文章到此,相信大家对 Core Animation 框架中的几种动画应该有了大致的了解。当然这些动画还有很懂的细节等待发掘,例如:关键帧动画中的几种插值模拟,读者可以自己去探索。Core Animation 特殊 Layer 部分的内容我们留着下篇再讲。