快速上手iOS高性能动画

2,606 阅读15分钟

一个设计严谨、精细的动画效果能给用户耳目一新的感觉,吸引他们的眼光 —— 这对于app而言是非常重要的。如果足够细心,我们不难发现一个酷炫的动画通过步骤分解后,本质上不过是由一个个简单的动画组合而成。


目录

  • CoreAnimation
  • 剖析CALayer
  • Lottie助力程序员解放双手

CoreAnimation

Core Animation是iOS和OS X平台上负责图形渲染与动画的基础框架。它可以作用于动画视图或者其他可视元素,可以完成动画所需的大部分绘帧工作。在图形渲染中,它将大部分实际的绘图任务交给了图形硬件(GPU)来处理,图形硬件会加速图形渲染的速度。这种自动化的图形加速技术让动画拥有更高的帧率并且显示效果更加平滑,不会加重CPU的负担而影响程序的运行速度。 在iOS中,普通的动画可以使用UIKit提供的方法来实现动画,但如果想要实现复杂的动画效果,使用CoreAnimation框架提供的动画效果是最好的选择。相比于UIKit提供的动画能力,CoreAnimation具有更强大的优势:

  • 使用轻便,轻量级的数据结构,可以同时让上百个图层产生动画效果。
  • 后台运行,拥有独立的线程用于执行我们的动画。
  • 动画托管,完成动画配置后,核心动画会代替我们控制完成对应的动画帧。
  • 提高应用性能,只有在发生改变的时候才重绘内容。

core.png 在日常开发中,我们能够直接使用的CoreAnimation对象有CABasicAnimation(基础动画)、CAKeyframeAnimation(关键帧动画)、CAAnimationGroup(组动画)、CATransition(转场动画/过渡动画)、CASPringAnimation(添加了物理效果的弹性动画)。

CABasicAnimation

基础动画主要提供了对CALayer对象的可变属性进行动画的操作,比如位移、旋转、缩放、透明度、背景色等。基础动画根据keyPath来生成不同的动画,系统提供有缩放比例转换(scale)、旋转(rotation) 、平移(postion)。当然,在CALayer中有很多属性都具备隐式动画的能力,在使用时可以根据具体的需求选择合适的方式。 CABasicAnimation有两个很重要的属性,分别是fromValue和toValue,像开发中常见的进度圆环动画,主要依赖于这两个属性值的改变。

CAKeyframeAnimation

CAKeyframeAnimation(关键帧动画)和CABasicAnimation都属于CAPropertyAnimatin的子类。不同的是CABasicAnimation只能从一个数值(fromValue)变换成另一个数值(toValue),而CAKeyframeAnimation则会使用一个数组(values)保存一组关键帧,也可以给定一个路径(path)来制作动画。 它还可以针对values中的每一帧单独设置动画的时长,产生更加精准的动画效果。

CAAnimationGroup

CAAnimationGroup作为CAAnimation的子类,它可以保存多个动画对象到数组(animations)中,数组中所有动画对象可以并发进行,例如在图形移动的同时不断放大缩小。通过对CAAnimationGroup进行拓展,也可以使多个动画依次执行,但这种用法在开发中并不多见。

CATransition

CATransition用于做过渡动画或者转场动画,能够为图层提供移出屏幕和移入屏幕的动画效果。通过设置不同type值可以产生对应的动画,CATransition其实有多种过渡效果,但官方只提供了四种:

  • fade 淡出,为系统默认效果
  • moveIn 覆盖原图
  • push 推出
  • reveal 底部显示出来

通过调用私有API还可以实现其他非常炫的过渡动画,如cube(立方旋转)、suckEffect(吸走)、oglFlip(水平翻转 沿y轴)、rippleEffect(滴水效果)、pageCurl(卷曲翻页 向上翻页)、pageUnCurl(卷曲翻页 向下翻页)、cameraIrisHollowOpen(相机开启)、cameraIrisHollowClose(相机关闭)等。 但是使用私有API会导致应用审核被拒,所以要实现上面的动画效果,只能通过其他途径来实现。

