iOS动画集锦(Core Animation)

iOS 动画主要是指 Core Animation 框架, Core AnimationiOSOS X 平台上负责图形渲染与动画的基础框架。Core Animation 可以作用于动画视图或者其他可视元素,可以完成动画所需的大部分绘帧工作。Core Animation 系统已经进行了封装, 所以在使用的时候你只需要配置少量的动画参数(如开始点的位置和结束点的位置)即可使用 Core Animation 的多种动画效果。Core Animation 将大部分实际的绘图任务交给了图形硬件(GPU)来处理,图形硬件会加速图形渲染的速度。这种自动化的图形加速技术让动画拥有更高的帧率并且显示效果更加平滑,不会加重CPU的负担而影响程序的运行速度。

本文主要总结下平时常用的动画, 如: 基础动画(CABasicAnimation)、关键帧动画(CAKeyframeAnimation)、组动画(CAAnimationGroup)、过渡动画(CATransition), 最后也扩展了下, 做了进度条、贝塞尔曲线画心❤️、弹球、钉钉效果、点赞等动画,希望对大家有所帮助.
github: https://github.com/YTiOSer/YTAnimation

Core Animation.jpeg

一、Core Animation类简介

  1. 首先通过官方的 Core Animation 类图了解下各个类之间的关系. 官网链接:Core Animation

    Core Animation.png

    建议详细看下上图, 这里对 CAAnimation 的子类和相互关系及属性介绍的比较详细, 看完后会对各个动画类型有个大概的了解.

  2. 接下来详细介绍下动画的各个属性及作用

  • fromValue: 动画的开始值(Any类型, 根据动画不同可以是CGPoint、NSNumber等)
  • toValue: 动画的结束值, 和fromValue类似
  • beginTime: 动画的开始时间
  • duration : 动画的持续时间
  • repeatCount : 动画的重复次数
  • fillMode: 动画的运行场景
  • isRemovedOnCompletion: 完成后是否删除动画
  • autoreverses: 执行的动画按照原动画返回执行
  • path:关键帧动画中的执行路径
  • values: 关键帧动画中的关键点数组
  • animations: 组动画中的动画数组
  • delegate : 动画代理, 封装了动画的执行和结束方法
  • timingFunction: 控制动画的显示节奏, 系统提供五种值选择,分别是:
    1.kCAMediaTimingFunctionDefault( 默认,中间快)
    2.kCAMediaTimingFunctionLinear (线性动画)
    3.kCAMediaTimingFunctionEaseIn (先慢后快 慢进快出)
    4.kCAMediaTimingFunctionEaseOut (先块后慢快进慢出)
    5.kCAMediaTimingFunctionEaseInEaseOut (先慢后快再慢)
  • type: 过渡动画的动画类型,系统提供了多种过渡动画, 分别是:
    1: fade (淡出 默认)
    2: moveIn (覆盖原图)
    3: push (推出)
    4: fade (淡出 默认)
    5: reveal (底部显示出来)
    6: cube (立方旋转)
    7: suck (吸走)
    8: oglFlip (水平翻转 沿y轴)
    9: ripple (滴水效果)
    10: curl (卷曲翻页 向上翻页)
    11: unCurl (卷曲翻页返回 向下翻页)
    12: caOpen (相机开启)
    13: caClose (相机关闭)
  • subtype : 过渡动画的动画方向, 系统提供了四种,分别是:
    1.fromLeft( 从左侧)
    2.fromRight (从右侧)
    3.fromTop (有上面)
    4.fromBottom (从下面)

二、Core Animation的使用

1. 基础动画( CABasicAnimation )

基础动画主要提供了对于CALayer对象中的可变属性进行简单动画的操作。比如:位移、旋转、缩放、透明度、背景色等。
基础动画根据 keyPath 来区分不同的动画,, 系统提供了多个类型,如: transform.scale (比例转换)、transform.scale.xtransform.scale.ytransform.rotation(旋转) 、transform.rotation.x(绕x轴旋转)、transform.rotation.y(绕y轴旋转)、transform.rotation.z(绕z轴旋转)、opacity (透明度)、marginbackgroundColor(背景色)、cornerRadius(圆角)、borderWidth(边框宽)、boundscontentscontentsRectcornerRadiusframehiddenmaskmasksToBoundsshadowColor(阴影色)、shadowOffsetshadowOpacityshadowOpacity, 在使用时候, 需要根据具体的需求选择合适的.

