iOS~核心动画

  • 核心动画的结构图如下:
    最近在研究OpenGL ES,但是学习的过程中有一些动画的实现,发下Core Animation 的底层也是使用OpenGL ES的原理来做的,就重新拾起iOS中动画的知识,再次记录下、增加记忆、防止遗忘!
    核心动画结构图.001.jpeg

在介绍动画操作之前我们必须先来了解一个动画中常用的对象CALayer。CALayer包含在QuartzCore框架中,这是一个跨平台的框架,既可以用在iOS中又可以用在Mac OS X中。在使用Core Animation开发动画的本质就是将CALayer中的内容转化为位图从而供硬件操作,所以要熟练掌握动画操作必须先来熟悉CALayer

Quartz 2D绘图时大家其实已经用到了CALayer,当利用drawRect:方法绘图的本质就是绘制到了UIView的layer(属性)中,可是这个过程大家在上一节中根本体会不到。但是在Core Animation中我们操作更多的则不再是UIView而是直接面对CALayer。下图描绘了CALayer和UIView的关系,在UIView中有一个layer属性作为根图层,根图层上可以放其他子图层,在UIView中所有能够看到的内容都包含在layer中:


CALayer和UIView.png

在iOS 中CALayer的设计主要是了为了内容展示和动画操作,CALayer本身并不包含在UIKit中,它不能响应事件。由于CALayer在设计之初就考虑它的动画操作功能,CALayer很多属性在修改时都能形成动画效果,这种属性称为“隐式动画属性”。但是对于UIView的根图层而言属性的修改并不形成动画效果,因为很多情况下根图层更多的充当容器的做用,如果它的属性变动形成动画效果会直接影响子图层。另外,UIView的根图层创建工作完全由iOS负责完成,无法重新创建,但是可以往根图层中添加子图层或移除子图层

  • UIView 的属性


    UIView部分属性图
  • 隐式属性动画的本质是这些属性的变动默认隐含了CABasicAnimation动画实现,详情大家可以参照Xcode帮助文档中“Animatable Properties”一节

  • 在CALayer中很少使用frame属性,因为frame本身不支持动画效果,通常使用bounds和position代替

  • CALayer中透明度使用opacity表示而不是alpha;中心点使用position而不是center

  • anchorPoint属性是图层的锚点,范围在(01,01)表示在x、y轴的比例,这个点永远可以同position(中心点)重合,当图层中心点固定后,调整anchorPoint
    即可达到调整图层显示位置的作用(因为它永远和position重合)

注意:下面着重介绍position和anchorPoint

图一

图二

  • anchorPoint、position、frame

anchorPoint的默认值为(0.5,0.5),也就是anchorPoint默认在layer的中心点。默认情况下,使用addSublayer函数添加layer时,如果已知layer的frame值,根据上面的结论,那么position的值便可以用下面的公式计算

position.x = frame.origin.x + 0.5 * bounds.size.width;  
position.y = frame.origin.y + 0.5 * bounds.size.height; 

里面的0.5是因为anchorPoint取默认值,更通用的公式应该是下面的:

position.x = frame.origin.x + anchorPoint.x * bounds.size.width;  
position.y = frame.origin.y + anchorPoint.y * bounds.size.height;

下面再来看另外两个问题,如果单方面修改layer的position位置,会对anchorPoint有什么影响呢?修改anchorPoint又如何影响position呢?
根据代码测试,两者互不影响,受影响的只会是frame.origin,也就是layer坐标原点相对superLayer会有所改变。换句话说,frame.origin由position和anchorPoint共同决定,上面的公式可以变换成下面这样的:

frame.origin.x = position.x - anchorPoint.x * bounds.size.width;  
frame.origin.y = position.y - anchorPoint.y * bounds.size.height;

这就解释了为什么修改anchorPoint会移动layer,因为position不受影响,只能是frame.origin做相应的改变,因而会移动layer。

在实际情况中,可能还有这样一种需求,我需要修改anchorPoint,但又不想要移动layer也就是不想修改frame.origin,那么根据前面的公式,就需要position做相应地修改。简单地推导,可以得到下面的公式:

positionNew.x = positionOld.x + (anchorPointNew.x - anchorPointOld.x)  * bounds.size.width  
positionNew.y = positionOld.y + (anchorPointNew.y - anchorPointOld.y)  * bounds.size.height

但是在实际使用没必要这么麻烦。修改anchorPoint而不想移动layer,在修改anchorPoint后再重新设置一遍frame就可以达到目的,这时position就会自动进行相应的改变。写成函数就是下面这样的:

- (void) setAnchorPoint:(CGPoint)anchorpoint forView:(UIView *)view{
  CGRect oldFrame = view.frame;
  view.layer.anchorPoint = anchorpoint;
  view.frame = oldFrame;
}
  • 总结:
    1、position是layer中的anchorPoint在superLayer中的位置坐标。
    2、互不影响原则:单独修改position与anchorPoint中任何一个属性都不影响另一个属性。
    3、frame、position与anchorPoint有以下关系:
frame.origin.x = position.x - anchorPoint.x * bounds.size.width;  
frame.origin.y = position.y - anchorPoint.y * bounds.size.height;

第2条的互不影响原则还可以这样理解:position与anchorPoint是处于不同坐标空间中的重合点,修改重合点在一个坐标空间的位置不影响该重合点在另一个坐标空间中的位置。
更详细介绍

更改图层大小、设置图层翻转的四种方法

import UIKit

let WIDTH : CGFloat = 150

class ViewController: UIViewController ,CALayerDelegate{

    override func viewDidLoad() {
        super.viewDidLoad()
 let size = UIScreen.main.bounds.size;
        
        let layer = CALayer.init()
        layer.bounds = CGRect.init(x: 0, y: 0, width: WIDTH, height: WIDTH)
        //设置中心点
        layer.position = CGPoint.init(x: size.width / 2 , y: size.height / 2)
        layer.backgroundColor = UIColor.red.cgColor
        layer.cornerRadius = WIDTH/2;
        layer.masksToBounds = true
//        layer.shadowColor = UIColor.gray.cgColor;//阴影颜色
//        layer.shadowOffset = CGSize.init(width: 2, height: 2)
//        layer.shadowOpacity = 0.9
        //方法二
        //利用图层解决倒立问题
//        layer.transform = CATransform3DMakeRotation(CGFloat(M_PI), 1, 0, 0);
        
        layer.borderColor = UIColor.white.cgColor
        layer.borderWidth = 2.0
        
        layer.delegate = self
        
        self.view.layer.addSublayer(layer)
        //调用图层setNeedDisplay 否则代理方法不会被调用
        layer.setNeedsDisplay()
        
        //方法三
//        let image:UIImage = UIImage.init(named: "kunkun.jpg")!
//        layer.contents = image.cgImage
        
        //方法四
        layer.setValue(Double.pi, forKeyPath:"transform.rotation.x")
         }
 func draw(_ layer: CALayer, in ctx: CGContext) {
        ctx.saveGState()
        
        //方法一
        //通过指定 x,y 缩放因子,可以倒转 x轴 和 y轴
//        ctx.scaleBy(x: 1, y: -1)
        //沿着x轴 y轴进行平移操作
//        ctx.translateBy(x: 0, y: -WIDTH)
        
        let image:UIImage = UIImage.init(named: "kunkun.jpg")!
        ctx.draw((image.cgImage!), in: CGRect.init(x: 0, y: 0, width: WIDTH, height: WIDTH))
    
        ctx.restoreGState()
    }
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = (touches as NSSet).anyObject()
        let layer = self.view.layer.sublayers![0]
        var width = layer.bounds.size.width
        if width == WIDTH {
            width = WIDTH * 4
        }else{
            width = WIDTH
        }
        layer.bounds = CGRect.init(x: 0, y: 0, width: width, height: width)
        layer.position = (touch as! UITouch).location(in:self.view)     //获取当前点击位置
        layer.cornerRadius = width / 2
    }
}

使用自定义图层绘图

在自定义图层中绘图时只要自己编写一个类继承于CALayer然后在drawInContext:中绘图即可。同前面在代理方法绘图一样,要显示图层中绘制的内容也要调用图层的setNeedDisplay方法,否则drawInContext方法将不会调用。

前面的文章中曾经说过,在使用Quartz 2D在UIView中绘制图形的本质也是绘制到图层中,为了说明这个问题下面演示自定义图层绘图时没有直接在视图控制器中调用自定义图层,而是在一个UIView将自定义图层添加到UIView的根图层中(例子中的UIView跟自定义图层绘图没有直接关系)。从下面的代码中可以看到:UIView在显示时其根图层会自动创建一个CGContextRef(CALayer本质使用的是位图上下文),同时调用图层代理(UIView创建图层会自动设置图层代理为其自身)的draw: inContext:方法并将图形上下文作为参数传递给这个方法。而在UIView的draw:inContext:方法中会调用其drawRect:方法,在drawRect:方法中使用UIGraphicsGetCurrentContext()方法得到的上下文正是前面创建的上下文

  • 自定义Layer