CASpringAnimation

iOS 9推出的一种用以产生带有物理弹性效果的动画方式,相比于CABasicAnimation,它多出了几个物理参数,如mass(质量,影响惯性)、stiffness(弹性系数)、damping(阻尼系数,也可以理解为摩擦力系数)等。

CoreAnimation与CALayer

CoreAnimation也可以称为是Layer Animation,它依托于图层进行工作,图层是Core Animation的核心。 Core Animation定义了许多标准的图层类,每一个图层类都有着各自的应用场景。CALayer类是所有图层对象的根类,它定义了所有图层对象必须支持的行为,它也是支持图层视图的默认图层类型。 在实际开发中CoreAnimation都是被添加在View的CALayer属性上,如果直接对View的图层进行修改,如圆角、蒙版等效果,就会产生额外的性能开支。这种场景下,熟悉CALayer的结构和功能对开发高性能的动画有着很大的帮助。 接下来我们先了解下日常开中比较常见的CALayer对象,并通过实践开发将图层绘制与CoreAnimation结合,制作一些比较简单的动画效果。

剖析CALayer

layer.jpg

CAShapeLayer

这个类可以用来完成日常开发中比较常见的图形绘制,结合path属性还可以快速绘制各种复杂的图形。从常见的圆环、多边形到眼花缭乱的走势图看板等,都可以通过CAShapeLaye来实现。可以说是iOS开发人员接触最多的Layer对象,同时CAShapeLayer的属性看起来也很容易理解。

/// path  路径
/// fillColor  填充颜色
/// fillRule  填充规则(‘非零’,‘奇偶’)
/// strokeColor  渲染颜色
/// strokeStart  渲染初始值
/// strokeEnd  渲染结束值
/// lineWidth  线宽(渲染)
/// miterLimit  最大斜长度(当lineJoin属性为kCALineJoinMiter生效)
/// lineCap  线两端的样式
/// lineJoin  连接点类型
/// lineDashPhase  虚线开始的位置
/// lineDashPattern  虚线设置

在演示Demo里,我将CAShapeLayerUIBezierPathCoreAnimation结合,写了一个雪花按照花瓣的路径移动的动画,效果如下图。

下面是该效果的代码实现,我首先根据屏幕尺寸创建了一个花瓣的路径,然后实例化CAShapeLayer对象绘制对应的花瓣曲线,在绘制的过程中使用CABasicAnimation来创建一个路径被快速描绘的动画效果。在最后的代码中可以看到,我还创建了一个雪花View,并使用CAKeyframeAnimation创建了一个关键帧动画,模拟雪花跟随的效果。 在这个示例中,CABasicAnimationCAKeyframeAnimation实现的效果是相同的,但在实际开发中可以使用CAKeyframeAnimation做出更酷炫的效果。

