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

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