效果图如下:


旋转.gif
  • 位移动画
 func positionAnimation() {
        
        let animation = CABasicAnimation.init(keyPath: "position") //keyPath为系统提供
        animation.fromValue = CGPoint.init(x: margin_ViewMidPosition, y: kScreenH / 2 - margin_Top)
        animation.toValue = CGPoint.init(x: kScreenW - margin_ViewMidPosition, y: kScreenH / 2 - margin_Top)
        animation.duration = 1.0
        view_Body.layer.add(animation, forKey: "positionAnimation") //key自定义
    }
  • 旋转动画:
    func rotateAnimation() {
        
        let animation = CABasicAnimation.init(keyPath: "transform.rotation.z")
        animation.toValue = NSNumber.init(value: Double.pi)
        animation.duration = 0.1
        animation.repeatCount = 1e100 //无限大重复次数
        view_Body.layer.add(animation, forKey: "rotateAnimation")
    }
  • 缩放动画
 func scaleAnimation() {
        
        let animation = CABasicAnimation.init(keyPath: "transform.scale")
        animation.toValue = NSNumber.init(value: 2.0)
        animation.duration = 1.0
        view_Body.layer.add(animation, forKey: "scaleAnimation")
    }
  • 透明度动画
func opacityAnimation() {
        
        let animation = CABasicAnimation.init(keyPath: "opacity")
        animation.fromValue = NSNumber.init(value: 1.0)
        animation.toValue = NSNumber.init(value: 0.0)
        animation.duration = 1.0
        view_Body.layer.add(animation, forKey: "opacityAnimation")
    }
  • 背景色动画
 func backgroundColorAnimation() {
        
        let animation = CABasicAnimation.init(keyPath: "backgroundColor")
        animation.toValue = UIColor.green.cgColor //因为layer层动画, 所以需要使用cgColor
        animation.duration = 1.0
        view_Body.layer.add(animation, forKey: "backgroundColorAnimation")
    }

2. 关键帧动画( CAKeyframeAnimation )

CAKeyframeAnimationCABasicAnimation 都属于CAPropertyAnimatin 的子类。不同的是 CABasicAnimation 只能从一个数值(fromValue)变换成另一个数值(toValue),而 CAKeyframeAnimation 则会使用一个数组(values) 保存一组关键帧, 也可以给定一个路径(path)制作动画。

CAKeyframeAnimation主要有 三个 重要属性:

  • values:存放关键帧(keyframe)的数组,动画对象会在指定的时间(duration)内,依次显示values数组中的每一个关键帧 .
  • path:可以设置一个 CGPathRefCGMutablePathRef,让层跟着路径移动. path 只对 CALayeranchorPointposition 起作用, 如果设置了path,那么values将被忽略.
  • keyTimes:可以为对应的关键帧指定对应的时间点,其取值范围为0到1.0, keyTimes 中的每一个时间值都对应 values 中的每一帧.当 keyTimes 没有设置的时候,各个关键帧的时间是根据 duration 平分的。

以抖动截图为例, 效果图如下:


抖动.gif

动画代码如下:

  • 关键帧动画
func keyFrameAnimation() {
        
        let animation = CAKeyframeAnimation.init(keyPath: "position")
        let value_0 = CGPoint.init(x: margin_ViewMidPosition, y: kScreenH / 2 - margin_ViewWidthHeight)
        let value_1 = CGPoint.init(x: kScreenW / 3, y: kScreenH / 2 - margin_ViewWidthHeight)
        let value_2 = CGPoint.init(x: kScreenW / 3, y: kScreenH / 2 + margin_ViewMidPosition)
        let value_3 = CGPoint.init(x: kScreenW * 2 / 3, y: kScreenH / 2 + margin_ViewMidPosition)
        let value_4 = CGPoint.init(x: kScreenW * 2 / 3, y: kScreenH / 2 - margin_ViewWidthHeight)
        let value_5 = CGPoint.init(x: kScreenW - margin_ViewMidPosition, y: kScreenH / 2 - margin_ViewWidthHeight)
        animation.values = [value_0, value_1, value_2, value_3, value_4, value_5]
        animation.duration = 2.0
        view_Body.layer.add(animation, forKey: "keyFrameAnimation")
    }
  • 路径动画
