04-iOS 多媒体技术| Core Animation要点回顾1【CAlayer与UIView树、核心动画、Core Animation渲染原理等】

前言

我们在前面,首先进行了针对 iOS中的多媒体技术相关几个框架概述:

  1. 进而 用 两篇文章 对 其中的 UIKit相关要点 进行了分述:
  2. 我们 在此篇文章 ,将 针对 Core Animation框架的要点 进一步展开分述:

一、Core Animation简介

1. 官方介绍

依照惯例,我们首先引入苹果官方文档对其的介绍:

Render, compose, and animate visual elements.
OverView

Core Animation provides high frame rates and smooth animations without burdening the CPU or slowing down your app. Core Animation does most of the work of drawing each frame of an animation for you. You're responsible for configuring the animation parameters, such as the start and end points, and Core Animation does the rest. It accelerates the rendering by handing over most of the work to dedicated graphics hardware. For more details, see Core Animation Programming Guide.

  • Core Animation 提供高帧速率和流畅的动画,不会增加 CPU 负担或降低应用程序速度。
  • 核心动画为您完成绘制动画每一帧的大部分工作
    • 开发者只需要负责配置动画参数,例如起点终点,核心动画会完成其余的工作。
    • 它通过将大部分工作移交给专用图形硬件来加速渲染。
    • 有关更多详细信息,请参阅 Core Animation Programming Guide

Core Animation核心要点

我们打开 核心动画的 编程指南 Core Animation Programming Guide ,我们可以看到,关于 Core Animation的介绍有很多章节,他们可以扼要概括为:

  • Core Animation 是 iOS 和 OS X 上都可用的图形渲染动画基础结构
  • Core Animation 的核心对象是 layer对象 (CAlayer以及它的派生类)

2. 结合项目实战理解

我在 探索 iOS图层渲染原理 的时候也对Core Animation做过注解:

  • Core Animation,它本质上可以理解为一个复合引擎主要职责包含:渲染、构建和实现动画
  • 通常我们会使用 Core Animation 来高效、方便地实现动画,但是实际上它的前身叫做Layer Kit,关于动画实现只是它功能中的一部分
  • 对于 iOS app,不论是否直接使用了 Core Animation,它都在底层深度参与了 app 的构建
  • Core Animation 是 AppKit 和 UIKit 完美的底层支持,同时也被整合进入 Cocoa 和 Cocoa Touch 的工作流之中,它是 app 界面渲染和构建的最基础架构
  • Core Animation 的职责就是尽可能快地组合屏幕上不同的可视内容,这个内容是被分解成独立的 layer(iOS 中具体而言就是 CALayer),并且被存储为树状层级结构。
  • 这个树也形成了 UIKit 以及在 iOS 应用程序当中我们所能在屏幕上看见的一切的基础。

接下来我们就围绕 layer对象图形渲染动画 这三个要点,逐步展开对 Core Animation 框架 的回顾

二、核心对象layer

  • Core Animation 本身并不是绘图系统
    它是用于在硬件中合成和操作应用程序内容的基础设施。
  • Core Animation 作为图形渲染动画的基础设施,其核心是 layer 对象
    • 开发者可以使用layer来管理和操作内容。
    • layer将您的内容捕获到可以由图形硬件轻松操作的位图中。
    • 在大多数应用程序中,layer用作管理视图内容的方式
    • 开发者也可以根据需要创建独立图层。
    • 修改layer中的属性触发动画
      • 与View一样,图层对象也有一个边界矩形、屏幕上的位置不透明度transform以及许多其他可以修改的面向视觉的属性。
      • 更改属性值会导致创建隐式动画
    • layer 可以像 View一样 有多个层级
    • ...

1. layer对象简介

  • layer对象是在 3D 空间中组织的 2D 表面,是使用 Core Animation 所做的一切的核心。
  • 与View类似:
    • layer管理有关其表面的几何形状内容视觉属性的信息。
  • 与View不同:
    • layer不定义自己的外观
    • layer仅管理位图周围的状态信息。
    • 位图本身可以是视图绘制本身的结果,也可以是指定的UIImage
    • 因此,在App中使用的主要layer被视为模型对象
      • 因为它们主要管理数据。因为它会影响动画的行为。

1.1 layer对象 作为 绘图动画的基础

1.1.1 基于layer的绘图模型

1.1.1 Core Animation绘制内容过程:

layer利用硬件加速渲染

  • layer对象捕获应用程序提供的内容并将其缓存在位图中。当我们更改图层的属性时,更改的是与layer对象关联的状态信息。
  • 当更改触发动画时,Core Animation 会将layer的位图和状态信息传递给图形硬件,图形硬件将使用新信息进行渲染位图的工作
  • 操作的是静态位图

view在主线程上使用 CPU 渲染

  • 对view本身的更改通常会导致调用View的drawRect:方法以使用新参数重新绘制内容。
  • 但这种方式的绘图成本很高,因为它是在主线程上使用 CPU 来完成
1.1.2 CALayer是显示的基础

我们在探索 iOS图层渲染原理,关于这部分,已经做过详尽阐述,在这里我们直接归纳结论:

CALayer 是显示的基础:存储 bitmap

  • CALayer 有一个属性 contents
  • contents 属性保存了由设备渲染流水线渲染好的位图 bitmap(通常也被称为 backing store
    • 而当设备屏幕进行刷新时,会从 CALayer 中读取生成好的 bitmap,进而呈现到屏幕上
    • 在代码中对 CALayer 的 contents 属性进行了设置:
  • An object providing the contents of the layer, typically a CGImageRef.
    • contents 提供了 layer 的内容,是一个指针类型
      • 在 iOS 中的类型就是 CGImageRef(在 OS X 中还可以是 NSImage)。
      • Apple 对 CGImageRef 的定义是:A bitmap image or image mask.
  • 那么在运行时,操作系统会调用底层的接口,将 image 通过 CPU+GPU 的渲染流水线渲染得到对应的 bitmap,存储于 CALayer.contents 中,在设备屏幕进行刷新的时候就会读取 bitmap 在屏幕上呈现。
  • 也正因为每次要被渲染的内容是被静态的存储起来的,所以每次渲染时,Core Animation 会触发调用 drawRect: 方法,使用存储好的 bitmap 进行新一轮的展示

1.1.2 基于layer的动画

  • layer对象的数据和状态信息(属性值)与该layer内容在屏幕上的视觉呈现分离。 我们可以通过修改layer对象的属性值,来实现动画
  • 在动画过程中,Core Animation 会在硬件中完成所有逐帧绘制。 我们只需要指定动画参数,如:动画的起点和终点、自定义计时信息和等
  • layer中的可动画属性:
    • anchorPoint
    • backgroundColor
    • backgroundFilters
    • borderColor
    • borderWidth
    • bounds
    • compositingFiltercontents
    • contentsRect
    • cornerRadius
    • doubleSided
    • filtersframehidden
    • mask
    • masksToBounds
    • opacity
    • position
    • shadowColor
    • shadowOffset
    • shadowOpacity
    • shadowPath
    • shadowRadius
    • sublayers
    • sublayerTransform
    • transform
    • zPosition

1.2 layer对象定义自己的几何形状

视觉几何包含有关该内容的borderboundspositiontransform(旋转、缩放或变换)、shadow

1.2.1 两种类型的坐标系

使用layer对象开发过程中,我们会涉及到两套坐标系: 点坐标系 , 单位坐标系 。 其中点坐标系 和我们在用 UIKIt中的view开发时,相差无几

1. 点坐标系

  • 指定layer的大小和位置,分别使用boundsposition属性

  • 定义bounds图层本身的坐标系并包含图层在屏幕上的大小。该position属性定义图层相对于其父级坐标系的位置。

    objc 复制代码
    @interface CALayer : NSObject <NSSecureCoding, CAMediaTiming>
    ...
    /** Geometry and layer hierarchy properties. **/
    
    /* The bounds of the layer. Defaults to CGRectZero. Animatable. */
    
    @property CGRect bounds;
    
    /* The position in the superlayer that the anchor point of the layer's
     * bounds rect is aligned to. Defaults to the zero point. Animatable. */
    
    @property CGPoint position;
    ...
    @end
  • 需要注意的一件事是该position属性位于图层的中间。该属性是其定义根据图层anchorPoint属性中的值而变化的多个属性之一

2. 单位坐标系

核心动画使用单位坐标来表示属性,这些属性的值可能会随着图层大小的变化而变化。

  • 锚点anchorPoint是我们使用单位坐标系指定的多个属性之一。
  • 可以将单位坐标视为指定总可能值的百分比
  • 单位坐标空间中的每个坐标的范围为0.0到1.0。例如:
    • 沿 x 轴,左边缘位于坐标 处0.0,右边缘位于坐标 处1.0。
    • 沿y轴,单位坐标值的方向根据平台的不同而变化

1.2.2 锚点影响几何操作

  • 使用图层的anchorPoint属性来访问该锚点
  • 图层的几何相关操作是相对于该图层的锚点进行
  • transform位置属性始终是相对于图层的锚点指定的,并且应用到图层的任何变换也相对于锚点发生

修改锚点值,影响旋转操作示例:

  • anchorPoint 从(0.5,0.5)改成(0.0,0.0)
  • 旋转效果变化:

1.3 三组layer树

使用 Core Animation 的应用程序具有三组图层对象。每组图层对象在使应用程序的内容显示在屏幕上方面都有不同的作用:

  • model layer 树 ("layer 树)
    • 应用程序交互最多的对象。
    • 此树中的对象是存储任何动画的目标值的模型对象。
    • 每当您更改图层的属性时,都会使用这些对象之一。
  • 演示树 presentation tree
    • 对象中包含任何正在运行的动画的运行中值
    • 图层树对象包含动画的目标值,而演示树中的对象反映屏幕上出现的当前值
    • 内部属性可读不可写
    • 可以从这些值开始创建一个新动画
  • 渲染树render tree
    • 对象执行实际的动画,并且是核心动画私有的

1.4 UIView与CAlayer 的关系

UIView 作为最常用的视图控件,和 CALayer 也有着千丝万缕的联系

我们在探索 iOS图层渲染原理,关于这部分,已经做过详尽阐述,在这里我们直接归纳结论:

1.4.1 UIView的职责

  • Drawing and animation:绘制与动画
  • Layout and subview management:布局与子 view 的管理
  • Event handling: 处理交互事件(如点击事件、旋转事件、press事件、加速事件、远程控制等)

1.4.2 CALayer的职责

  • CALayer 是 UIView 的属性之一,负责渲染和动画,提供可视内容的呈现
  • 我们创建一个 UIView 的时候,UIView 会自动创建一个 CALayer,为自身提供存储 bitmap 的地方,并将自身固定设置为 CALayer 的代理

1.4.3 两个核心关系

  • CALayer 是 UIView 的属性之一,负责渲染动画,提供可视内容的呈现。
  • UIView 提供了对 CALayer 部分功能的封装,同时也另外负责了交互事件的处理
    • 为什么 UIKit 中的视图能够呈现可视化内容?就是因为 UIKit 中的每一个 UI 视图控件其实内部都有一个关联的 CALayer,即 backing layer
    • CALayer 事实上是用户所能在屏幕上看见的一切的基础

1.4.4 基于 两个核心关系 的拓展

1.4.4.1 两者的异同

相同点:

  • 相同的层级结构:
    每个 UIView 都一一对应CALayer 负责页面的绘制,所以视图层级拥有 视图树 的树形结构,对应 CALayer 层级也拥有 图层树 的树形结构
    • 其中,View的职责是 创建并管理 图层,以确保当子视图在层级关系中 添加或被移除 时,其关联的图层在图层树中也有相同的操作(即保证视图树和图层树在结构上的一致性)

不同点:

  • 部分效果的设置: 因为 UIView 只对 CALayer 的部分功能进行了封装,而另一部分如圆角阴影边框等特效都需要通过调用 layer 属性来设置。
  • 是否响应点击事件: CALayer 不负责点击事件,所以不响应点击事件,而 UIView 会响应。
  • 不同继承关系:
    • CALayer 继承自 NSObject
    • UIView 由于要负责交互事件,所以继承自 UIResponder。
1.4.4.2 提供两个平行的层级关系的意义

为什么要将 CALayer 独立出来,直接使用 UIView 统一管理不行吗?为什么不用一个统一的对象来处理所有事情呢?

  • 这样设计的主要原因就是为了职责分离拆分功能方便代码的复用;
  • 通过 Core Animation 框架来负责可视内容的呈现,这样在 iOS 和 OS X 上都可以使用 Core Animation 进行渲染;
  • 与此同时,两个系统还可以根据交互规则的不同来进一步封装统一的控件,比如 iOS 有 UIKit 和 UIView,OS X 则是AppKit 和 NSView。
  • 实际上,这里并不是两个层级关系,而是四个。每一个layer都有三层树:layer树呈现树渲染树(除了 视图树图层树 ,还有 呈现树渲染树)

2. 设置layer对象

2.1 启用核心动画支持

  • 链接到 QuartzCore 框架。(iOS 应用程序仅在显式使用 Core Animation 接口时才必须链接到此框架。)
swift 复制代码
import UIKit
import QuartzCore

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // 创建一个视图
        let redView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
        redView.backgroundColor = UIColor.red
        self.view.addSubview(redView)
        
        // 创建基本动画
        let animation = CABasicAnimation(keyPath: "position")
        animation.fromValue = redView.layer.position
        animation.toValue = CGPoint(x: 200, y: 200)
        animation.duration = 1.0
        
        // 添加动画到视图的图层
        redView.layer.add(animation, forKey: "positionAnimation")
    }
}

在这个示例中,我们使用了 CABasicAnimation 类来创建基本动画,而 redView 的图层是通过 redView.layer 属性来访问的,这就涉及到了 QuartzCore 框架。通过 Core Animation 的高级 API,我们可以更加方便地创建和管理动画效果,而不必直接操作底层的 QuartzCore 框架