/// demo of animation with a snow image position at the path
    static func show(inView view: UIView) {
        
        //fllower path
        let bezierPath = UIBezierPath()
        bezierPath.move(to: view.center)
        bezierPath.addQuadCurve(to: CGPoint(x: 80, y: view.center.y - 90), controlPoint: CGPoint(x: view.center.x + 50, y: view.center.y - 90))
        bezierPath.addQuadCurve(to: view.center, controlPoint: CGPoint(x: 80, y: view.center.y - 20))
        bezierPath.addQuadCurve(to: CGPoint(x: 80, y: view.center.y + 90), controlPoint: CGPoint(x: view.center.x + 50, y: view.center.y + 90))
        bezierPath.addQuadCurve(to: view.center, controlPoint: CGPoint(x: 80, y: view.center.y + 20))
        bezierPath.addQuadCurve(to: CGPoint(x: screen_width - 80, y: view.center.y + 90), controlPoint: CGPoint(x: view.center.x - 50, y: view.center.y + 90))
        bezierPath.addQuadCurve(to: view.center, controlPoint: CGPoint(x: screen_width - 80, y: view.center.y + 20))
        bezierPath.addQuadCurve(to: CGPoint(x: screen_width - 80, y: view.center.y - 90), controlPoint: CGPoint(x: view.center.x - 50, y: view.center.y - 90))
        bezierPath.addQuadCurve(to: view.center, controlPoint: CGPoint(x: screen_width - 80, y: view.center.y - 20))
        
        //layer
        let shapeLayer = CLShapeLayer()
        shapeLayer.strokeColor = UIColor.white.cgColor
        shapeLayer.lineWidth = 2
        shapeLayer.lineJoin = .round
        shapeLayer.lineCap = .round
        shapeLayer.path = bezierPath.cgPath
        view.layer.addSublayer(shapeLayer)
        
        //CABasicAnimation
        let pathAnim = CABasicAnimation(keyPath: "strokeEnd")
        pathAnim.duration = 5.0
        pathAnim.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
        pathAnim.fromValue = 0
        pathAnim.toValue = 1
        pathAnim.autoreverses = true// 动画是否按原路径返回
        pathAnim.fillMode = .forwards
        pathAnim.repeatCount = Float.infinity
        shapeLayer.add(pathAnim, forKey: "strokeEndAnim")
        
        //snow
        let snow = UIImageView.init(image: UIImage.init(named: "snow"))
        snow.frame = CGRect(x: 0, y: 0, width: 20, height: 20)
        view.addSubview(snow)
        
        //CAKeyframeAnimation
        let keyAnima = CAKeyframeAnimation.init(keyPath: "position")
        keyAnima.duration = 5.0
        keyAnima.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
        keyAnima.path = bezierPath.cgPath
        keyAnima.autoreverses = true
        keyAnima.repeatCount = Float.infinity
        keyAnima.fillMode = .forwards
        snow.layer.add(keyAnima, forKey: "moveAnimation")
    }

CAReplicatorLayer

通过这个类的名称,我们可以猜测到它具备复制图层的功能,事实上它的确有类似的功能,CAReplicatorLayer的目的是为了高效生成许多相似的图层,它会绘制一个或多个图层的子图层,并在每个复制体上应用不同的变换。直接演示或许能加深我们的理解,我们先来看下它都有哪些属性。

/// instanceCount  复制图层的个数,包括加到上面的
/// preservesDepth  子图层是否平面化(参考CATransformLayer)
/// instanceDelay  复制层动画延迟时间
/// instanceTransform  子图层的transform变换,一般用来决定复制图层的初始位置以及初始试图变换
/// instanceColor   复制层颜色,该颜色是与本体元素色值相乘,鬼知道是什么颜色
/// instanceRedOffset   复制层红色偏移量
/// instanceGreenOffset   复制层绿色偏移量
/// instanceBlueOffset   复制层蓝色偏移量
/// instanceAlphaOffset   复制层透明度偏移量

接下来我使用CAReplicatorLayerCoreAnimation结合,绘制了一些开发中比较常见的加载动效,看下面的演示图,有没有觉得很眼熟?

没错,在iOS开发中很多HUD和Loading动画都是通过CAReplicatorLayer来实现的。实现这些效果不是很难,通过系统提供的属性赋值后添加在View的layer上就可以达到类似的效果,下面贴一个Loading动画的实现源码。

    /// demo of a circle dot loading animation
    private func circleDot(_ view: UIView) {
        
        //CAReplicatorLayer
        let replicatorLayer = CLReplicatorLayer()
        replicatorLayer.bounds = view.frame;
        replicatorLayer.position = view.center;
        view.layer.addSublayer(replicatorLayer)
        
        //CALayer
        let layer = CALayer()
        layer.backgroundColor = UIColor.red.cgColor
        layer.cornerRadius = 20
        layer.bounds = CGRect(x: 0, y: 0, width: 40, height: 40)
        layer.position = CGPoint(x: 50, y: view.center.y)
        replicatorLayer.addSublayer(layer)
        layer.transform = CATransform3DMakeScale(0.01, 0.01, 0.01)
        
        //CABasicAnimation
        let animation = CABasicAnimation(keyPath: "transform.scale")
        animation.fromValue = 1
        animation.toValue = 0.1
        animation.duration = 0.75
        animation.repeatCount = MAXFLOAT
        layer.add(animation, forKey: "LayerPositionCircle")
        
        //config
        replicatorLayer.instanceCount = 15
        replicatorLayer.preservesDepth = true
        var transform = CATransform3DIdentity
        transform = CATransform3DRotate(transform, CGFloat(Double.pi * 2 / 15.0), 0, 0, 1);
        replicatorLayer.instanceTransform = transform
        replicatorLayer.instanceDelay = 0.05
        replicatorLayer.instanceAlphaOffset = -1.0 / 15.0
        replicatorLayer.instanceBlueOffset = 1.0 / 15
        replicatorLayer.instanceColor = UIColor.gray.cgColor
    }