func pathAnimation() {
        
        let animation = CAKeyframeAnimation.init(keyPath: "position")
        let path = UIBezierPath.init(arcCenter: CGPoint.init(x: kScreenW / 2, y: kScreenH / 2), radius: 60, startAngle: 0.0, endAngle: .pi * 2, clockwise: true)
        animation.duration = 2.0
        animation.path = path.cgPath
        view_Body.layer.add(animation, forKey: "pathAnimation")
    }
  • 抖动动画
func shakeAnimation() {
        
        let animation = CAKeyframeAnimation.init(keyPath: "transform.rotation")
        let value_0 = NSNumber.init(value: -Double.pi / 180 * 8)
        let value_1 = NSNumber.init(value: Double.pi / 180 * 8)
        animation.values = [value_0, value_1, value_0]
        animation.duration = 1.0
        animation.repeatCount = 1e100
        view_Body.layer.add(animation, forKey: "shakeAnimation")
    }

3. 组动画( CAAnimationGroup )

CAAnimationGroupCAAnimation 的子类,可以保存一组动画对象,可以保存基础动画、关键帧动画等,数组中所有动画对象可以同时并发运行, 也可以通过实践设置为串行连续动画.

效果截图如下:


组动画2.gif

动画代码如下:

  • 同时
   //同时
    func sameTimeAnimation() {
        
        let animation_Position = CAKeyframeAnimation.init(keyPath: "position")
        let value_0 = CGPoint.init(x: margin_ViewMidPosition, y: kScreenH / 2 - margin_ViewMidPosition)
        let value_1 = CGPoint.init(x: kScreenW / 3, y: kScreenH / 2 - margin_ViewMidPosition)
        let value_2 = CGPoint.init(x: kScreenW / 3, y: kScreenH / 2 + margin_ViewMidPosition)
        let value_3 = CGPoint.init(x: kScreenW / 3 * 2, y: kScreenH / 2 + margin_ViewMidPosition)
        let value_4 = CGPoint.init(x: kScreenW / 3 * 2, y: kScreenH / 2 - margin_ViewMidPosition)
        let value_5 = CGPoint.init(x: kScreenW - margin_ViewMidPosition, y: kScreenH / 2 - margin_ViewMidPosition)
        animation_Position.values = [value_0, value_1, value_2, value_3, value_4, value_5]
        
        let animation_BGColor = CABasicAnimation.init(keyPath: "backgroundColor")
        animation_BGColor.toValue = UIColor.green.cgColor
        
        let animation_Rotate = CABasicAnimation.init(keyPath: "transform.rotation")
        animation_Rotate.toValue = NSNumber.init(value: Double.pi * 4)
        
        let animation_Group = CAAnimationGroup()
        animation_Group.animations = [animation_Position, animation_BGColor, animation_Rotate]
        animation_Group.duration = 4.0
        view_Body.layer.add(animation_Group, forKey: "groupAnimation")
    }
  • 连续
 //连续动画 最主要的是处理好各个动画时间的衔接
    func goOnAnimation() {
        
        //定义一个动画开始的时间
        let currentTime = CACurrentMediaTime()
        
        let animation_Position = CABasicAnimation.init(keyPath: "position")
        animation_Position.fromValue = CGPoint.init(x: margin_ViewMidPosition, y: kScreenH / 2)
        animation_Position.toValue = CGPoint.init(x: kScreenW / 2, y: kScreenH / 2)
        animation_Position.duration = 1.0
        animation_Position.fillMode = "forwards" //只在前台
        animation_Position.isRemovedOnCompletion = false //切出界面再回来动画不会停止
        animation_Position.beginTime = currentTime
        view_Body.layer.add(animation_Position, forKey: "positionAnimation")
        
        let animation_Scale = CABasicAnimation.init(keyPath: "transform.scale")
        animation_Scale.fromValue = NSNumber.init(value: 0.7)
        animation_Scale.toValue = NSNumber.init(value: 2.0)
        animation_Scale.duration = 1.0
        animation_Scale.fillMode = "forwards"
        animation_Scale.isRemovedOnCompletion = false
        animation_Scale.beginTime = currentTime + 1.0
        view_Body.layer.add(animation_Scale, forKey: "scaleAnimation")
        
        let animation_Rotate = CABasicAnimation.init(keyPath: "transform.rotation")
        animation_Rotate.toValue = NSNumber.init(value: Double.pi * 4)
        animation_Rotate.duration = 1.0
        animation_Rotate.fillMode = "forwards"
        animation_Rotate.isRemovedOnCompletion = false
        animation_Rotate.beginTime = currentTime + 2.0
        view_Body.layer.add(animation_Rotate, forKey: "rotateAnimation")
    }