//  Created by apple on 2019/11/28.
//  Copyright © 2019年 apple. All rights reserved.
//

import UIKit

class SFLayer: CALayer {
    override func draw(in ctx: CGContext) {
        ctx.setFillColor(UIColor.init(red: 1.0, green: 0.0, blue: 0.0, alpha: 1).cgColor)
        ctx.setStrokeColor(UIColor.init(red: 0.0, green: 1.0, blue: 0.0, alpha: 1).cgColor)
        
        //起点位置
        ctx.move(to: CGPoint.init(x: 94.5, y: 33.5))
        
        //开始画线
        ctx.addLine(to: CGPoint.init(x: 104.02, y: 47.39))
        ctx.addLine(to: CGPoint.init(x: 120.18, y: 52.16))
        ctx.addLine(to: CGPoint.init(x: 109.9, y: 65.51))
        ctx.addLine(to: CGPoint.init(x: 110.37, y: 82.34))
        ctx.addLine(to: CGPoint.init(x: 94.5, y: 76.7))
        ctx.addLine(to: CGPoint.init(x: 78.63, y: 82.34))
        ctx.addLine(to: CGPoint.init(x: 79.09, y: 65.51))
        ctx.addLine(to: CGPoint.init(x: 68.82, y: 52.16))
        ctx.addLine(to: CGPoint.init(x: 84.98, y: 47.39))
        ctx.closePath()
        
        ctx.drawPath(using: CGPathDrawingMode.fill)
        
    }
}
  • 自定义View
import UIKit

class SFView: UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.setupUI()
    }
 
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func draw(_ rect: CGRect) {
        
    }
    func setupUI() {
        let layer = SFLayer.init()
        layer.bounds = CGRect.init(x: 0, y: 0, width: 185, height: 185)
        /// position是相对于父视图的
        layer.position = CGPoint.init(x: 200, y: 300)
        layer.backgroundColor = UIColor.init(red: 0, green: 246/255, blue: 1.0, alpha: 1).cgColor;
        
        //显示图层
        layer.setNeedsDisplay()
        self.layer.addSublayer(layer)
    }
    override func draw(_ layer: CALayer, in ctx: CGContext) {
        super.draw(layer, in: ctx)
    }
}

  • 控制器显示
import UIKit

let WIDTH : CGFloat = 150

class ViewController: UIViewController ,CALayerDelegate{

    override func viewDidLoad() {
        super.viewDidLoad()
        let view = SFView.init(frame: UIScreen.main.bounds)
        view.backgroundColor = UIColor.init(red: 249/255, green: 249/255, blue: 249/255, alpha: 1)
        self.view.addSubview(view)
      }
}
  • 添加帧动画
   /*
         如果不使用UIView封装的动画,动画创建一般分为以下几个步骤:
         1.初始化动画并设置动画属性
         2.设置动画属性初始值(可以省略)结束值以及其他属性
         3.给图层添加动画
         */
        
        self.view.backgroundColor = UIColor.init(patternImage: image!)
        //自定义一个图层
        _layer = CALayer.init()
        _layer?.bounds = CGRect.init(x: 0, y: 0, width: 10, height: 20)
        _layer?.position = CGPoint.init(x: 50, y: 150)
        _layer?.contents = UIImage.init(named: "kunkun.jpg")?.cgImage
        self.view.layer.addSublayer(_layer!)


 override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = (touches as NSSet).anyObject()
//        let layer = self.view.layer.sublayers![0]
//        var width = layer.bounds.size.width
//        if width == WIDTH {
//            width = WIDTH * 4
//        }else{
//            width = WIDTH
//        }
//        layer.bounds = CGRect.init(x: 0, y: 0, width: width, height: width)
//        layer.position = (touch as! UITouch).location(in:self.view)     //获取当前点击位置
//        layer.cornerRadius = width / 2
        
        let point = (touch as! UITouch).location(in:self.view)
        
        self.translationAnimation(point: point)
        
    }
    
    func translationAnimation(point location:CGPoint) {
        //1.创建动画,并制定动画属性
        let basicAnimation = CABasicAnimation.init(keyPath: "position")
        //2.设置动画属性初始值和结束值
        basicAnimation.toValue = NSValue.init(cgPoint: location)
        //设置其他动画属性
        basicAnimation.duration = 5.0
        basicAnimation.repeatCount = HUGE
        // 3.添加动画到图层 ,注意key相当于给动画进行命名,以后获得该动画时可以使用此名称获取
        _layer?.add(basicAnimation, forKey: "KCBasicAnimation_Translation")
        
    }

最后的最后附上Demo

CALayer.gif