2.2 更改与View关联的layer对象

  • 图层支持的视图会创建该类的实例CALayer

  • CoreAnimation提供了不同的layer类,每个layer类都提供了专门功能。

  • 选择不同的layer类可能使我们能够以简单的方式提高性能或支持特定类型的内容。

  • 更改 UIView 使用的图层类,通过重写方法:

    objc 复制代码
    + (Class)layerClass {
        return [CAMetalLayer class];
    }
  • 例如:使用 Metal 或 OpenGL ES 绘制View的内容时,使用CAMetalLayer或CAEAGLLayer对象更合适。

  • CALayer子类及其用途介绍

2.3 提供图层的内容|contents属性

2.3.1 设置图层内容

  • 使用 contents 属性设置图层的内容,可以是 CGImageUIImageUIColor 等类型

  • 这个属性的类型被定义为id,意味着它可以是任何类型的对象

    • 在这种情况下,可以给 contents 属性赋任何值,app 仍然能够编译通过
    • 但是,在实践中,如果给 contents 赋的不是CGImage, 那么得到的图层将是空白的
  • 事实上,真正要赋值的类型应该是CGImageRef,它是一个指向CGImage结构的指针。

  • UIImage有一个CGImage属性,它返回一个"CGImageRef",如果想把这个值直接赋值给CALayer 的 contents ,那将会得到一个编译错误。

    • 因为CGImageRef并不是一个真正的 Cocoa对象,而是一个Core Foundation类型。
    • 可以通过__bridge关键字转换。如果要 给图层的寄宿图赋值,你可以按照以下这个方法:
    objc 复制代码
    layer.contents = (__bridge id)image.CGImage; 
  • ...

代码如下:

swift 复制代码
class ViewController: UIViewController {
    lazy var v: UIView = {
        let v = UIView()
        v.backgroundColor = .white
        v.frame = CGRect.init(x: UIScreen.main.bounds.size.width / 2 - 100 , y: UIScreen.main.bounds.size.height / 2 - 100, width: 200, height: 200)
        return v
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(self.v)
        let image = UIImage.init(named: "hello")
        
        self.v.layer.contents = image?.cgImage
        
    }
}

运行结果如下:

2.3.2 contentGravity属性

加载的图片并不 刚好是一个方的,为了适应这个视图,它有一点点被拉伸了。在使用UIImageView 的时候遇到过同样的问题,解决方法就是把 contentMode 属性设置成更合适的 值,像这样:

objc 复制代码
imageView.contentMode = .scaleAspectFill

CALayer与 contentMode 对应的属性叫做 contentsGravity

objc 复制代码
self.v.layer.contentsGravity = .resizeAspect

运行结果如下:

将上面的contentGravity改一下:

objc 复制代码
self.v.layer.contentsGravity = .resizeAspectFill

运行效果如下:

2.4 调整图层的视觉样式和外观

  • backgroundColor:设置图层的背景颜色。

    swift 复制代码
    let layer = CALayer()
    layer.frame = CGRect(x: 100, y: 100, width: 100, height: 100)
    layer.backgroundColor = UIColor.red.cgColor
  • borderColor 和 borderWidth:设置图层的边框颜色和宽度。

    swift 复制代码
    layer.borderColor = UIColor.blue.cgColor
    layer.borderWidth = 2.0
  • cornerRadius:设置图层的圆角半径。

    swift 复制代码
    layer.cornerRadius = 10.0
  • shadowColor、shadowOffset、shadowOpacity 和 shadowRadius:设置图层的阴影颜色、偏移、不透明度和半径。

    swift 复制代码
    layer.shadowColor = UIColor.gray.cgColor
    layer.shadowOffset = CGSize(width: 0, height: 3)
    layer.shadowOpacity = 0.5
    layer.shadowRadius = 5.0
  • mask:设置图层的蒙版,用于裁剪图层内容。

    swift 复制代码
    let maskLayer = CALayer()
    maskLayer.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
    maskLayer.backgroundColor = UIColor.black.cgColor
    layer.mask = maskLayer
  • ...

3. Layer层级管理

3.1 修改层次结构的方法

与View中管理父子视图的API差不多:

objc 复制代码
...
@property(nullable, readonly) CALayer *superlayer;
- (void)removeFromSuperlayer;
@property(nullable, copy) NSArray<__kindof CALayer *> *sublayers;

- (void)addSublayer:(CALayer *)layer;

- (void)insertSublayer:(CALayer *)layer atIndex:(unsigned)idx;
- (void)insertSublayer:(CALayer *)layer below:(nullable CALayer *)sibling;
- (void)insertSublayer:(CALayer *)layer above:(nullable CALayer *)sibling;

- (void)replaceSublayer:(CALayer *)oldLayer with:(CALayer *)newLayer;

3.2 子层的定位和大小调整

  • 设置子图层的大小用bounds(等同于view中的bounds)
  • 使用该属性设置其在其superlayer中的位置position,(等同于view中的center)
objc 复制代码
myLayer.bounds = CGRectMake(0, 0, 100, 100);
myLayer.position = CGPointMake(200, 200);

3.3 子图层和剪辑

  • 启用剪切 layer.masksToBounds = YES
    • 图层剪切蒙版的形状包括图层的角半径(如果已指定)。图 4-3显示的图层演示了该masksToBounds属性如何影响具有圆角的图层。
      当该属性设置为 时NO,子图层将完整显示,即使它们超出了父图层的边界。更改属性会YES导致其内容被剪裁。

3.4 转换层之间的坐标值

objc 复制代码
- (CGPoint)convertPoint:(CGPoint)p fromLayer:(nullable CALayer *)l;
- (CGPoint)convertPoint:(CGPoint)p toLayer:(nullable CALayer *)l;
- (CGRect)convertRect:(CGRect)r fromLayer:(nullable CALayer *)l;
- (CGRect)convertRect:(CGRect)r toLayer:(nullable CALayer *)l;

4. transform变换

  • 使用CGAffineTransform 可以用来对图层旋转,摆放或者扭曲
  • 使用CATransform3D可以将扁平物体转换成三维空间对象 的

4.1 2D变换|CGAffineTransform

创建一个CGAffineTransform

Core Graphics提供了一系 列函数,对完全没有数学基础的开发者也能够简单地做一些变换。如下几个函数都创建了一个 CGAffineTransform 实例:

objc 复制代码
// 1. 旋转变换
CGAffineTransformMakeRotation(CGFloat angle) 
// 2. 缩放变换
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy) 
// 3. 平移变换
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)
  • 旋转缩放变换都可以很好解释
    分别旋转或者缩放一个向量的值。
  • 平移变换是指每个点都移动了向量指定的x或者y值--所以如果向量代表了一个点,那它就平移了这个点的距离。

需求:将原始视图旋转45角度

  • UIView 可以通过设置 transform 属性做变换,但实际上它只是封装了内部图层 的变换。
  • CALayer 同样也有一个 transform 属性,但它的类型是 CATransform3D ,而不是 CGAffineTransform
  • CALayer 对应 于 UIView 的 transform 属性叫做 affineTransform
objc 复制代码
CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4); 
self.layerView.layer.affineTransform = transform;


注意我们使用的旋转常量是 M_PI_4 ,而不是你想象的45,因为iOS的变换函数使 用弧度而不是角度作为单位。弧度用数学常量pi的倍数表示,一个pi代表180度,所 以四分之一的pi就是45度。
C的数学函数库(iOS会自动引入)提供了pi的一些简便的换算, M_PI_4 于是就 是pi的四分之一,如果对换算不太清楚的话,可以用如下的宏做换算:
#define RADIANS_TO_DEGREES(x) ((x)/M_PI*180.0)

4.2 混合变换

在一个变换的基础上做更深层次的变换

  • Core Graphics提供了一系列的函数可以在一个变换的基础上做更深层次的变换
  • 如果做一个既要缩放又要旋转的变换,这就会非常有用了。

下面函数:

objc 复制代码
CGAffineTransformRotate(CGAffineTransform t, CGFloat angle) 
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy) 
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)

当操纵一个变换的时候,初始生成一个什么都不做的变换很重要--也就是创建一 个 CGAffineTransform 类型的空值,矩阵论中称作单位矩阵,Core Graphics同样也提供了一个方便的常量:CGAffineTransformIdentity

最后,如果需要混合两个已经存在的变换矩阵,就可以使用如下方法,在两个变换的基础上创建一个新的变换:

objc 复制代码
CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);

用例1:使用若干方法创建一个复合变换

代码下:

objc 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIView *v = [[UIView alloc]init];
    [self.view addSubview: v];
    v.backgroundColor = UIColor.redColor;
    v.frame = CGRectMake(150, 150, 100, 100);
    
    CGAffineTransform transform = CGAffineTransformIdentity;
    //scale by 50%
    transform = CGAffineTransformScale(transform, 0.5, 0.5);
    //rotate by 30 degrees
    transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 30.0);
    //translate by 200 points
    transform = CGAffineTransformTranslate(transform, 200, 0);
    //apply transform to layer
    v.layer.affineTransform = transform;    
}

4.3 3D变换|CATransform3D

  • CGAffineTransform 类型属于Core Graphics框架

    • Core Graphics实际上是一个严格意义上的2D绘图API
    • 并且 CGAffineTransform 仅仅对2D变换有效
  • CGAffineTransformCATransform3D 的异同

    • CGAffineTransform 类似, CATransform3D 也是一个矩阵,但是和2x3的矩阵不同, CATransform3D 是一个可以在3维空间内做变换的4x4的矩阵。
    • CGAffineTransform 矩阵类似, Core Animation提供了一系列的方法用来创建和组合 CATransform3D 类型的矩阵,和Core Graphics的函数类似
    • 但是3D的平移和旋转多出了一个 z 参数,并且旋转函数除了 angle 之外多出 了 x , y , z 三个参数,分别决定了每个坐标轴方向上的旋转:
    objc 复制代码
    CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z) 
    CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz) 
    CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)

Z轴和这两个轴分别垂直,指向视角外为正方向。

用例1:对视图内的图层绕Y轴做45度角的旋转

objc 复制代码
CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
v.layer.transform = transform;

4.3.1 透视投影

  • CATransform3D 的透视效果通过一个矩阵中一个很简单的元素来控制:m34
  • m34 用于按比例缩放XY的值来计算到底要离视角多远。
  • m34 的默认值是0,可以通过设置 m34 为-1.0 / d 来应用透视效果
  • d 代表了想象中视角相机和屏幕之间的距离,以像素为单位,
    • 那应该如何计算这个距离呢?
    • 实际上并不需要,大概估算一个就好了 【通常500-1000就已经很好了】

用例1:对图片做透视效果

swift 复制代码
override func viewDidLoad() {
    super.viewDidLoad()
    self.view.addSubview(imgView)

    //create a new transform
    var transform: CATransform3D = CATransform3DIdentity

    // 透视效果
    transform.m34 = -1.0 / 500

    //rotate by 45 degrees along the Y axis
    transform = CATransform3DRotate(transform, .pi / 4, 0, 1, 0)

    //apply to layer
    self.imgView.layer.transform = transform

}

5. CAlayer的常用属性

objc 复制代码
//宽度和高度
@property CGRect bounds;

//位置(默认指中点,具体由anchorPoint决定)
@property CGPoint position;

//锚点(x,y的范围都是0-1),决定了position的含义
@property CGPoint anchorPoint;

//背景颜色(CGColorRef类型)
@propertyCGColorRefbackgroundColor;

//形变属性
@property CATransform3D transform;

//边框颜色(CGColorRef类型)
@property  CGColorRef  borderColor;

//边框宽度
@property CGFloat borderWidth;

//圆角半径
@property CGFloat cornerRadius;

//内容(比如设置为图片CGImageRef)
@property(retain) id contents;

6. Hit Testing

CALayer 并不关心任何响应链事件,所以不能直接处理触摸事件或者手势。但是它有两个API处理事件: -containsPoint: 和 -hitTest:

  • -containsPoint: 接受一个在本图层坐标系下的 CGPoint
    • 如果这个点在图层 frame 范围内就返回 YES 。
    • 也就是使用-containsPoint: 方法来判断到底是红色还是蓝色的图层被触摸了
  • -hitTest:方法同样接受一个 CGPoint 类型参数,
    • 它返回图层本身,或者包含这个坐标点的sublayer。
    • 这意味着不再需要像使用 - containsPoint:
    • 那样,人工地在每个子图层变换或者测试点击的坐标。如果这个点在最外面图层的范围之外,则返回nil。

案例:

swift 复制代码
class ViewController: UIViewController {
    
    lazy var blueLayer = CALayer()
    lazy var v: UIView = {
        let v = UIView()
        v.backgroundColor = .red
        v.frame = CGRect.init(x: UIScreen.main.bounds.size.width / 2 - 100 , y: UIScreen.main.bounds.size.height / 2 - 100, width: 200, height: 200)
        return v
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(self.v)
        
        let blueLayer = CALayer()
        blueLayer.frame = CGRect.init(x: 50, y: 50, width: 100, height: 100)
        blueLayer.backgroundColor = UIColor.blue.cgColor
        blueLayer.delegate = self
        self.blueLayer = blueLayer
        self.v.layer.addSublayer(blueLayer)

    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        //得到在主view中的position
        guard var point = touches.first?.location(in: self.view) else { return }
        //转换到v.layer的位置
        point = self.v.layer.convert(point, from: self.view.layer)
        if self.v.layer.contains(point) {
            point = self.blueLayer.convert(point, from: self.v.layer)
            if self.blueLayer.contains(point) {
                print("点击了蓝色")
            } else {
                print("点击了红色")
            }
        }
    }
}

案例2:

7. 其它layer对象介绍