4. 过渡动画( CATransition )

CATransitionCAAnimation 的子类,用于做过渡动画或者 转场 动画,能够为层提供移出屏幕和移入屏幕的动画效果。

过渡动画通过 type 设置不同的动画效果, CATransition 有多种过渡效果, 但其实 Apple 官方的SDK只提供了种:

  • fade 淡出 默认
  • moveIn 覆盖原图
  • push 推出
  • reveal 底部显示出来

私有API提供了其他很多非常炫的过渡动画,如 cube(立方旋转)、suckEffect(吸走)、oglFlip(水平翻转 沿y轴)、 rippleEffect(滴水效果)、pageCurl(卷曲翻页 向上翻页)、pageUnCurl(卷曲翻页 向下翻页)、cameraIrisHollowOpen(相机开启)、cameraIrisHollowClose(相机关闭)等。

: 因 Apple 不提供维护,并且有可能造成你的app审核不通过, 所以不建议开发者们使用这些私有API.

效果如下:


过渡动画.gif

翻页动画代码如下:

 func curlAnimation() {
        
        let animation_Curl = CATransition()
        animation_Curl.type = "pageCurl"
        animation_Curl.subtype = "fromRight"
        animation_Curl.duration = 1.0
        view_Body.layer.add(animation_Curl, forKey: "curlAnimation")
    }

5. 项目案例

  1. 进度条
    效果如下:
    进度条.gif

    这里主要用到了 CAShapeLayer + CAGradientLayer, 使用 CAGradientLayer 画进度圈(GPU执行, 高效), 然后使用 CAGradientLayer 渐变色layer, 结合动画显示进度条.
    代码如下:
  • UI视图
func createView() {
        
        label_Progress = UILabel()
        label_Progress.text = ""
        label_Progress.textAlignment = .center
        label_Progress.font = UIFont.systemFont(ofSize: 25)
        addSubview(label_Progress)
        label_Progress.snp.makeConstraints { (make) in
            make.centerX.centerY.equalTo(self)
            make.width.equalTo(kScreenW)
            make.height.equalTo(30)
        }
        
        layer_BackPath = CAShapeLayer()
        layer_BackPath.fillColor = UIColor.clear.cgColor //填充颜色
        layer_BackPath.strokeColor = UIColor.white.withAlphaComponent(0.5).cgColor //划线颜色
        layer_BackPath.lineWidth = width_MainPath
        layer.addSublayer(layer_BackPath)
        
        layer_MainPathLayer = CAShapeLayer()
        layer_MainPathLayer.fillColor = UIColor.clear.cgColor
        layer_MainPathLayer.strokeColor = UIColor.white.cgColor
        layer_MainPathLayer.lineWidth = width_MainPath
        layer.addSublayer(layer_MainPathLayer)
        
        //渐变色
        layer_Gradient = CAGradientLayer()
        layer_Gradient.frame = CGRect.init(x: 0, y: 0, width: kScreenW, height: kScreenH)
        layer_Gradient.type = "axial" //线性变化  默认目前只有这一个type
        layer_Gradient.colors = [UIColor.init(hex: 0xf31414).cgColor, UIColor.init(hex: 0xf27200).cgColor, UIColor.init(hex: 0xffff00).cgColor, UIColor.init(hex: 0x2bee22).cgColor, UIColor.init(hex: 0x32a7eb).cgColor]
        layer_Gradient.locations = [0, 0.3, 0.5, 0.7, 1] //每个渐变颜色的终止位置,这些值必须是递增的,数组的长度和colors的长度最好一致
        //startPoint endPoint 分别表示渐变层的起始位置和终止位置,这两个点被定义在一个单元坐标空间,[0,0]表示左上角位置,[1,1]表示右下角位置,默认值分别是[.5,0] and [.5,1];
        layer_Gradient.startPoint = CGPoint.init(x: 0, y: 0)
        layer_Gradient.endPoint = CGPoint.init(x: 1, y: 0)
        layer.addSublayer(layer_Gradient)
        
    }
  • 进度