如果你觉得上面的这些都没有达到你想要的效果,请耐心接着往下看,压箱底的东西往往都需要在后面压轴,相信我,接下来你看到的只会更精彩。

CAEmitterLayer

关键的东西来了,如果我们要实现如雨雪、流星、烟雾之类的效果,使用上面的方式就没办法满足我们的需求了。但是苹果已经给开发者们提供了解决方案,我们可以使用CAEmitterLayer来实现粒子特效的展示,像雨雪这种需要对大量同类型的图层添加动画效果的场景,使用它可以事半功倍。

    /// emitterCells  装着CAEmitterCell对象的数组,用于把粒子投放到layer上
    /// birthRate  粒子产生系数
    /// lifetime  存在时长
    /// emitterPosition  发射位置
    /// emitterZPosition  发射源的z坐标位置
    /// emitterSize  发射源的尺寸大小
    /// emitterDepth  决定粒子形状
    /// emitterShape  粒子发射源的形状
    /// emitterMode  粒子发射模式
    /// renderMode  渲染模式
    /// velocity  粒子速度
    /// scale  粒子的缩放比例
    /// spin  自旋转速度
    /// seed  用于初始化随机数产生的种子

看完CAEmitterLayer的属性,是不是觉得很简单?年轻的我也是这么想的,看完属性后我立马撸了一段代码,期待中的界面显示效果是这样的。

然而事实无情的嘲讽了我,Demo跑起来之后屏幕一片漆黑。

那么到底是哪里出了问题呢?好看的粒子特效为什么没有显示出来? 不甘心的我又回去看了一遍文档,最后发现了是因为CAEmitterLayeremitterCells属性还没有赋值。CAEmitterLayer看上去像是许多CAEmitterCell的容器,这些CAEmitierCell定义了一个个粒子效果。看来问题找到了,可以接着进行下一步改造。但是CAEmitterCell的属性让我看得很头疼,各位先看一下。

/*
     CAEmitterCell 属性介绍
     
     CAEmitterCell类代表从CAEmitterLayer射出的粒子;emitter cell定义了粒子发射的方向。
     
     alphaRange:  一个粒子的颜色alpha能改变的范围;
     
     alphaSpeed:粒子透明度在生命周期内的改变速度;
     
     birthrate:粒子参数的速度乘数因子;每秒发射的粒子数量
     
     blueRange:一个粒子的颜色blue 能改变的范围;
     
     blueSpeed: 粒子blue在生命周期内的改变速度;
     
     color:粒子的颜色
     
     contents:是个CGImageRef的对象,既粒子要展现的图片;
     
     contentsRect:应该画在contents里的子rectangle:
     
     emissionLatitude:发射的z轴方向的角度
     
     emissionLongitude:x-y平面的发射方向
     
     emissionRange;周围发射角度
     
     emitterCells:粒子发射的粒子
     
     enabled:粒子是否被渲染
     
     greenrange: 一个粒子的颜色green 能改变的范围;
     
     greenSpeed: 粒子green在生命周期内的改变速度;
     
     lifetime:生命周期
     
     lifetimeRange:生命周期范围      lifetime= lifetime(+/-) lifetimeRange
     
     magnificationFilter:不是很清楚好像增加自己的大小
     
     minificatonFilter:减小自己的大小
     
     minificationFilterBias:减小大小的因子
     
     name:粒子的名字
     
     redRange:一个粒子的颜色red 能改变的范围;
     
     redSpeed; 粒子red在生命周期内的改变速度;
     
     scale:缩放比例:
     
     scaleRange:缩放比例范围;
     
     scaleSpeed:缩放比例速度:
     
     spin:粒子旋转角度
     
     spinrange:粒子旋转角度范围
     
     style:不是很清楚:
     
     velocity:速度
     
     velocityRange:速度范围
     
     xAcceleration:粒子x方向的加速度分量
     
     yAcceleration:粒子y方向的加速度分量
     
     zAcceleration:粒子z方向的加速度分量
     
     emitterCells:粒子发射的粒子
     
     注意:粒子同样有emitterCells属性,也就是说粒子同样可以发射粒子。
     */