Class Usage
CAEmitterLayer 用于实现基于 Core Animation 的粒子发射器系统。发射器层对象控制粒子的生成及其来源。
CAGradientLayer 用于绘制填充图层形状的颜色渐变(在任何圆角的范围内)。
CAMetalLayer 用于设置和 vend 可绘制纹理,以使用 Metal 渲染图层内容。
CAEAGLLayer/CAOpenGLLayer 用于设置备份存储和上下文,以使用 OpenGL ES(iOS)或 OpenGL(OS X)渲染图层内容。
CAReplicatorLayer 当你要自动制作一个或多个子层的副本时使用。复制器为你制作副本,并使用你指定的属性来更改副本的 appearance 或 attributes。
CAScrollLayer 用于管理由多个子层组成的较大的可滚动区域。
CAShapeLayer 用于绘制三次贝塞尔曲线样条曲线。Shape 图层对于绘制基于路径的形状非常有利,因为它们始终会产生清晰的路径,而与你绘制到图层的备份存储中的路径相反,后者在缩放时看起来并不好。但是,清晰的结果确实涉及在主线程上渲染 Shape 并缓存结果。
CATextLayer 用于呈现纯文本字符串或属性字符串。
CATiledLayer 用于管理可分为较小图块的大图像,并支持放大和缩小内容,分别渲染。
CATransformLayer 用于渲染真实的 3D 图层层次结构,而不是由其他图层类实现的平坦的图层层次结构。
QCCompositionLayer 用于渲染 Quartz Composer 合成。(仅支持 OS X)

7.1 CAShapeLayer|CALayer

  • CAShapeLayer 是一个通过矢量图形而不是bitmap来绘制的图层子类。
  • 指定诸如颜色colorlinewidth线宽等属性,用 CGPath 来定义想要绘制的图形,最后CAShapeLayer 就自动渲染出来了
    • CGPath 能表示的形状,CAShapeLayer 都可以绘制出来。
    • 换句话说CGPath可以限制CAShapeLayer的形状。
    • CAShapeLayer有一个属性Path,将路径赋值给这个属性即可。
  • 也可以用Core Graphics直接向原始的 CALyer 的内容中绘制一个路径,相比之下,使用 CAShapeLayer 有以下一些优点:
    • 渲染快速:
      • CAShapeLayer 使用了硬件加速,绘制同一图形会比用Core Graphics快很多
    • 高效使用内存:
      • 一个 CAShapeLayer 不需要像普通 CALayer 一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存
    • 不会被图层边界剪裁掉:
      • 一个 CAShapeLayer 可以在边界之外绘制。
      • 你的图层路径不会像在使用Core Graphics的普通 CALayer 一样被剪裁掉。
    • 不会出现像素化:
      • 当你给 CAShapeLayer 做3D变换时,它不像一个有寄宿图 的普通图层一样变得像素化。

一些应用场景:

    1. 作为遮罩:
    • CAShapeLayer可以作为其他图层的遮罩使用,用于限制其他图层的形状

    • 通过图层的mask属性赋值。

      objc 复制代码
      /*
      下面是绘制一个圆形的图片
      通常我们通过设置imageView.layer的圆角半径来让imageView变成圆形
      现在可以直接使用CAShapeLayer生成一个圆形遮罩覆盖在imageView上
      */
      //创建imageView
      UIImageView *imagev = [[UIImageView alloc] init];
      imagev.frame = CGRectMake(100, 100, 100, 100);   //边长100的正方形
      [self.view addSubview:imagev];
      
      CAShapeLayer *shaplayer = [CAShapeLayer layer];
      UIBezierPath *path = [UIBezierPath bezierPath];     //创建路径
      [path addArcWithCenter:CGPointMake(50, 50) radius:50 startAngle:0 endAngle:M_PI*2 clockwise:YES];       //圆形路径 注意这里的center是以imageView为坐标系的
      shaplayer.path = path.CGPath;    //要转成CGPath
      imagev.layer.mask = shaplayer;   //限制imageView的外形
    • 注意:

      • 作为遮罩时不用设置颜色属性,只需设置path属性。
      • 作为遮罩时才会限制父layer的形状
      • 作为子layer时不会限制父layer的形状
    1. 动画效果:
    • 通过不断的改变CAShapeLayer的path从而达到动画的效果
    • 可以做出核心动画难以实现的效果,比如
      • 粘性动画单边的弹性下拉效果qq的粘性按钮效果正弦波浪线等等,相当丰富,我这里提供几个链接
        粘性动画
    1. 两个属性strokeStart和strokeEnd:
    • 这两个属性用于对绘制的Path进行区域限制,值为0-1之间,并且这两个属性可做动画,例子如下。
    objc 复制代码
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        CAShapeLayer *layer = [CAShapeLayer layer];
        layer.strokeColor = kRedColor.CGColor;
        layer.fillColor = kClearColor.CGColor;
        layer.lineWidth = 2;   //通过调整线宽可以做成饼状图
        UIBezierPath *path = [UIBezierPath bezierPath];
        //值得一提的是这里圆的起点是-π
        [path addArcWithCenter:CGPointMake(150, 150) radius:100 startAngle:-M_PI endAngle:M_PI clockwise:YES];
        layer.path = path.CGPath;
        layer.strokeEnd = 0.0;
        self.layer = layer;
        [self.view.layer addSublayer:layer];
    }
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        CABasicAnimation *anim = [CABasicAnimation animation];
        anim.keyPath = @"strokeEnd";//KVC
        anim.fromValue = @0.0;
        anim.toValue = @1.0;
        anim.duration = 2;
        anim.repeatCount = 100;
        [self.layer addAnimation:anim forKey:nil];   
    }
    1. 虚线效果:
    • 虚线效果只需设置lineDashPattern 属性:
      • self.shapLayer.lineDashPattern = @[@(4),@(4)];
    • 数组中第一个4表示先画4个点的实线,第二4表示接着间隔4个点不画线\
    1. 二维码扫描框: 二维码的扫描框通常是中间矩形为透明,其余边框为带透明度的黑色
    objc 复制代码
    //包裹self.view的路径
    UIBezierPath *overlayPath = [UIBezierPath bezierPathWithRect:self.view.bounds];
    //中间透明边框的路径
    UIBezierPath *transparentPath = [UIBezierPath bezierPathWithRect:CGRectMake(100, 150, 200, 200)];
    //合并为一个路径
    [overlayPath appendPath:transparentPath];
    
    CAShapeLayer *layer = [CAShapeLayer layer];
    layer.path = overlayPath.CGPath;
    layer.fillRule = kCAFillRuleEvenOdd;  //奇偶填充规则
    layer.fillColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.3].CGColor;
    
    [self.view.layer addSublayer:layer];

7.2 CATextLayer|CALayer

  • CALayer 的子类 CATextLayer以图层的形式包含了 UILabel 几乎所有的绘制特性,并且额外提供了一些新的特性。

  • CATextLayer也要比 UILabel 渲染得快得多。

    • 很少有人知道在iOS 6及之前的版本, UILabel 其实是通过WebKit来实现绘制的,这样就造成了当有很多文字的时候就会有极大的性能压力。
    • 而 CATextLayer 使用了Core text,并且渲染得 非常快。
  • CATextLayer 显示文字示例:

    objc 复制代码
    CATextLayer *textLayer = [CATextLayer layer];
    textLayer.string = @"123abcABC123abcABC123abcABC123abcABC123abcABC123abcABC123a3abcABC123abcABC123abcABC123a3abcABC123abcABC123abcABC123abcABC123abcABC呵呵呵";
    textLayer.font = CGFontCreateWithFontName((__bridge CFStringRef)(@"Georgia"));
    textLayer.fontSize = 12;
    textLayer.backgroundColor = kYellowColor.CGColor;
    textLayer.foregroundColor = kRedColor.CGColor;         //文字颜色,普通字符串时可以使用该属性
    textLayer.wrapped = YES;                               //为yes时自动换行
    textLayer.truncationMode = @"start";                   //字符串过长时的省略位置,注意是最后一行的哪个位置
    //    textLayer.alignmentMode = kCAAlignmentCenter;        //对齐方式
    //    textLayer.allowsFontSubpixelQuantization = NO;
    textLayer.frame = CGRectMake(0, 0, 100, 100);
    //    textLayer.position = self.view.center;                //图层的中心点位于父层的位置
    textLayer.contentsScale = [UIScreen mainScreen].scale;  //按当前的屏幕分辨率显示   否则字体会模糊
    [self.view.layer addSublayer:textLayer];

7.3 CATransformLayer|CALayer

  • CATransformLayer是一个容器layer

    • backgroundColor等外观显示属性对他是无效的,
  • CATransformLayer 只是一个容器,

    • 只负责容纳其他layer并显示其他layer
    • CATransformLayer通常用于构造复杂的3D事物,他不是一个平面化的图层,能够构造多层次的3D结构。
    objc 复制代码
    //这里就创建一个简单的立方体,代码如下
    @interface ViewController ()
    @property (nonatomic,strong) NSMutableArray<CALayer *> *layerArray;
    @property (nonatomic,strong) NSMutableArray *transArray;
    @end
    
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        //创建图层
        CATransformLayer *cublayer = [CATransformLayer layer];
    //    layer.borderColor = kBlackColor.CGColor;      //这些属性设置都是无效的
    //    layer.borderWidth = 1;
    //    layer. backgroundColor = [UIColor redColor].CGColor;
        cublayer.bounds = CGRectMake(0, 0, 200, 200);
        cublayer.position = self.view.center;
        [self.view.layer addSublayer:cublayer];
    
        //对容器图层做动画
        CATransform3D transA = CATransform3DMakeRotation(M_PI, 1, 1, 0);
        CATransform3D transB = CATransform3DMakeRotation(M_PI*2, 1, 1, 0);
        CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform"];
        animation.duration  = 4;
        animation.autoreverses     = YES;
        animation.repeatCount     = 100;
        animation.fromValue     = [NSValue valueWithCATransform3D:transA];
        animation.toValue      = [NSValue valueWithCATransform3D:transB];
        [cublayer addAnimation:animation forKey:nil];
    
        //创建立方体的6个面
        self.layerArray = [NSMutableArray array];
        for (NSInteger i = 0; i<6; i++) {
            CALayer *sublayer = [CALayer layer];
            sublayer.bounds = CGRectMake(0, 0, 100, 100);
            sublayer.position = CGPointMake(100, 100);
            sublayer.backgroundColor = kRandomColorAndAlpha(0.3).CGColor;
            sublayer.speed = 0.1;
            [self.layerArray addObject:sublayer];
            [cublayer addSublayer:sublayer];
        }
    
        //为六个面的图层创建3D变换,使之组成立方体
        CATransform3D ct1 = CATransform3DMakeTranslation(0, 0, 50);
        CATransform3D ct2 = CATransform3DMakeTranslation(0, 0, -50);
        CATransform3D ct3 = CATransform3DMakeTranslation(-50, 0, 0);
        ct3 = CATransform3DRotate(ct3, M_PI_2, 0, 1, 0);
        CATransform3D ct4 = CATransform3DMakeTranslation(50, 0, 0);
        ct4 = CATransform3DRotate(ct4, M_PI_2, 0, 1, 0);
        CATransform3D ct5 = CATransform3DMakeTranslation(0, -50, 0);
        ct5 = CATransform3DRotate(ct5, M_PI_2, 1, 0, 0);
        CATransform3D ct6 = CATransform3DMakeTranslation(0, 50, 0);
        ct6 = CATransform3DRotate(ct6, M_PI_2, 1, 0, 0);
        //存入数组待用
        self.transArray = [NSMutableArray arrayWithArray:@[[NSValue valueWithCATransform3D:ct1],
                                                        [NSValue valueWithCATransform3D:ct2],
                                                        [NSValue valueWithCATransform3D:ct3],
                                                        [NSValue valueWithCATransform3D:ct4],
                                                        [NSValue valueWithCATransform3D:ct5],
                                                        [NSValue valueWithCATransform3D:ct6]]];
    }
    
    //一开始六个面叠在一起,点击屏幕后,立方体的六个面慢慢归位
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        for (NSInteger i = 0; i<6; i++) {
            NSValue *value = self.transArray[I];
            CATransform3D ct = [value CATransform3DValue];
            CALayer *layer = self.layerArray[I];
            [UIView animateWithDuration:1.0 animations:^{
                layer.transform = ct;
            }];
        } 
    }

7.4 CAGradientLayer|CALayer

CAGradientLayer是用来生成渐变图层。两种更多颜色平滑渐变的图层

  • 属性locations
    • locations 表示的是渐变区间,数组中的数字必须是递增的。比如
      • 下面这个例子layer.locations = @[@0.5,@0.8];
      • 渐变区间是0.5-0.8,也就是说0.0-0.5是纯红色,0.5-0.8是红色渐变到绿色,0.8-1.0是纯绿色。
      • 不设置这个属性就是整个区间0.0-1.0均匀渐变。
    • locations 数组并不是强制要求的,但是如果你给它赋值了就一定要
      • 确保 locations 的数组大小和 colors 数组大小一定要相同
      • 否则你将会得到一个空白的渐变。
    • locations 属性是一个浮点数值的数组 (以 NSNumber 包装), 0.0代表着渐变的开始,1.0代表着结束
  • startPointendPoint 属性
    • 他们决定了渐变的方向。
    • 这两个参数是以单位坐标系进行的定义,所以左上角坐标是{0, 0},右下角坐标 是{1, 1}

案例1:红黄绿色彩渐变:

  • 从红到黄最后到绿色的渐变。

  • locations 数组指定了0.0,0.25和0.5三个数值,这样这三个渐变就有点像挤在了左上角

    swift 复制代码
    class ViewController: UIViewController {
        lazy var containV: UIView = {
            let v = UIView()
            v.frame = CGRect(x: 80, y: 150, width: 200, height: 200)
            return v
        }()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            self.view.addSubview(containV)
    
            let gradientLayer: CAGradientLayer = CAGradientLayer()
            gradientLayer.frame = self.self.containV.bounds
            self.containV.layer.addSublayer(gradientLayer)
    
            let startColor = UIColor.red.cgColor
            let minddleColor = UIColor.yellow.cgColor
            let endColor = UIColor.green.cgColor
    
            gradientLayer.colors = [startColor, minddleColor, endColor]
    
            gradientLayer.locations = [0.0, 0.25, 0.5]
    
            gradientLayer.startPoint = CGPoint(x: 0, y: 0)
            gradientLayer.endPoint = CGPoint(x: 1, y: 1)
    
        }
    
    }

案例2:两种颜色的对角线渐变:

swift 复制代码
class ViewController: UIViewController {
    lazy var containV: UIView = {
        let v = UIView()
        v.frame = CGRect(x: 80, y: 150, width: 200, height: 200)
        return v
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(containV)
        
        let gradientLayer: CAGradientLayer = CAGradientLayer()
        gradientLayer.frame = self.self.containV.bounds
        self.containV.layer.addSublayer(gradientLayer)
        
        let startColor = UIColor.red.cgColor
        let endColor = UIColor.blue.cgColor
        gradientLayer.colors = [startColor,endColor]
        
        gradientLayer.startPoint = CGPoint(x: 0, y: 0)
        gradientLayer.endPoint = CGPoint(x: 1, y: 1)
        
    }
}

使用Core Graphics相关方法实现渐变

  • iOS Core Graphics中有两个方法用于绘制渐变颜色:
    • CGContextDrawLinearGradient 可以用于生成线性渐变
    • CGContextDrawRadialGradient 用于生成圆半径方向颜色渐变
  • 函数可以自定义path,无论是什么形状都可以,原理都是用来做Clip,所以需要在CGContextClip函数前调用CGContextAddPath函数把CGPathRef加入到Context中。
  • 另外一个需要注意的地方是渐变的方向,方向是由两个点控制的,点的单位就是坐标。
    • 因此需要正确从CGPathRef中找到正确的点,方法当然有很多种看具体实现,本例中,我就是简单得通过调用CGPathGetBoundingBox函数,返回CGPathRef的矩形区域,然后根据这个矩形取两个点
objc 复制代码
// 线性渐变
- (void)drawLinearGradient:(CGContextRef)context
                      path:(CGPathRef)path
                startColor:(CGColorRef)startColor
                  endColor:(CGColorRef)endColor
{
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGFloat locations[] = { 0.0, 1.0 };
    NSArray *colors = @[(__bridge id) startColor, (__bridge id) endColor];
    CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef) colors, locations);
    CGRect pathRect = CGPathGetBoundingBox(path);
    //具体方向可根据需求修改
    CGPoint startPoint = CGPointMake(CGRectGetMinX(pathRect), CGRectGetMidY(pathRect));
    CGPoint endPoint = CGPointMake(CGRectGetMaxX(pathRect), CGRectGetMidY(pathRect));
    CGContextSaveGState(context);
    CGContextAddPath(context, path);
    CGContextClip(context);
    CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0);
    CGContextRestoreGState(context);
    CGGradientRelease(gradient);
    CGColorSpaceRelease(colorSpace);
}
- (void)viewDidLoad 
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //创建CGContextRef
    UIGraphicsBeginImageContext(self.view.bounds.size);
    CGContextRef gc = UIGraphicsGetCurrentContext();
    //创建CGMutablePathRef
    CGMutablePathRef path = CGPathCreateMutable();
    //绘制Path
    CGRect rect = CGRectMake(0, 100, 300, 200);
    CGPathMoveToPoint(path, NULL, CGRectGetMinX(rect), CGRectGetMinY(rect));
    CGPathAddLineToPoint(path, NULL, CGRectGetMidX(rect), CGRectGetMaxY(rect));
    CGPathAddLineToPoint(path, NULL, CGRectGetWidth(rect), CGRectGetMaxY(rect));
    CGPathCloseSubpath(path);
    //绘制渐变
    [self drawLinearGradient:gc path:path startColor:[UIColor greenColor].CGColor endColor:[UIColor redColor].CGColor];
    //注意释放CGMutablePathRef
    CGPathRelease(path);
    //从Context中获取图像,并显示在界面上
    UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    UIImageView *imgView = [[UIImageView alloc] initWithImage:img];
    [self.view addSubview:imgView];
}
objc 复制代码
圆半径方向渐变
- (void)drawRadialGradient:(CGContextRef)context
                      path:(CGPathRef)path
                startColor:(CGColorRef)startColor
                  endColor:(CGColorRef)endColor
{
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGFloat locations[] = { 0.0, 1.0 };
    NSArray *colors = @[(__bridge id) startColor, (__bridge id) endColor];
    CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef) colors, locations);
    CGRect pathRect = CGPathGetBoundingBox(path);
    CGPoint center = CGPointMake(CGRectGetMidX(pathRect), CGRectGetMidY(pathRect));
    CGFloat radius = MAX(pathRect.size.width / 2.0, pathRect.size.height / 2.0) * sqrt(2);
    CGContextSaveGState(context);
    CGContextAddPath(context, path);
    CGContextEOClip(context);
    CGContextDrawRadialGradient(context, gradient, center, 0, center, radius, 0);
    CGContextRestoreGState(context);
    CGGradientRelease(gradient);
    CGColorSpaceRelease(colorSpace);
}
- (void)viewDidLoad 
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //创建CGContextRef
    UIGraphicsBeginImageContext(self.view.bounds.size);
    CGContextRef gc = UIGraphicsGetCurrentContext();
    //创建CGMutablePathRef
    CGMutablePathRef path = CGPathCreateMutable();
    //绘制Path
    CGRect rect = CGRectMake(0, 100, 300, 200);
    CGPathMoveToPoint(path, NULL, CGRectGetMinX(rect), CGRectGetMinY(rect));
    CGPathAddLineToPoint(path, NULL, CGRectGetMidX(rect), CGRectGetMaxY(rect));
    CGPathAddLineToPoint(path, NULL, CGRectGetWidth(rect), CGRectGetMaxY(rect));
    CGPathAddLineToPoint(path, NULL, CGRectGetWidth(rect), CGRectGetMinY(rect));
    CGPathCloseSubpath(path);
    //绘制渐变
    [self drawRadialGradient:gc path:path startColor:[UIColor greenColor].CGColor endColor:[UIColor redColor].CGColor];
    //注意释放CGMutablePathRef
    CGPathRelease(path);
    //从Context中获取图像,并显示在界面上
    UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    UIImageView *imgView = [[UIImageView alloc] initWithImage:img];
    [self.view addSubview:imgView];
}

7.5 CAReplicatorLayer|CALayer

CAReplicatorLayer又是一个容器图层(复制图层),他可以将他的子图层复制指定的次数,复制出来的这些图层都拥有相同的图层属性和动画属性,通过下面的例子来介绍一些重要的属性

1. 辐射动画:

  • 属性instanceCount表示拷贝图层的次数,默认为1 。 举个例子instanceCount = 6 表示总共有6个子图层,其中5个是拷贝出来的。

  • 属性 instanceDelay 表示拷贝延时,拷贝一个图层后延时多少秒拷贝下一个图层 这里为了使动画连续,我让动画的duration = 0.6 * 6 = 3.6

    objc 复制代码
    //创建复制图层容器
    CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
    replicator.frame = self.view.bounds;
    [self.view.layer addSublayer:replicator];
    replicator.instanceCount = 6;
    replicator.instanceDelay = 0.6;
    //放大动画
    CABasicAnimation *anim = [CABasicAnimation animation];
    anim.keyPath = @"transform.scale";
    anim.fromValue = @1;
    anim.toValue = @20;
    anim.duration = 3.6;
    //透明度动画
    CABasicAnimation *anim2 = [CABasicAnimation animation];
    anim2.keyPath = @"opacity";
    anim2.toValue = @0.0;
    anim2.fromValue = @1.0;
    anim2.duration = 3.6;
    
    CAAnimationGroup *group = [CAAnimationGroup animation];
    group.animations = @[anim,anim2];
    group.duration = 3.6;
    group.repeatCount = 100;
    //创建子图层
    CALayer *layer = [CALayer layer];
    [layer addAnimation:group forKey:nil];
    layer.bounds = CGRectMake(0, 0, 10, 10);
    layer.position = self.view.center;
    layer.cornerRadius = 5;
    layer.backgroundColor = kRedColor.CGColor;
    [replicator addSublayer:layer];
    • 更改一下属性replicator.instanceCount = 3; 动画就不连续了

2. 加载动画:

  • 属性instanceTransform 表示复制图层在被创建时产生的和上一个复制图层的位移

    objc 复制代码
    CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
    replicator.frame = self.view.bounds;
    [self.view.layer addSublayer:replicator];
    replicator.instanceCount = 6;
    replicator.instanceDelay = 0.2;
    //位移属性
    CATransform3D trans = CATransform3DMakeTranslation(25, 0, 0); //圆点依次向右移动25
    replicator.instanceTransform = trans;
    //透明度动画
    CAKeyframeAnimation *anim = [CAKeyframeAnimation animation];
    anim.keyPath = @"opacity";
    anim.values = @[@1.0,@0.0,@1.0];
    anim.duration = 1.2;
    anim.repeatCount = 100;
    
    CALayer *layer = [CALayer layer];
    [layer addAnimation:anim forKey:nil];
    layer.bounds = CGRectMake(0, 0, 20, 20);
    layer.position = self.view.center;
    layer.cornerRadius = 10;
    layer.backgroundColor = kRedColor.CGColor;
    [replicator addSublayer:layer];

3. 其他的一些属性:

  • instanceColor : 设置多个复制图层的颜色,默认位白色
  • //RGB偏移量
    • instanceRedOffset: 设置每个复制图层相对上一个复制图层的红色偏移量

    • instanceGreenOffset: 设置每个复制图层相对上一个复制图层的绿色偏移量

    • instanceBlueOffset: 设置每个复制图层相对上一个复制图层的蓝色偏移量

    • instanceAlphaOffset: 设置每个复制图层相对上一个复制图层的透明度偏移量 以下就是设置instanceAlphaOffset = -0.1的效果,其他几个属性用法类似

      objc 复制代码
      CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
          replicator.frame = self.view.bounds;
          [self.view.layer addSublayer:replicator];
          replicator.instanceCount = 6;
          replicator.instanceAlphaOffset = -0.1; // 透明度递减,每个图层都比上一个复制图层的透明度小0.1
      
          CATransform3D trans = CATransform3DMakeTranslation(25, 0, 0);
          replicator.instanceTransform = trans;
      
          CALayer *layer = [CALayer layer];
          layer.bounds = CGRectMake(0, 0, 20, 20);
          layer.position = self.view.center;
          layer.cornerRadius = 10;
          layer.backgroundColor = kRedColor.CGColor;
          [replicator addSublayer:layer];

4. 着重介绍一下instanceTransform属性:

swift 复制代码
CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
replicator.frame = self.view.bounds;
[self.view.layer addSublayer:replicator];
replicator.instanceCount = 2;
CATransform3D trans = CATransform3DMakeTranslation(0, -50, 0);  //y的负方向平移50  也就是方块的上方
trans = CATransform3DRotate(trans, M_PI_4, 0, 0, 1);    //然后旋转45度
replicator.instanceTransform = trans;

CALayer *layer = [CALayer layer];
layer.bounds = CGRectMake(0, 0, 30, 30);
layer.position = self.view.center;
layer.backgroundColor = kRedColor.CGColor;
[replicator addSublayer:layer];

我们来看看instanceTransform是怎么运作的:

  • 先设置 replicator.instanceCount = 2;
  • 效果如下,很明显上面那个小方块向上平移了50个点然后旋转了45度。
  • 设置 replicator.instanceCount = 3;
    • 效果如下,由于方块2旋转了45度,所以方块2的上方(黑色边表示上方)也是旋转之后的上方,方块3就是沿着方块2的上方平移50点然后再旋转45度。
  • 设置replicator.instanceCount = 4; 经过上面的推断,下面的效果应该能自己想出来了。
  • 这里要特别注意CATransform3DRotate旋转变换,该方块旋转之后自己的坐标系也发生了同样角度的旋转(感觉是每个方块都有自己的坐标系),在旋转之后再要进行平移操作,那也是按照旋转之后的坐标系进行平移。 (上面的推断纯属个人判断,如有错误还望指正!)

    objc 复制代码
    CATransform3D trans = CATransform3DMakeTranslation(0, -50, 0);
    trans = CATransform3DRotate(trans, M\_PI\_4, 0, 0, 1);
    trans = CATransform3DTranslate(trans, 21, 0, 0);  //按照上面旋转之后的坐标系进行平移(当前坐标系的右方向平移21)

7.6 CAScrollLayer|CALayer

objc 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];

    CALayer *layer = [CALayer layer];
    layer.contents = (id)kImage(@"111").CGImage;
    layer.frame = CGRectMake(0, 0, 375, 667);
    
    self.scrollLayer = [CAScrollLayer layer];
    self.scrollLayer.frame = CGRectMake(60, 60, 200, 200);
    [self.scrollLayer addSublayer:layer];
    self.scrollLayer.scrollMode = kCAScrollBoth;
    [self.view.layer addSublayer:self.scrollLayer];
    
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(gestureChanged:)];
    [self.view addGestureRecognizer:pan];
}

-(void)gestureChanged:(UIPanGestureRecognizer *)gesture
{
    CGPoint translation = [gesture translationInView:self.view];
    CGPoint origin = self.scrollLayer.bounds.origin;
    origin = CGPointMake(origin.x-translation.x, origin.y-translation.y);
    [self.scrollLayer scrollToPoint:origin];
    [gesture setTranslation:CGPointZero inView:self.view];
}

7.7 CAEmitterLayer|CALayer

  • CAEmitterLayer 是一个高性能的粒子引擎,被用来创建 实时粒子动画如: 烟雾火花等等这些效果
  • CAEmitterLayer 常与 CAEmitterCell 结合使用
    • 你将会为不同的例子效果定义一个或多个CAEmitterCell作为模版,
    • 同时CAEmitterLayer负责基于这些模版实例化一个粒子流。
    • 一个CAEmitterCell类似于一个CALayer:
      它有一个contents属性可以定义为一个CGImage,另外还有一些可设置属性控制着表现和行为。

