4、CAAnimation家族详解:CABasicAnimation、CAKeyframeAnimation、CAAnimationGroup的配合使用

说实话,很多iOS开发者做动画,翻来覆去就只会用个UIView.animate。一旦遇到复杂点的动效,就卡住了。

我刚开始带团队那会儿,有个同事做了一个转场动画,代码写了三百多行,全是UIView嵌套回调。我看完头皮发麻——其实用CAAnimationGroup,三十行就能搞定。

今天咱们就把CAAnimation家族这三个核心成员聊透。你掌握了它们,iOS动画这块基本就通了八成。

4.1 CABasicAnimation:最基础的“从A到B”

CABasicAnimation,说白了就是让某个属性从起始值变到结束值。比如让一个圆从左边移到右边,或者让透明度从1变成0。

我习惯这么写:

let animation = CABasicAnimation(keyPath: "position.x")
animation.fromValue = 50
animation.toValue = 300
animation.duration = 1.0
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
// 记得设置这个,不然动画结束会跳回原位
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false

view.layer.add(animation, forKey: "moveAnimation")

嗯,这里要注意一个坑。很多人第一次用CABasicAnimation,发现动画结束后控件“啪”一下跳回原位。为什么会这样?

因为CAAnimation默认只在渲染层生效,动画结束后模型层的值没变。所以要么设置fillMode和isRemovedOnCompletion,要么在动画结束后手动更新layer的model值。

我曾经踩过的坑: 在UITableViewCell里用CABasicAnimation做点赞动画,结果快速滑动时动画卡顿。后来发现是没设置isRemovedOnCompletion = false,导致大量动画对象没有被正确释放。记住,复用视图里的动画一定要做好清理。

4.2 CAKeyframeAnimation:不止是起点和终点

如果你想让动画沿着一条曲线走,或者中间经过几个关键点,CABasicAnimation就不够用了。这时候得上CAKeyframeAnimation。

举个例子,让一个球沿着抛物线轨迹运动:

let keyframeAnimation = CAKeyframeAnimation(keyPath: "position")
let path = UIBezierPath()
path.move(to: CGPoint(x: 50, y: 300))
path.addQuadCurve(to: CGPoint(x: 300, y: 300), 
                  controlPoint: CGPoint(x: 175, y: 50))

keyframeAnimation.path = path.cgPath
keyframeAnimation.duration = 2.0
keyframeAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
keyframeAnimation.calculationMode = .paced

ballView.layer.add(keyframeAnimation, forKey: "parabolaAnimation")

你想想看,如果不用CAKeyframeAnimation,你要怎么实现这个抛物线?用CADisplayLink逐帧计算?那代码量就上去了。

我的个人习惯: 做路径动画时,我一般用calculationMode = .paced。这样系统会自动根据路径长度均匀分配速度,不会出现拐弯处快、直道处慢的违和感。这个参数很多人不知道,但效果差别很大。

4.3 CAAnimationGroup:把动画“打包”起来

单个动画能做的事情有限。实际项目中,我们经常需要同时执行多个动画——比如一个按钮既要放大,又要变色,还要旋转。

用CAAnimationGroup,你可以把多个动画组合成一个组,统一管理:

// 1. 缩放动画
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.fromValue = 1.0
scaleAnimation.toValue = 1.5

// 2. 透明度动画
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 1.0
opacityAnimation.toValue = 0.5

// 3. 旋转动画
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.fromValue = 0
rotationAnimation.toValue = Double.pi * 2

// 组合起来
let group = CAAnimationGroup()
group.animations = [scaleAnimation, opacityAnimation, rotationAnimation]
group.duration = 2.0
group.repeatCount = .infinity
group.autoreverses = true
group.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)

view.layer.add(group, forKey: "groupAnimation")

我在项目中遇到过这样一个需求:一个加载动画需要同时旋转和呼吸(缩放+透明度变化)。如果用多个独立的CAAnimation分别添加,很难保证它们同步结束。用CAAnimationGroup就简单了——所有子动画共享duration和timingFunction。

4.4 三者的配合使用:一个实战案例

光讲理论没意思。咱们来个真实场景:做一个“购物车添加商品”的动效。

需求是这样的:点击商品,商品图片飞入购物车,同时购物车图标弹一下。

// 第一步:图片从商品位置飞到购物车(路径动画)
let pathAnimation = CAKeyframeAnimation(keyPath: "position")
let bezierPath = UIBezierPath()
bezierPath.move(to: productCenter)
bezierPath.addCurve(to: cartCenter, 
                    controlPoint1: CGPoint(x: midX, y: productCenter.y),
                    controlPoint2: CGPoint(x: midX, y: cartCenter.y))
pathAnimation.path = bezierPath.cgPath
pathAnimation.duration = 0.6

// 第二步:图片在飞行过程中逐渐缩小
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.fromValue = 1.0
scaleAnimation.toValue = 0.3
scaleAnimation.duration = 0.6

// 第三步:购物车图标弹跳
let bounceAnimation = CAKeyframeAnimation(keyPath: "transform.scale")
bounceAnimation.values = [1.0, 1.4, 0.9, 1.1, 1.0]
bounceAnimation.keyTimes = [0, 0.2, 0.4, 0.6, 1.0]
bounceAnimation.duration = 0.4
bounceAnimation.beginTime = 0.5  // 等图片飞到一半再开始

// 组合飞行动画
let flyGroup = CAAnimationGroup()
flyGroup.animations = [pathAnimation, scaleAnimation]
flyGroup.duration = 0.6
flyGroup.fillMode = .forwards
flyGroup.isRemovedOnCompletion = false

// 分别添加到两个视图
productImageView.layer.add(flyGroup, forKey: "flyToCart")
cartIconView.layer.add(bounceAnimation, forKey: "bounce")

你看,这个动效里三个动画家族成员都用上了:

  • CAKeyframeAnimation:控制图片沿贝塞尔曲线飞行
  • CABasicAnimation:控制图片缩放
  • CAAnimationGroup:把飞行和缩放打包同步执行
核心要点: 用beginTime属性控制不同动画的启动时机,可以实现“先后顺序”的效果。比如购物车弹跳在图片飞到一半时触发,看起来更自然。这个技巧我在做电商App时屡试不爽。

4.5 性能优化:别让动画拖垮你的FPS

动画做得好不好,最终看帧率。我见过太多人把动画做出来了,但一跑就掉帧。

几个关键点:

  1. 尽量操作transform和opacity——这两个属性不会触发离屏渲染,性能最好
  2. 避免同时操作太多layer——我建议单个动画组里的子动画不要超过5个
  3. 用shouldRasterize做缓存——如果动画视图有复杂阴影或圆角,开启光栅化能大幅提升性能
  4. 及时移除不需要的动画——用removeAllAnimations()清理,尤其是cell复用场景
我曾经踩过的坑: 在一个列表页里给每个cell都加了入场动画,结果快速滑动时内存暴涨。后来发现是每个cell的动画都持有对layer的强引用,导致cell无法释放。解决方案是在cell的prepareForReuse里调用layer.removeAllAnimations()。

好了,CAAnimation家族这三个成员,你只要理解它们的定位:CABasicAnimation做简单变化,CAKeyframeAnimation做复杂路径,CAAnimationGroup做组合控制。配合使用,基本能覆盖90%的UI动效需求。

下一章咱们聊聊更高级的CASpringAnimation和自定义时序曲线,到时候你会发现,原来“弹性动画”可以这么简单。