一大堆的属性而且全是数值,文档也没有可以参考的方向,没办法,只能一点点的尝试了。不过最后的结果还是比较让人满意的,折腾了一整天的时间,终于整出来一个热乎的烟花绽放动画。

实际动画效果没图片上显示的这么快,有兴趣的话可以复制代码查看效果。

    //烟花粒子特效
    static func showFireworks(_ view: UIView) {
        //分为3种粒子,子弹粒子,爆炸粒子,散开粒子
        let fireworkEmitter = CLEmitterLayer()
        fireworkEmitter.emitterPosition = CGPoint(x: screen_width / 2, y: screen_height)
        fireworkEmitter.emitterSize = CGSize(width: screen_width / 2, height: 0)
        fireworkEmitter.emitterMode = .outline
        fireworkEmitter.emitterShape = .line
        fireworkEmitter.renderMode = .additive
        fireworkEmitter.seed = (arc4random()%100)+1
        
        // Create the rocket
        let rocket = CAEmitterCell()
        rocket.birthRate = 1.0
        rocket.velocity = 500
        rocket.velocityRange = 100
        rocket.yAcceleration = 75
        rocket.lifetime = 1.02 //发射源存在时长
        //小圆球图片
        rocket.contents = UIImage.init(named: "dot")?.cgImage
        rocket.scale = 0.2
        rocket.color = UIColor.yellow.cgColor
        rocket.greenRange = 1.0 // different colors
        rocket.redRange = 1.0
        rocket.blueRange = 1.0
        rocket.spinRange = CGFloat(Double.pi) // slow spin
        
        //爆炸粒子
        let burst = CAEmitterCell()
        burst.birthRate = 1.0 // at the end of travel
        burst.velocity = 0 //速度为0
        burst.scale = 1 //大小
        burst.redSpeed = -1.5 // shifting
        burst.blueSpeed = 1.5
        burst.greenSpeed = 1.0
        burst.lifetime = 0.35 //存在时间
        
        //爆炸散开粒子
        let spark = CAEmitterCell()
        spark.scale = 0.5
        spark.birthRate = 400
        spark.velocity = 125
        spark.emissionRange = CGFloat(2 * Double.pi) // 360spark.yAcceleration = 75 // gravity
        spark.lifetime = 3
        //散开的粒子样式
        spark.contents = UIImage.init(named: "dot")?.cgImage
        spark.greenSpeed = -0.1
        spark.redSpeed = 0.4
        spark.blueSpeed = -0.1
        spark.alphaSpeed = -0.25
        spark.spin = CGFloat(2 * Double.pi)
        spark.spinRange = CGFloat(2 * Double.pi)
        
        // 3种粒子组合,可以根据顺序,依次烟花弹-烟花弹粒子爆炸-爆炸散开粒子
        fireworkEmitter.emitterCells = [rocket]
        rocket.emitterCells = [burst]
        burst.emitterCells = [spark]
        
        view.layer.addSublayer(fireworkEmitter)
    }