7.7.1 CAEmitterLayer

  • renderMode:渲染模式,控制着在视觉上粒子图片是如何混合的。

    objc 复制代码
    NSString * const kCAEmitterLayerUnordered;
    NSString * const kCAEmitterLayerOldestFirst;
    NSString * const kCAEmitterLayerOldestLast;
    NSString * const kCAEmitterLayerBackToFront;
    NSString * const kCAEmitterLayerAdditive;
  • emitterMode: 发射模式,这个字段规定了在特定形状上发射的具体形式是什么

    objc 复制代码
    kCAEmitterLayerPoints: 点模式,发射器是以点的形势发射粒子。
    kCAEmitterLayerOutline:这个模式下整个边框都是发射点,即边框进行发射
    kCAEmitterLayerSurface:这个模式下是我们边框包含下的区域进行抛洒
    kCAEmitterLayerVolume: 同上
  • emitterShape:规定了发射源的形状。

    objc 复制代码
    kCAEmitterLayerPoint:点形状,发射源的形状就是一个点,位置在上面position设置的位置
    kCAEmitterLayerLine:线形状,发射源的形状是一条线,位置在rect的横向的位于垂直方向中间那条
    kCAEmitterLayerRectangle:矩形状,发射源是一个矩形,就是上面生成的那个矩形rect
    kCAEmitterLayerCuboid:立体矩形形状,发射源是一个立体矩形,这里要生效的话需要设置z方向的数据,如果不设置就同矩形状
    kCAEmitterLayerCircle:圆形形状,发射源是一个圆形,形状为矩形包裹的那个圆,二维的
    kCAEmitterLayerSphere:立体圆形,三维的圆形,同样需要设置z方向数据,不设置则通二维一样
  • emitterSize:发射源的大小,这个emitterSize结合position构建了发射源的位置及大小的矩形区域rect

  • emitterPosition:发射点的位置。

  • lifetime:粒子的生命周期。

  • velocity:粒子速度。

  • scale:粒子缩放比例。

  • spin:自旋转速度。

  • seed:用于初始化产生的随机数产生的种子。

  • emitterCells:CAEmitterCell对象的数组,被用于把粒子投放到layer上