func drawCircle(){
        
        //贝塞尔曲线画圆
        let path_Back = UIBezierPath.init(arcCenter: CGPoint.init(x: kScreenW / 2, y: kScreenH / 2), radius: kScreenW / 5 - width_MainPath, startAngle: CGFloat.pi, endAngle: CGFloat.pi * 3, clockwise: true)
        let path_Main = UIBezierPath.init(arcCenter: CGPoint.init(x: kScreenW / 2, y: kScreenH / 2), radius: kScreenW / 5 - width_MainPath + 3, startAngle: CGFloat.pi, endAngle: CGFloat.pi * 3, clockwise: true)
        
        layer_BackPath.path = path_Back.cgPath
        layer_MainPathLayer.path = path_Main.cgPath
        
        layer_Gradient.mask = layer_MainPathLayer //用 layer_MainPathLayer 截取渐变层
        
        //动画 显示路径
        let animation = CABasicAnimation.init(keyPath: "strokeEnd")
        animation.duration = CFTimeInterval(Double(progress) * 0.01)
        animation.fromValue = NSNumber.init(value: 0)
        animation.toValue = NSNumber.init(value: Double(progress) * 0.01)
        animation.fillMode = "forwards"
        animation.isRemovedOnCompletion = false //完成后不删除动画
        layer_MainPathLayer.add(animation, forKey: "strokeEndAnimation")
        
        if progress > 0{
            DispatchQueue.global().async {
                self.timer_ProgressLabel = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(YTProgressView.progressLabelTimerAction), userInfo: nil, repeats: true)
                RunLoop.current.run()
            }
        }else{
            label_Progress.text = "0%"
        }
        
    }
    
    func progressLabelTimerAction() {
        
        DispatchQueue.main.async {
            self.label_Progress.text = String(self.num_Progress) + "%"
        }
        
        if num_Progress >= progress{ //销毁计时器
            timer_ProgressLabel.invalidate()
            timer_ProgressLabel = nil
        }else{
            num_Progress += 1
        }
        
    }

这里只展示了核心代码, 详细代码可到github下载完整代码: https://github.com/YTiOSer/YTAnimation

  1. 弹球, 仿Path菜单效果
  • 点击红色按钮,红色按钮旋转。(旋转动画
  • 黑色小按钮依次弹出,并且带有旋转效果。(位移动画、旋转动画、组动画
  • 点击黑色小按钮,其他按钮消失,被点击的黑色按钮变大变淡消失。(缩放动画、alpha动画、组动画
    tanqiu.gif
  1. 仿钉钉菜单效果


    dingding.png

    动画实现用到了位移动画和缩放动画, 其实不难.

  2. 点赞


    点赞.gif

三、总结

看完整篇文章相信你对 iOS 中的动画有了一个详细的了解, 其实单个动画都是比较简单的, 而复杂的动画其实都是由一个个简单的动画组装而成的,所以遇到比较难得动画需求, 我们只要充分组装不同的动画,就能实现出满意的效果.

好记性不如烂笔头, 光说不练假把戏, 建议大家结合我的代码, 自己边看边练习, 这样才能记得牢, 才能转换成自己的知识.

github: https://github.com/YTiOSer/YTAnimation

如果觉得对你还有些用,给个喜欢关注吧。你的支持是我继续的动力。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容