因为时间原因,对CALyer的剖析就先进行到这里,如果还有其他想了解的可以联系我,我会不定期更新Demo的内容,尽量为大家演示所有CALayer的派生类的功能。

Lottie助力程序员解放双手

现在我们已经能够上手实现一些比较简单的动画,但这种动画显然不能满足日常开发的需要,在实际开发中,产品和开发人员经常会发生下面的对话。

很多产品大佬不仅要求程序员开发的UI控件动起来,还要配合一些很玄妙的动效,这就需要开发人员拿出足够多的头发来应付。同时,开发中也许会存在沟通理解失误的场景,这种情况下,要推翻之前的逻辑重新架构的话对程序员来说也是不小的负担。那么,如何用最少的工作量实现高性能的酷炫动画呢? 俗话说,聪明的人要学会站在巨人的肩膀上奔跑。在实际的开发中,为了花更少的时间做出更高性能的动画,我们就需要借助一些工具来实现我们的需求,接下来我要向大家推荐一款能够解放程序员双手的开源框架--Lottie。**

简介

Lottie 是 Airbnb 开源的一套跨平台的、完整的动画效果解决方案,设计师可以使用 Adobe After Effects 设计出漂亮的动画之后,使用 Lottie 提供的 Bodymovin 插件将设计好的动画导出成 JSON 格式,就可以直接运用在 iOSAndroidWebReact Native之上,无需其他额外操作。 在iOS平台上,Lottie中的大部分动画实现都是通过CALayer来进行绘制,相比于其他第三方框架,Lottie在性能上有着天然的优势。

iOS中如何使用Lottie

  • 从本地加载动画资源
// 通过本地JSON文件加载动画资源
private func circleLoop() -> AnimationView? {
    let bundleJsonNameString = "circle-loop"
    animationView = AnimationView(name: bundleJsonNameString)
    animationView?.contentMode = .scaleAspectFill
    return animationView
}
  • 播放Lottie动画
// 播放、暂停、停止

// 从上一次的动画位置开始播放
animationView?.play()

//暂停动画播放
animationView?.pause()

//停止动画播放,此时动画进度重置为0
animationView?.stop()

Lottie动画的播放控制,除了常规的控制,还支持进度播放、帧播放。

// 直接播放到指定进度
animationView?.play(toProgress: 0.8)

// 从进度A播放到进度B
animationView?.play(fromProgress: 0.2, toProgress: 0.8, loopMode: .playOnce, completion: { (state) in
    //do anything
})

//直接设置当前进度
animationView?.currentProgress = 0.3

// 直接播放到指定帧
animationView?.play(toFrame: 30)

// 从A帧播放到B帧
animationView?.play(fromFrame: 5, toFrame: 26, loopMode: .playOnce, completion: { (state) in
    // do anything
})
  • 循环播放动画
// 设置循环播放
animationView?.loopMode = .loop
// 动画按原路径返回
animationView?.loopMode = .autoReverse

看起来是不是相当的简单,只需要调用框架提供的api,就可以轻松的实现复杂流畅的动画。而且一份设计文档能够多端通用,大大减少了项目开发的时间,对于开发人员和设计师来说,意义非凡!下面让我们看一下Lottie都能够实现哪些动画效果。

Lottie动画的应用场景

  • 动态启动页

  • 动态图标/按钮

  • 空白页

  • 加载/下拉刷新

  • Banner/弹框

  • 表情/礼物/动态贴纸

当然,实际开发中Lottie能够实现的动画远远不止这些,只要想象力足够丰富,几乎都可以通过Lottie来实现。 但是Lottie还存在一些缺陷,对AE的属性还不能够完全兼容,并且设计师如果想要在动画中添加mask效果,对于应用程序来说将会产生额外的性能开支。尽管如此,Lottie依然能够满足绝大部分的开发场景,为了能够让你开发的程序看起来更加酷炫,请尽快上手吧!

演示示例下载: AnimationDemo

参考文档:

iOS核心动画详解

Lottie官网

Lottie动画社区

Lottie GitHub地址-iOS

支付宝Lottie工具