7.7.2. CAEmitterCell

  • 粒子在X.Y.Z三个方向上的加速度。

    objc 复制代码
    @property CGFloat xAcceleration;
    @property CGFloat yAcceleration;
    @property CGFloat zAcceleration;
  • 粒子缩放比例、缩放范围及缩放速度。(0.0`1.0)

    objc 复制代码
    @property CGFloat scale;
    @property CGFloat scaleRange;
    @property CGFloat scaleSpeed;
  • 粒子自旋转速度及范围:

    objc 复制代码
    @property CGFloat spin;
    @property CGFloat spinRange;
  • 粒子RGB及alpha变化范围、速度。

    objc 复制代码
    //范围:
    @property float redRange;
    @property float greenRange;
    @property float blueRange;
    @property float alphaRange;
    //速度:
    @property float redSpeed;
    @property float greenSpeed;
    @property float blueSpeed;
    @property float alphaSpeed;
  • emitterCells:子粒子。

  • color:指定了一个可以混合图片内容颜色的混合色。

  • birthRate:粒子产生系数,默认1.0.

  • contents:是个CGImageRef的对象,即粒子要展现的图片;

  • emissionRange:值是2π(代码写成M_PI * 2.0f),这意味着粒子可以从360度任意位置反射出来。如果指定一个小一些的值,就可以创造出一个圆锥形。

  • 指定值在时间线上的变化,例如:alphaSpeed = 0.4,说明粒子每过一秒减小0.4。

7.7.3 注意

  • CAEmitterLayerCAEmitterCell 中 有相同的属性,他们控制相同的特性

  • 若是相同属性都各自设置了值,粒子发射引擎在工作的时候,会把两个值相乘。作为这个属性的最终值来控制显示效果

  • 相同属性如下:

    objc 复制代码
    @property float birthRate; // 每秒产生的粒子数量
    @property float lifetime; // 粒子的生命周期.单位是秒
    @property CGFloat scale; // 粒子的缩放比例

代码示例:

objc 复制代码
    UIView * containView = [[UIView alloc]initWithFrame:self.view.bounds];
    containView.center = self.view.center;
    containView.backgroundColor = self.view.backgroundColor;
    self.containView = containView;
    [self.view addSubview:self.containView];

    CAEmitterLayer *emitter = [CAEmitterLayer layer];
    emitter.frame = self.containView.bounds;
    [self.containView.layer addSublayer:emitter];
    emitter.renderMode = kCAEmitterLayerAdditive;//这会让重叠的地方变得更亮一些。
    emitter.emitterPosition = CGPointMake(emitter.frame.size.width / 2.0, emitter.frame.size.height / 2.0);

    CAEmitterCell *cell = [[CAEmitterCell alloc] init];
    cell.contents = (__bridge id)[UIImage imageNamed:@"star_yellow"].CGImage;
    cell.birthRate = 150;
    cell.lifetime = 5.0;
    cell.color = [UIColor colorWithRed:1 green:0.5 blue:0.1 alpha:1.0].CGColor;
    cell.alphaSpeed = -0.4;
    cell.velocity = 50;
    cell.velocityRange = 50;
    cell.emissionRange = M_PI * 2.0;

    emitter.emitterCells = @[cell];

案例2:瀑布飘洒效果

objc 复制代码
- (void)setupSubviews {
    self.layer.backgroundColor = [UIColor blackColor].CGColor;
    // 配置emitter
    [self emiterLayer].renderMode = kCAEmitterLayerAdditive; // 粒子如何混合, 这里是直接重叠
    [self emiterLayer].emitterPosition = CGPointMake(self.frame.size.width, 0); // 发射点的位置
    [self emiterLayer].emitterShape = kCAEmitterLayerPoint;
    

    NSMutableArray * mArr = @[].mutableCopy;
    int cellCount = 6;
    for (int i = 0; i<cellCount; i++) {
        CAEmitterCell * cell = [self getEmitterCellAction];
        [mArr addObject:cell];
    }
    [self emiterLayer].emitterCells = mArr; // 将粒子组成的数组赋值给CAEmitterLayer的emitterCells属性即可.
}

- (CAEmitterCell *)getEmitterCellAction {
    CAEmitterCell *cell = [[CAEmitterCell alloc] init];
    //    cell.contents = (__bridge id)[UIImage imageNamed:@"coin"].CGImage; // 粒子中的图片
    cell.contents = (__bridge id _Nullable)([self setRandomColorCircleImageSize:CGSizeMake(20, 20)].CGImage);
    cell.yAcceleration = arc4random_uniform(80);   // 粒子的初始加速度
    cell.xAcceleration = -cell.yAcceleration-10;
    cell.birthRate = 10.f;           // 每秒生成粒子的个数
    cell.lifetime = 6.f;            // 粒子存活时间
    cell.alphaSpeed = -0.1f;        // 粒子消逝的速度
    cell.velocity = 30.f;           // 粒子运动的速度均值
    cell.velocityRange = 100.f;      // 粒子运动的速度扰动范围
    cell.emissionRange = M_PI; // 粒子发射角度, 这里是一个扇形.
    
//    cell.scale = 0.2;
//    cell.scaleRange = 0.1;
//    cell.scaleSpeed = 0.02;
    
    CGFloat colorChangeValue  = 50.0f;
    cell.blueRange = colorChangeValue;
    cell.redRange =  colorChangeValue;
    cell.greenRange =  colorChangeValue;
    
    return cell;
}

emitterShape发射源形状取值不同时会有不同效果。

  • kCAEmitterLayerPoint:
  • kCAEmitterLayerLine: 线

7.8 CATiledLayer|CALayer

7.8.1 常规加载图片的做法

  • 有些时候我们可能需要绘制一个很大的图片,常见的例子就是一个高像素的照片或者是地球表面的详细地图
  • iOS应用通畅运行在内存受限的设备上,所以读取整个图片到内存中是不明智的。
  • 载入大图可能会相当地慢,那些对你看上去比较方便的做法(在主线程调用UIImage-imageNamed:方法或者-imageWithContentsOfFile:方法)将会阻塞你的用户界面,至少会引起动画卡顿现象。
  • 能高效绘制在iOS上的图片也有一个大小限制。所有显示在屏幕上的图片最终都会被转化为OpenGL纹理,同时OpenGL有一个最大的纹理尺寸(通常是2048*2048,或 4096*4096,这个取决于设备型号)。
    • 如果你想在单个纹理中显示一个比这大的图,即便图片已经存在于内存中了,你仍然会遇到很大的性能问题,因为Core Animation强制用CPU处理图片而不是更快的GPU
  • CATiledLayer为载入大图造成的性能问题提供了一个解决方案:将大图分解成小片然后将他们单独按需载入

7.8.2 CATiledLayer加载大图

CATiledLayerCore Animation 框架中的一个特殊的 CALayer 子类,用于有效地显示大图或者高分辨率的内容

它的作用是将大图分割成小块只在需要时才加载和显示这些小块,以提高性能和内存效率。

原理和工作机制

  • 分割大图:
    CATiledLayer 会将一个大的图片或者内容分割成多个小的切片(tiles)。
  • 动态加载:
    当用户浏览大图时,CATiledLayer 会动态地加载并显示用户所需要的切片,而不是一次性加载整张图片。
  • 显示优化:
    只有在需要时,CATiledLayer 才会加载和渲染切片,因此它能够在处理大尺寸图片或者高分辨率内容时,保持较低的内存占用和较好的性能表现。
  • 多线程处理:
    CATiledLayer 使用多线程机制来处理切片的加载和渲染,以提高用户体验和整体性能。

使用方法

  • 创建 CATiledLayer:
    通过创建 CATiledLayer 对象并将其添加到需要显示大图的视图中。
  • 设置代理:
    CATiledLayer 的代理对象需要实现 drawLayer:inContext: 方法来绘制每个切片。
  • 指定分辨率和缩放级别:
    设置 CATiledLayerlevelsOfDetaillevelsOfDetailBias 属性来控制切片的分辨率显示优先级
  • 实现代理方法:
    实现 drawLayer:inContext: 方法,根据给定的 rectcontext 绘制对应切片的内容。

7.8.3 CATiledLayer的三个重要属性

  • CATiledLayer将需要绘制的内容分割成许多小块,然后在许多线程里按需异步绘制相应的小块,具体如何划分小块和缩放时的加载策略与CATiledLayer三个重要属性有关:
    • levelsOfDetail
      • 作用: levelsOfDetail 属性用于指定 CATiledLayer 的级别(levels)的数量,即分辨率级别。这决定了在不同缩放级别下加载的切片数量。
      • 类型: Int 类型,表示级别的数量。
      • 默认值: 默认值为 1。
      • 使用场景:
        • 如果设置为 1,表示只有一个分辨率级别,所有缩放级别下加载的切片都是相同的分辨率。
        • 如果设置为较大的值,表示在不同缩放级别下会加载不同分辨率的切片,以提高显示效果和性能。
    • levelsOfDetailBias
      • 作用: levelsOfDetailBias 属性用于指定 CATiledLayer 在选择加载切片时的偏好级别(bias level)。它决定了在缩放时优先加载哪个分辨率级别的切片。
      • 类型: Int 类型,表示偏好级别的数量。
      • 默认值: 默认值为 0。
      • 使用场景:
        • 设置为较大的正数时,会倾向于加载较高分辨率的切片,从而提高显示质量。
        • 设置为负数时,则倾向于加载低分辨率的切片,以提高性能。
    • tileSize
      • 作用: tileSize 属性用于指定每个切片的尺寸。切片是 CATiledLayer 内部用于加载和显示的基本单位。
      • 类型: CGSize 类型,表示切片的尺寸。
      • 默认值: 默认值为 (256, 256)。
      • 使用场景:
        • 可以根据具体的需求和性能要求来调整切片的尺寸。较大的切片尺寸可能会提高加载效率,但也会增加内存占用和渲染负担。
        • 通常情况下,建议将切片尺寸设置为较小的值,以便在加载和显示时能够更好地控制内存使用和性能。

代码示例:

核心代码:

objc 复制代码
#import "TileImageView.h"


@implementation TileImageView{
    
    UIImage *originImage;
    CGRect imageRect;
    CGFloat imageScale;
}

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
    // Drawing code
}
*/


+(Class)layerClass{
    return [CATiledLayer class];
}
-(id)initWithImageName:(NSString*)imageName andFrame:(CGRect)frame{
    
    self = [super initWithFrame:frame];
    
    if(self){
    
        self.tileCount = 36;
        self.imageName = imageName;
        [self initSelf];
    }
    return self;
}

-(id)initWithImageName:(NSString *)imageName andFrame:(CGRect)frame andTileCount:(NSInteger)tileCount{
    self = [self initWithFrame:frame];
    
    if(self){
        self.tileCount = tileCount;
        self.imageName = imageName;
        [self initSelf];
    }
    return self;
}

-(void)initSelf{
    NSString *path = [[NSBundle mainBundle]pathForResource:[_imageName stringByDeletingPathExtension] ofType:[_imageName pathExtension]];
    originImage = [UIImage imageWithContentsOfFile:path];
    imageRect = CGRectMake(0.0f, 0.0f,CGImageGetWidth(originImage.CGImage),CGImageGetHeight(originImage.CGImage));
    imageScale = self.frame.size.width/imageRect.size.width;
    CATiledLayer *tiledLayer = (CATiledLayer *)[self layer];
    
     //根据图片的缩放计算scrollview的缩放次数
     // 图片相对于视图放大了1/imageScale倍,所以用log2(1/imageScale)得出缩放次数,
     // 然后通过pow得出缩放倍数,至于为什么要加1,
     // 是希望图片在放大到原图比例时,还可以继续放大一次(即2倍),可以看的更清晰
    
    int lev = ceil(log2(1/imageScale))+1;
    tiledLayer.levelsOfDetail = 1;
    tiledLayer.levelsOfDetailBias = lev;
    if(self.tileCount>0){
        NSInteger tileSizeScale = sqrt(self.tileCount)/2;
        CGSize tileSize = self.bounds.size;
        tileSize.width /=tileSizeScale;
        tileSize.height/=tileSizeScale;
        tiledLayer.tileSize = tileSize;
    }
    
}


-(void)setFrame:(CGRect)frame{
    [super setFrame:frame];
    imageScale = self.frame.size.width/imageRect.size.width;
    if(self.tileCount>0){
        CATiledLayer *tileLayer = (CATiledLayer *)self.layer;
        CGSize tileSize = self.bounds.size;
        NSInteger tileSizeScale = sqrt(self.tileCount)/2;
        tileSize.width /=tileSizeScale;
        tileSize.height/=tileSizeScale;
        tileLayer.tileSize = tileSize;
    }
    
}
-(CGPoint)rectCenter:(CGRect)rect{
    CGFloat centerX = (CGRectGetMaxX(rect)+CGRectGetMinX(rect))/2;
    CGFloat centerY = (CGRectGetMaxY(rect)+CGRectGetMinY(rect))/2;
    
    return CGPointMake(centerX, centerY);
}

-(void)drawRect:(CGRect)rect {
    //将视图frame映射到实际图片的frame
    CGRect imageCutRect = CGRectMake(rect.origin.x / imageScale,
                                     rect.origin.y / imageScale,
                                     rect.size.width / imageScale,
                                     rect.size.height / imageScale);
    //截取指定图片区域,重绘
    @autoreleasepool{
        CGImageRef imageRef = CGImageCreateWithImageInRect(originImage.CGImage, imageCutRect);
        UIImage *tileImage = [UIImage imageWithCGImage:imageRef];
        CGContextRef context = UIGraphicsGetCurrentContext();
        UIGraphicsPushContext(context);
        [tileImage drawInRect:rect];
        UIGraphicsPopContext();

    }
    static NSInteger drawCount = 1;
    drawCount ++;
    if(drawCount == self.tileCount){

    }
}


-(CGSize)returnTileSize{
    return [(CATiledLayer*)self.layer tileSize];
}

@end

具体大图,可以根据自己的大图进行设置

7.9 CAEAGLLayer|CALayer

CAEAGLLayerCore Animation 框架中的一个特殊类型的 CALayer 子类,用于在 iOS 和 macOS 上显示 OpenGL ES 渲染内容。它提供了一个将 OpenGL ES 渲染结果直接显示在屏幕上的高效方式。下面是对 CAEAGLLayer 的详细介绍:

  • 工作原理和特点:
    • OpenGL ES 渲染: CAEAGLLayer 提供了一个用于显示 OpenGL ES 渲染结果的表面,并通过 EAGLContext 提供的 OpenGL ES 上下文来实现渲染。
    • 高效显示: 由于 CAEAGLLayer 直接与 OpenGL ES 交互,所以能够以高效的方式显示 OpenGL ES 渲染内容,避免了额外的内存拷贝和转换。
    • 跨平台: CAEAGLLayer 可以在 iOS 和 macOS 平台上使用,以显示相同的 OpenGL ES 渲染结果。
    • 灵活性: 通过将 CAEAGLLayer 添加到视图层次结构中,可以将 OpenGL ES 渲染内容与其他 Core Animation 图层混合在一起,实现更丰富的用户界面效果。
  • 使用方法:
    • 创建 CAEAGLLayer 实例: 使用 init() 方法创建 CAEAGLLayer 实例,并设置其属性。
    • 创建并配置 EAGLContext: 创建一个 EAGLContext 实例,并将其与 CAEAGLLayer 关联。
    • 实现 OpenGL ES 渲染逻辑: 在 EAGLContext 中执行 OpenGL ES 渲染操作,将结果绘制到 CAEAGLLayer 中。
    • 将 CAEAGLLayer 添加到视图层次结构中: 通过将 CAEAGLLayer 添加到视图层次结构中,以显示 OpenGL ES 渲染结果。

代码示例:

objc 复制代码
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
#import <GLKit/GLKit.h>
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *glView;
@property (nonatomic, strong) EAGLContext *glContext;
@property (nonatomic, strong) CAEAGLLayer *glLayer;
@property (nonatomic, assign) GLuint framebuffer;
@property (nonatomic, assign) GLuint colorRenderbuffer;
@property (nonatomic, assign) GLint framebufferWidth;
@property (nonatomic, assign) GLint framebufferHeight;
@property (nonatomic, strong) GLKBaseEffect *effect;

@end
@implementation ViewController
- (void)setUpBuffers
{
    //set up frame buffer
    glGenFramebuffers(1, &_framebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
    //set up color render buffer
    glGenRenderbuffers(1, &_colorRenderbuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorRenderbuffer);
    [self.glContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.glLayer];
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_framebufferWidth);
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_framebufferHeight);
    //check success
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
        NSLog(@"Failed to make complete framebuffer object: %i", glCheckFramebufferStatus(GL_FRAMEBUFFER));
    }
}
- (void)tearDownBuffers
{
    if (_framebuffer) {
        //delete framebuffer
        glDeleteFramebuffers(1, &_framebuffer);
        _framebuffer = 0;
    }
    if (_colorRenderbuffer) {
        //delete color render buffer
        glDeleteRenderbuffers(1, &_colorRenderbuffer);
        _colorRenderbuffer = 0;
    }
}
- (void)drawFrame {
    //bind framebuffer & set viewport
    glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
    glViewport(0, 0, _framebufferWidth, _framebufferHeight);
    //bind shader program
    [self.effect prepareToDraw];
    //clear the screen
    glClear(GL_COLOR_BUFFER_BIT); glClearColor(0.0, 0.0, 0.0, 1.0);
    //set up vertices
    GLfloat vertices[] = {
        -0.5f, -0.5f, -1.0f, 0.0f, 0.5f, -1.0f, 0.5f, -0.5f, -1.0f,
    };
    //set up colors
    GLfloat colors[] = {
        0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
    };
    //draw triangle
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    glEnableVertexAttribArray(GLKVertexAttribColor);
    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 0, vertices);
    glVertexAttribPointer(GLKVertexAttribColor,4, GL_FLOAT, GL_FALSE, 0, colors);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    //present render buffer
    glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
    [self.glContext presentRenderbuffer:GL_RENDERBUFFER];
}
- (void)viewDidLoad
{
    [super viewDidLoad];
    //set up context
    self.glContext = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES2];
    [EAGLContext setCurrentContext:self.glContext];
    //set up layer
    self.glLayer = [CAEAGLLayer layer];
    self.glLayer.frame = self.glView.bounds;
    [self.glView.layer addSublayer:self.glLayer];
    self.glLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking:@NO, kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8};
    //set up base effect
    self.effect = [[GLKBaseEffect alloc] init];
    //set up buffers
    [self setUpBuffers];
    //draw frame
    [self drawFrame];
}
- (void)viewDidUnload
{
    [self tearDownBuffers];
    [super viewDidUnload];
}
- (void)dealloc
{
    [self tearDownBuffers];
    [EAGLContext setCurrentContext:nil];
}
@end

在一个真正的OpenGL应用中,我们可能会用 NSTimerCADisplayLink 周期性地每秒钟调用 -drawRect 方法60次,同时会将几何图形生成和绘制分开以便不会每次都重新生成三角形的顶点(这样也可以让我们绘制其他的一些东西而不是一个三角形而已),不过上面这个例子已经足够演示了绘图原则了

三、CoreAnimation核心动画

Core Animation 提供高效地动画能力,我们先按照派生关系的方式来了解一下动画相关类:

1. 动画相关类介绍

派生关系如图所示:

1. CAAnimation

CAAnimation是核心动画的基类

  • 不能直接使用,主要负责动画的时间速度
  • 本身实现了CAMediaTiming协议。
objc 复制代码
@interface CAAnimation : NSObject
    <NSSecureCoding, NSCopying, CAMediaTiming, CAAction>
{
@private
  void *_attr;
  uint32_t _flags;
}
@property(nullable, strong) id <CAAnimationDelegate> delegate;
CAAnimation属性 说明
timingFunction CAMediaTimingFunction速度控制函数,控制动画运行的节奏
removedOnCompletion 默认为YES,代表动画执行完毕后就从图层上移除,图形会恢复到动画执行前的状态。如果想让图层保持显示动画执行后的状态,那就设置为NO,不过还要设置fillMode为kCAFillModeForwards
delegate 代理(animationDidStartanimationDidStop

ps:CAMediaTimingFunction介绍

objc 复制代码
kCAMediaTimingFunctionLinear(线性):匀速,给你一个相对静态的感觉
kCAMediaTimingFunctionEaseIn(渐进):动画缓慢进入,然后加速离开
kCAMediaTimingFunctionEaseOut(渐出):动画全速进入,然后减速的到达目的地
kCAMediaTimingFunctionEaseInEaseOut(渐进渐出):动画缓慢的进入,中间加速,然后减速的到达目的地。这个是默认的动画行为。

1.1 CAMediaTiming协议

duration,beginTime、repeatCount、speed、timeOffset、repeatDuration、autoreverses这些时间相关的属性都在这个类中。协议中的这些属性通过一些方式结合在一起,准确的控制着时间。

CAMediaTiming属性 说明
beginTime 指定动画开始的时间。从开始延迟几秒的话,设置为【CACurrentMediaTime() + 秒数】 的方式
duration 动画的时长
speed 动画运行速度(如果把动画的duration设置为3秒,而speed设置为2,动画将会在1.5秒结束,因为它以两倍速在执行)
timeOffset 结合一个暂停动画(speed=0)一起使用来控制动画的"当前时间"。暂停的动画将会在第一帧卡住,然后通过改变timeOffset来随意控制动画进程
repeatCount 重复的次数。不停重复设置为 HUGE_VALF
repeatDuration 设置动画的时间。在该时间内动画一直执行,不计次数。
autoreverses 动画结束时是否执行逆动画,如果duration为1s,则完成一次autoreverse就需要2s。
fillMode CAMediaTimingFillMode枚举

ps:CAMediaTimingFillMode介绍

objc 复制代码
kCAFillModeRemoved:这个是默认值,也就是说当动画开始前和动画结束后,动画对layer都没有影响,动画结束后,layer会恢复到之前的状态
kCAFillModeForwards:当动画结束后,layer会一直保持着toValue的状态
kCAFillModeBackwards:如果要让动画在开始之前(延迟的这段时间内)显示fromValue的状态
kCAFillModeBoth:这个其实就是上面两个的合成.动画加入后开始之前,layer便处于动画初始状态,动画结束后layer保持动画最后的状态

注意必须配合animation.removeOnCompletion = NO才能达到以上效果

2. CAAnimationGroup|派生自CAAnimation

  • CAAnimation的子类
  • 单一的动画并不能满足某些特定需求,这时就需要用到CAAnimationGroup
  • 默认情况下,一组动画对象是同时运行的,也可以通过设置动画对象的beginTime属性来更改动画的时间
CATransition属性 说明
animations [CAAnimation],动画组

代码如下

swift 复制代码
let groupAnim = CAAnimationGroup()

//创建keyAnim
let keyAnim = CAKeyframeAnimation(keyPath: "position")
//设置values
keyAnim.values = [NSValue(cgPoint: CGPoint(x: 100, y: 200)),
                  NSValue(cgPoint: CGPoint(x: 200, y: 200)),
                  NSValue(cgPoint: CGPoint(x: 200, y: 300)),
                  NSValue(cgPoint: CGPoint(x: 100, y: 300)),
                  NSValue(cgPoint: CGPoint(x: 100, y: 400)),
                  NSValue(cgPoint: CGPoint(x: 200, y: 500))]
keyAnim.duration = 4.0

keyAnim.timingFunctions = [CAMediaTimingFunction(name: .easeInEaseOut)]

//创建渐变圆角
let animation = CABasicAnimation(keyPath: "cornerRadius")
animation.toValue = 40
animation.duration = 4.0
imgView?.layer.masksToBounds = true

groupAnim.animations = [keyAnim, animation]
groupAnim.duration = 4.0
groupAnim.repeatCount = MAXFLOAT
groupAnim.autoreverses = true

imgView?.layer.add(groupAnim, forKey: "groupAnim")
        

将动画分组在一起的更高级方法是使用事务对象(CATransaction事务类)。通过允许您创建嵌套的动画集并为每个动画分配不同的动画参数,事务提供了更大的灵活性。

3. CATransition|派生自CAAnimation

CATransition头文件

  • 动画属性:
    • type:动画过渡类型
    • subtype:动画过渡方向
    • startProgress:动画起点(在整体动画的百分比)
    • endProgress:动画终点(在整体动画的百分比)
    • .......
objc 复制代码
@interface CATransition : CAAnimation

/* The name of the transition. Current legal transition types include
 * `fade', `moveIn', `push' and `reveal'. Defaults to `fade'. */

@property(copy) NSString *type;

/* An optional subtype for the transition. E.g. used to specify the
 * transition direction for motion-based transitions, in which case
 * the legal values are `fromLeft', `fromRight', `fromTop' and
 * `fromBottom'. */

@property(copy) NSString *subtype;

/* The amount of progress through to the transition at which to begin
 * and end execution. Legal values are numbers in the range [0,1].
 * `endProgress' must be greater than or equal to `startProgress'.
 * Default values are 0 and 1 respectively. */

@property float startProgress;
@property float endProgress;

/* An optional filter object implementing the transition. When set the
 * `type' and `subtype' properties are ignored. The filter must
 * implement `inputImage', `inputTargetImage' and `inputTime' input
 * keys, and the `outputImage' output key. Optionally it may support
 * the `inputExtent' key, which will be set to a rectangle describing
 * the region in which the transition should run. Defaults to nil. */

@property(nullable, strong) id filter;

@end

/* Common transition types. */

CA_EXTERN CATransitionType const kCATransitionFade
    API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
CA_EXTERN CATransitionType const kCATransitionMoveIn
    API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
CA_EXTERN CATransitionType const kCATransitionPush
    API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
CA_EXTERN CATransitionType const kCATransitionReveal
    API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

/* Common transition subtypes. */

CA_EXTERN CATransitionSubtype const kCATransitionFromRight
    API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
CA_EXTERN CATransitionSubtype const kCATransitionFromLeft
    API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
CA_EXTERN CATransitionSubtype const kCATransitionFromTop
    API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
CA_EXTERN CATransitionSubtype const kCATransitionFromBottom
    API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));


/** Animation subclass for grouped animations. **/

转场动画过渡效果

objc 复制代码
CATransition *anim = [CATransition animation];
// 转场类型
anim.type = @"cube";
// 动画执行时间
anim.duration = 0.5;
// 动画执行方向
anim.subtype = kCATransitionFromLeft;
// 添加到View的layer
[self.redView.layer addAnimation:anim forKey];

示例Demo:

objc 复制代码
#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageV;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
 
    self.imageV.userInteractionEnabled = YES;
    //添加手势
    UISwipeGestureRecognizer *leftSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipe:)];
    leftSwipe.direction = UISwipeGestureRecognizerDirectionLeft;
    [self.imageV addGestureRecognizer:leftSwipe];
    
    UISwipeGestureRecognizer *rightSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipe:)];
    
    rightSwipe.direction = UISwipeGestureRecognizerDirectionRight;
    [self.imageV addGestureRecognizer:rightSwipe];
    
}

static int _imageIndex = 0;
- (void)swipe:(UISwipeGestureRecognizer *)swipe {
    
    
    //转场代码与转场动画必须得在同一个方法当中.
    NSString *dir = nil;
    if (swipe.direction == UISwipeGestureRecognizerDirectionLeft) {
        
        _imageIndex++;
        if (_imageIndex > 4) {
            _imageIndex = 0;
        }
        NSString *imageName = [NSString stringWithFormat:@"%d",_imageIndex];
        self.imageV.image = [UIImage imageNamed:imageName];
        
        dir = @"fromRight";
    }else if (swipe.direction == UISwipeGestureRecognizerDirectionRight) {

        _imageIndex--;
        if (_imageIndex < 0) {
            _imageIndex = 4;
        }
        NSString *imageName = [NSString stringWithFormat:@"%d",_imageIndex];
        self.imageV.image = [UIImage imageNamed:imageName];
        
        dir = @"fromLeft";
    }
    
    //添加动画
    CATransition *anim = [CATransition animation];
    //设置转场类型
    anim.type = @"cube";
    //设置转场的方向
    anim.subtype = dir;
    
    anim.duration = 0.5;
    //动画从哪个点开始
    //    anim.startProgress = 0.2;
    //    anim.endProgress = 0.3;
    
    [self.imageV.layer addAnimation:anim forKey:nil];
        
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

UIView类自带转场动画函数

  • 1、单视图

    objc 复制代码
    +(void)transitionWithView:(UIView*)view duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void(^)(void))animations 
    completion:(void(^)(BOOL finished))completion;
    • 参数说明:
      • duration:动画的持续时间
      • view:需要进行转场动画的视图
      • options:转场动画的类型
      • animations:将改变视图属性的代码放在这个block中
      • completion:动画结束后,会自动调用这个block
  • 2、双视图

    objc 复制代码
    + (void)transitionFromView:(UIView*)fromView toView:(UIView*)toView duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options
    completion:(void(^)(BOOLfinished))completion;
    • 参数说明:
      • duration:动画的持续时间
      • options:转场动画的类型
      • animations:将改变视图属性的代码放在这个block中
      • completion:动画结束后,会自动调用这个block
  • 转场动画使用注意点:转场代码必须和转场动画代码写在一起,否则无效

不同 type 的动画效果:

  • kCATransitionFade
  • kCATransitionMoveIn
  • kCATransitionPush
  • kCATransitionReveal
  • kCATransitionCube
  • kCATransitionSuckEffect
  • kCATransitionOglFlip
  • kCATransitionRippleEffect
  • kCATransitionPageCurl
  • kCATransitionPageUnCurl
  • kCATransitionCameraIrisHollowOpen
  • kCATransitionCameraIrisHollowClose

4. CAPropertyAnimation|派生自CAAnimation

  • 继承自CAAnimation,不能直接使用
  • 要想创建动画对象,应该使用它的两个子类:CABasicAnimationCAKeyframeAnimation

You do not create instances of CAPropertyAnimation: to animate the properties of a Core Animation layer, create instance of the concrete subclasses CABasicAnimation or CAKeyframeAnimation.

CAPropertyAnimation属性 说明
keyPath 通过指定CALayer的一个属性名称为keyPath(NSString类型),并且对CALayer的这个属性的值进行修改,达到相应的动画效果。比如,指定@"position"为keyPath,就修改CALayer的position属性的值,以达到平移的动画效果

5. CAKeyframeAnimation|派生自CAPropertyAnimation

  • CABasicAnimation是将属性从起始值更改为结束值
  • CAKeyframeAnimation对象是允许你以线性非线性的方式设置一组目标值的动画。
  • 关键帧动画由一组目标数据值和每个值到达的时间组成。
    • 不但可以简单的只指定值数组时间数组
    • 还可以按照路径进行更改图层的位置。
  • 动画对象采用您指定的关键帧,并通过在给定时间段内从一个值插值到下一个值来构建动画。
CAKeyframeAnimation属性 说明
values 关键帧值表示动画必须执行的值,此属性中的值仅在path属性的值为nil时才使用。根据属性的类型,您可能需要用NSValue对象的NSNumber包装这个数组中的值。对于一些核心图形数据类型,您可能还需要将它们转换为id,然后再将它们添加到数组中。将给定的关键帧值应用于该层的时间取决于动画时间,由calculationMode、keyTimes和timingFunctions属性控制。关键帧之间的值是使用插值创建的,除非将计算模式设置为kcaanimation离散
path 基于点的属性的路径,对于包含CGPoint数据类型的层属性,您分配给该属性的路径对象定义了该属性在动画长度上的值。如果指定此属性的值,则忽略值属性中的任何数据
keyTimes keyTimes的值与values中的值一一对应指定关键帧在动画中的时间点,取值范围为[0,1]。当keyTimes没有设置的时候,各个关键帧的时间是平分的
timingFunctions 一个可选的CAMediaTimingFunction对象数组,指定每个关键帧之间的动画缓冲效果
calculationMode 关键帧间插值计算模式
rotationMode 定义沿路径动画的对象是否旋转以匹配路径切线

ps:

timingFunctions:动画缓冲效果

swift 复制代码
kCAMediaTimingFunctionLinear:线性起搏,使动画在其持续时间内均匀地发生
kCAMediaTimingFunctionEaseIn:使一个动画开始缓慢,然后加速,随着它的进程
kCAMediaTimingFunctionEaseOut:使动画快速开始,然后缓慢地进行
kCAMediaTimingFunctionEaseInEaseOut:使动画开始缓慢,在其持续时间的中间加速,然后在完成之前再放慢速度
kCAMediaTimingFunctionDefault:默认,确保动画的时间与大多数系统动画的匹配

calculationMode:动画计算方式

swift 复制代码
kCAAnimationLinear:默认差值
kCAAnimationDiscrete:逐帧显示
kCAAnimationPaced:匀速 无视keyTimes和timingFunctions设置
kCAAnimationCubic:keyValue之间曲线平滑 可用 tensionValues,continuityValues,biasValues 调整
kCAAnimationCubicPaced:keyValue之间平滑差值 无视keyTimes

rotationMode:旋转方式

swift 复制代码
kCAAnimationRotateAuto:自动
kCAAnimationRotateAutoReverse:自动翻转 不设置则不旋转

代码1、用values属性

swift 复制代码
//创建动画对象
let keyAnim = CAKeyframeAnimation(keyPath: "position")
//设置values
keyAnim.values = [NSValue(cgPoint: CGPoint(x: 100, y: 200)),
                  NSValue(cgPoint: CGPoint(x: 200, y: 200)),
                  NSValue(cgPoint: CGPoint(x: 200, y: 300)),
                  NSValue(cgPoint: CGPoint(x: 100, y: 300)),
                  NSValue(cgPoint: CGPoint(x: 100, y: 400)),
                  NSValue(cgPoint: CGPoint(x: 200, y: 500))]
//重复次数 默认为1
keyAnim.repeatCount = MAXFLOAT
//设置是否原路返回 默认为false
keyAnim.autoreverses = true
//设置移动速度,越小越快
keyAnim.duration = 4.0

keyAnim.isRemovedOnCompletion = false
keyAnim.fillMode = .forwards

keyAnim.timingFunctions = [CAMediaTimingFunction(name: .easeInEaseOut)]

imgView?.layer.add(keyAnim, forKey: "keyAnim-Values")

代码2、用path属性

swift 复制代码
//创建动画对象
let keyAnim = CAKeyframeAnimation(keyPath: "position")

//创建一个CGPathRef对象,就是动画的路线
let path = CGMutablePath()
//自动沿着弧度移动
path.addEllipse(in: CGRect(x: 150, y: 200, width: 200, height: 100))
//设置开始位置
path.move(to: CGPoint(x: 100, y: 100))
//沿着直线移动
path.addLine(to: CGPoint(x: 200, y: 100))
path.addLine(to: CGPoint(x: 200, y: 200))
path.addLine(to: CGPoint(x: 100, y: 200))
path.addLine(to: CGPoint(x: 100, y: 300))
path.addLine(to: CGPoint(x: 200, y: 400))

//沿着曲线移动
path.addCurve(to: CGPoint(x: 50.0, y: 275.0), control1: CGPoint(x: 150.0, y: 275.0), control2: CGPoint(x: 70.0, y: 120.0))
path.addCurve(to: CGPoint(x: 150.0, y: 275.0), control1: CGPoint(x: 250.0, y: 275.0), control2: CGPoint(x: 90.0, y: 120.0))
path.addCurve(to: CGPoint(x: 250.0, y: 275.0), control1: CGPoint(x: 350.0, y: 275.0), control2: CGPoint(x: 110, y: 120.0))
path.addCurve(to: CGPoint(x: 350.0, y: 275.0), control1: CGPoint(x: 450.0, y: 275.0), control2: CGPoint(x: 130, y: 120.0))

keyAnim.path = path
//重复次数 默认为1
keyAnim.repeatCount = MAXFLOAT
//设置是否原路返回 默认为false
keyAnim.autoreverses = true
//设置移动速度,越小越快
keyAnim.duration = 4.0

keyAnim.isRemovedOnCompletion = false
keyAnim.fillMode = .forwards

keyAnim.timingFunctions = [CAMediaTimingFunction(name: .easeInEaseOut)]

imgView?.layer.add(keyAnim, forKey: "keyAnim-Path")

7. CABasicAnimation|派生自CAPropertyAnimation

  • CABasicAnimation是核心动画类簇中的一个类
    • 其父类是CAPropertyAnimation
    • 其子类是CASpringAnimation
    • 它的祖父是CAAnimation。
  • 它主要用于制作比较单一的动画,例如,平移缩放旋转颜色渐变边框的值的变化等,也就是将layer的某个属性值从一个值到另一个值的变化
CABasicAnimation属性 说明
fromValue 所改变属性的起始值
toValue 所改变属性的结束时的值
byValue 所改变属性相同起始值的改变量

代码如下

swift 复制代码
let baseAnim = CABasicAnimation(keyPath: "position")
baseAnim.duration = 2;
//开始的位置
baseAnim.fromValue = NSValue(cgPoint: (imgView?.layer.position)!)
baseAnim.toValue = NSValue(cgPoint: CGPoint(x: 260, y: 260))
//        baseAnim.isRemovedOnCompletion = false
//        baseAnim.fillMode = CAMediaTimingFillMode.forwards
imgView?.layer.add(baseAnim, forKey: "baseAnim-position")
imgView?.center = CGPoint(x: 260, y: 260)

7.1 防止动画结束后回到初始状态

如上面代码所示,需要添加imgView?.center = CGPoint(x: 260, y: 260)来防止防止动画结束后回到初始状态,网上还有另外一种方法是 设置removedOnCompletion、fillMode两个属性

objc 复制代码
baseAnim.removedOnCompletion = NO;
baseAnim.fillMode = kCAFillModeForwards;

但是这种方法会造成modelLayer没有修改,_view1的实际坐标点并没有在所看到的位置,会产生一些问题

7.2 CALayer动画运行的原理

CALayer有两个实例方法presentationLayer(简称P)和 modelLayer(简称M),

objc 复制代码
/* presentationLayer
 * 返回一个layer的拷贝,如果有任何活动动画时,包含当前状态的所有layer属性
 * 实际上是逼近当前状态的近似值。
 * 尝试以任何方式修改返回的结果都是未定义的。
 * 返回值的sublayers 、mask、superlayer是当前layer的这些属性的presentationLayer
 */

- (nullable instancetype)presentationLayer;

/* modelLayer
 * 对presentationLayer调用,返回当前模型值。
 * 对非presentationLayer调用,返回本身。
 * 在生成表示层的事务完成后调用此方法的结果未定义。
 */

- (instancetype)modelLayer;

从中可以看到P即是我们看到的屏幕上展示的状态,而M就是我们设置完立即生效的真实状态;打一个比方的话,P是个瞎子,只负责走路(绘制内容),而M是个瘸子,只负责看路(如何绘制)

CALayer动画运行的原理:

  • P会在每次屏幕刷新时更新状态
    • 当有动画CAAnimation(简称A)加入时,P由动画A控制进行绘制,
    • 当动画A结束被移除时P则再去取M的状态展示。
  • 但是由于M没有变化,所以动画执行结束又会回到起点。
  • 如果想要P在动画结束后就停在当前状态而不回到M的状态,我们就需要给A设置两个属性:
    • 一个是A.removedOnCompletion = NO,表示动画结束后A依然影响着P;
    • 另一个是A.fillMode = kCAFillModeForwards;
    • 这两行代码将会让A控制住P在动画结束后保持不变
  • 但是此时我们的P和M不同步,我们看到的P是toValue的状态,而M则还是自己原来的状态。举个例子:
    • 我们初始化一个view,它的状态为1,我们给它的layer加个动画,from是0,to是2,设置fillMode为kCAFillModeForewards,则动画结束后P的状态是2,M的状态是1,这可能会导致一些问题出现。比如
      • 你点P所在的位置点不动,因为响应点击的是M。所以我们应该让P和M同步,如上代码imgView?.center = CGPoint(x: 260, y: 260)需要提一点的是:对M赋值,不会影响P的显示,当P想要显示的时候,它已经被A控制了,并不会先闪现一下。

7.3 Animation-KeyPath值

上面的动画的KeyPath值我们只使用了position,其实还有很多类型可以设置,下面我们列出了一些比较常用的

keyPath值 说明 值类型
position 移动位置 CGPoint
opacity 透明度 0-1
bounds 变大与位置 CGRect
bounds.size 由小变大 CGSize
backgroundColor 背景颜色 CGColor
cornerRadius 渐变圆角 任意数值
borderWidth 改变边框border的大小((图形周围边框,border默认为黑色)) 任意数值
contents 改变layer内容(图片)注意如果想要达到改变内容的动画效果;首先在运行动画之前定义好layer的contents contents CGImage
transform.scale 缩放、放大 0.0-1.0
transform.rotation.x 旋转动画(翻转,沿着X轴) M_PI*n
transform.rotation.Y 旋转动画(翻转,沿着Y轴) M_PI*n
transform.rotation.Z 旋转动画(翻转,沿着Z轴) M_PI*n
transform.translation.x 旋转动画(翻转,沿着X轴) 任意数值
transform.translation.y 旋转动画(翻转,沿着Y轴) 任意数值

8. 检测动画的结束

核心动画支持检测动画开始或结束的时间。这些通知是进行与动画相关的任何内务处理任务的好时机。

例如,您可以使用开始通知来设置一些相关的状态信息,并使用相应的结束通知来拆除该状态。

有两种不同的方式可以通知动画的状态:

  • 使用setCompletionBlock:方法将完成块添加到当前事务。当事务中的所有动画完成后,事务将执行完成块。
  • 将委托分配给CAAnimation对象并实现animationDidStart:animationDidStop:finished:委托方法。

使用beginTime属性

  • 如果要让两个动画链接在一起,以便在另一个完成时启动,请不要使用动画通知。
  • 而是使用动画对象的beginTime属性按照所需的时间启动每个动画对象。
  • 将两个动画链接在一起,只需将第二个动画的开始时间设置为第一个动画的结束时间。

每个图层都有自己的本地时间,用于管理动画计时。通常,两个不同层的本地时间足够接近,您可以为每个层指定相同的时间值,用户可能不会注意到任何内容。但是由于superLayer或其本身Layer的时序参数设置,层的本地时间会发生变化。例如,更改Layer的speed属性会导致该Layer(及其子Layer)上的动画持续时间按比例更改。

为了确保Layer的时间值合适,CALayer类定义了convertTime:fromLayer:convertTime:toLayer:方法。我们可以使用这些方法将固定时间值转换为Layer的本地时间或将时间值从一个Layer转换为另一个Layer。这些方法可能影响图层本地时间的媒体计时属性,并返回可与其他图层一起使用的值。

可使用下面示例来获取图层的当前本地时间。CACurrentMediaTime函数返回计算机的当前时钟时间,该方法将本机时间并转换为图层的本地时间。

获取图层的当前本地时间

objc 复制代码
CFTimeInterval localLayerTime = [myLayer convertTime:CACurrentMediaTime()fromLayer:nil];

在图层的本地时间中有时间值后,可以使用该值更新动画对象或图层的与时序相关的属性。使用这些计时属性,您可以实现一些有趣的动画行为,包括:

  • beginTime 属性设置动画的开始时间
    • 通常动画开始下一个周期的时候,我们可以使用beginTime将动画开始时间延迟几秒钟。
    • 将两个动画链接在一起的方法是将一个动画的开始时间设置为与另一个动画的结束时间相匹配。
    • 如果延迟动画的开始,则可能还需要将fillMode属性设置为kCAFillModeBackwards
    • 即使图层树中的图层对象包含不同的值,此填充模式也会使图层显示动画的起始值。
    • 如果没有此填充模式,您将看到在动画开始执行之前跳转到最终值。其他填充模式也可用。
  • autoreverses 属性使动画在指定时间内执行,然后返回到动画的起始值。
    • 我们可以将autoreverses与repeatCount组合使用,就可以起始值和结束值之间来回动画。
    • 将重复计数设置为自动回转动画的整数(例如1.0)会导致动画停止在其起始值上。
    • 添加额外的半步(例如重复计数为1.5)会导致动画停止在其结束值上。
    • 使用timeOffset具有组动画的属性可以在稍后的时间启动某些动画。

9. 暂停和恢复图层的动画

ojc 复制代码
/**
 layer 暂停动画
 */
- (void)pauseLayer:(CALayer*)layer {
    CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
    layer.speed = 0.0;
    layer.timeOffset = pausedTime;
}

/**
 layer 继续动画
 */
- (void)resumeLayer:(CALayer*)layer {
    CFTimeInterval pausedTime = [layer timeOffset];
    layer.speed = 1.0;
    layer.timeOffset = 0.0;
    layer.beginTime = 0.0;
    CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
    layer.beginTime = timeSincePause;
}

2. 动画实战

由于篇幅太大,把动画相关的总结放置在下一篇文章:iOS 多媒体技术| Core Animation要点回顾2【iOS动画】

四、CoreAnimation图层渲染原理

我们在探索 iOS图层渲染原理,关于这部分,已经做过详尽阐述,在这里我们直接归纳结论:

1. CALayer显示可视化内容的原理

为什么 CALayer 可以呈现可视化内容呢?

  • CALayer 基本等同于一个 纹理 。纹理是 GPU 进行图像渲染的重要依据。
    • 计算机图形渲染原理 中提到纹理本质上就是一张图片
    • 因此 CALayer 也包含一个 contents 属性指向一块缓存区,称为 backing store,可以存放位图(Bitmap)。iOS 中将该缓存区保存的图片称为 寄宿图
  • 图形渲染流水线
    • 支持从顶点开始进行绘制(在流水线中,顶点会被处理生成纹理)
    • 也支持直接使用纹理(图片)进行渲染。
    • 相应地,在实际开发中,绘制界面也有两种方式:
      • 一种是 手动绘制;
      • 另一种是 使用图片
    • 对此,iOS 中也有两种相应的实现方式:
      • 使用图片:contents image
      • 手动绘制:custom drawing

2. Contents Image

  • Contents Image 是指通过 CALayer 的 contents 属性来配置图片
    • contents 属性的类型为 id。在这种情况下
    • 如果 content 的值不是 CGImage ,得到的图层将是空白的。
      • 在 Mac OS 系统中,该属性对 CGImage 和 NSImage 类型的值都起作用
      • 在 iOS 系统中,该属性只对 CGImage 起作用
  • 本质上,contents 属性指向的一块缓存区域,称为 backing store,可以存放 bitmap 数据。

3. Custom Drawing

  • Custom Drawing 是指使用 Core Graphics 直接绘制寄宿图

    • 实际开发中,一般通过继承 UIView 并实现 - drawRect: 方法来自定义绘制。
    • 虽然 -drawRect: 是一个 UIView 方法,但事实上都是底层的 CALayer 完成了重绘工作并保存了产生的图片。
    • 下图所示为 -drawRect: 绘制定义寄宿图的基本原理
  • UIView 有一个关联图层,即 CALayer

    • CALayer 有一个可选的 delegate 属性,实现了 CALayerDelegate 协议。UIView 作为 CALayer 的代理实现了 CALayerDelegae 协议。

    • 当需要重绘时,即调用 -drawRect:,CALayer 请求其代理给予一个寄宿图来显示。

    • CALayer 首先会尝试调用 -displayLayer: 方法,此时代理可以直接设置 contents 属性

      objc 复制代码
      - (void)displayLayer:(CALayer *)layer;
    • 如果代理没有实现 -displayLayer: 方法,CALayer 则会尝试调用 -drawLayer:inContext: 方法。

      • 在调用该方法前,CALayer 会创建一个空的寄宿图(尺寸由 bounds 和 contentScale 决定)和一个 Core Graphics 的绘制上下文,为绘制寄宿图做准备,作为 ctx 参数传入
      objc 复制代码
      - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
  • 最后,由 Core Graphics 绘制生成的寄宿图会存入 backing store

4. Core Animation 渲染流水线

4.1 Core Animation Pipeline 渲染流水线

Core Animation 渲染流水线的工作原理

  • 事实上,app 本身并不负责渲染,渲染则是由一个独立的进程负责,即 Render Server 进程
  • App 通过 IPC 将渲染任务及相关数据提交给 Render Server
  • Render Server 处理完数据后,再传递至 GPU
  • 最后由 GPU 调用 iOS 的图像设备进行显示

Core Animation 流水线的详细过程

  • Handle Events: 首先,由 app 处理事件(Handle Events)
    • 如:用户的点击操作,在此过程中 app 可能需要更新 视图树,相应地,图层树 也会被更新;
  • Commit Transaction: 其次,app 通过 CPU 完成对显示内容的计算
    • 如:视图的创建、布局计算、图片解码、文本绘制等。在完成对显示内容的计算之后,app 对图层进行打包,并在下一次 RunLoop 时将其发送至 Render Server,即完成了一次 Commit Transaction 操作;
  • Render Server: Render Server 主要执行 Open GL/Metal、Core Graphics 相关程序,并调用 GPU;
    • Decode: 打包好的图层被传输到 Render Server 之后,首先会进行解码。注意完成解码之后需要等待下一个 RunLoop 才会执行下一步 Draw Calls
    • Draw Calls: 解码完成后,Core Animation 会调用下层渲染框架(比如 OpenGL 或者 Metal)的方法进行绘制,进而调用到 GPU
    • Render: 这一阶段主要由 GPU 进行渲染,GPU 在物理层上完成了对图像的渲染
  • Display: 显示阶段。最终,GPU 通过 Frame Buffer、视频控制器等相关部件,将图像显示在屏幕上。需要等 render 结束的下一个 RunLoop 才触发显示;

对上述步骤进行串联,它们执行所消耗的时间远远超过 16.67 ms,因此为了满足对屏幕的 60 FPS 刷新率的支持,需要将这些步骤进行分解,通过流水线的方式进行并行执行,如下图所示

4.2 Commit Transaction 发生了什么

一般开发当中能影响到的就是 Handle EventsCommit Transaction 这两个阶段,这也是开发者接触最多的部分。 Handle Events 就是处理触摸事件等交互事件; 在 Core Animation 流水线中,app 调用 Render Server 前的最后一步 Commit Transaction 其实可以细分为 4 个步骤:

  • Layout
  • Display
  • Prepare
  • Commit

4.2.1 Layout(构建视图)

Layout 阶段主要进行视图构建和布局,具体步骤包括:

  1. 调用重载的 layoutSubviews 方法
  2. 创建视图,并通过 addSubview 方法添加子视图
  3. 计算视图布局,即所有的 Layout Constraint

由于这个阶段是在 CPU 中进行,通常是 CPU 限制或者 IO 限制,所以我们应该尽量高效轻量地操作,减少这部分的时间 。比如减少非必要的视图创建、``简化布局计算减少视图层级等。

4.2.2 Display(绘制视图)

  • 这个阶段主要是交给 Core Graphics 进行视图的绘制,注意不是真正的显示,而是得到前文所说的图元 primitives 数据
    • 根据上一阶段 Layout 的结果创建得到图元信息。
    • 如果重写了 drawRect: 方法,那么会调用重载的 drawRect: 方法,在 drawRect: 方法中手动绘制得到 bitmap 数据,从而自定义视图的绘制
  • 注意正常情况下 Display 阶段只会得到图元 primitives 信息,而位图 bitmap 是在 GPU 中根据图元信息绘制得到的
  • 但是如果重写了 drawRect: 方法,这个方法会直接调用 Core Graphics 绘制方法得到 bitmap 数据,同时系统会额外申请一块内存,用于暂存绘制好的 bitmap;
  • 由于重写了 drawRect: 方法,导致绘制过程从 GPU 转移到了 CPU,这就导致了一定的效率损失
  • 与此同时,这个过程会额外使用 CPU 和内存,因此需要高效绘制,否则容易造成 CPU 卡顿或者内存爆炸

4.2.3 Prepare(Core Animation 额外的工作)

Prepare 阶段属于附加步骤,一般处理图像的解码和转换等操作

  • 关于图像的 解码渲染,以及相关的优化,可以借鉴文章:
  • 1

4.2.4 Commit(打包并发送)

  • 这一步主要是:将图层打包并发送到 Render Server
  • 注意 commit 操作是 依赖图层树递归执行 的,所以如果图层树过于复杂,commit 的开销就会很大
  • 这也是我们希望减少视图层级,从而降低图层树复杂度的原因

4.3 Rendering Pass: Render Server 的具体操作

Render Server 通常是 OpenGL 或者是 Metal。以 OpenGL 为例,那么上图主要是 GPU 中执行的操作,具体主要包括:

  • GPU 收到 Command Buffer,包含图元 primitives 信息
  • Tiler 开始工作:先通过顶点着色器 Vertex Shader 对顶点进行处理,更新图元信息
  • 平铺过程:平铺生成 tile bucket 的几何图形,这一步会将图元信息转化为像素,之后将结果写入 Parameter Buffer 中
  • Tiler 更新完所有的图元信息,或者 Parameter Buffer 已满,则会开始下一步
  • Renderer 工作:将像素信息进行处理得到 bitmap,之后存入 Render Buffer
  • Render Buffer 中存储有渲染好的 bitmap,供之后的 Display 操作使用

参考与推荐

相关推荐
iFlyCai2 小时前
Xcode 16 pod init失败的解决方案
ios·xcode·swift
郝晨妤11 小时前
HarmonyOS和OpenHarmony区别是什么?鸿蒙和安卓IOS的区别是什么?
android·ios·harmonyos·鸿蒙
Hgc5588866611 小时前
iOS 18.1,未公开的新功能
ios
CocoaKier13 小时前
苹果商店下载链接如何获取
ios·apple
zhlx283515 小时前
【免越狱】iOS砸壳 可下载AppStore任意版本 旧版本IPA下载
macos·ios·cocoa
XZHOUMIN1 天前
网易博客旧文----编译用于IOS的zlib版本
ios
爱吃香菇的小白菜1 天前
H5跳转App 判断App是否安装
前端·ios
二流小码农1 天前
鸿蒙开发:ForEach中为什么键值生成函数很重要
android·ios·harmonyos
hxx2211 天前
iOS swift开发--- 加载PDF文件并显示内容
ios·pdf·swift
B.-2 天前
在 Flutter 应用中调用后端接口的方法
android·flutter·http·ios·https