前言
我们在前面,首先进行了针对 iOS中的多媒体技术相关几个框架概述:
- 进而 用 两篇文章 对 其中的
UIKit
相关要点 进行了分述:- 我们 在此篇文章 ,将 针对 Core Animation框架的要点 进一步展开分述:
一、Core Animation简介
1. 官方介绍
依照惯例,我们首先引入苹果官方文档对其的介绍:
Render, compose, and animate visual elements.
OverViewCore 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
以及许多其他可以修改的面向视觉的属性。 - 更改属性值会导致创建隐式动画
- 与View一样,图层对象也有一个边界矩形、屏幕上的
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 属性进行了设置:
- 而当设备屏幕进行刷新时,会从 CALayer 中读取生成好的
- 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.
- contents 提供了 layer 的内容,是一个指针类型
- 那么在运行时,操作系统会调用底层的接口,将 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对象定义自己的几何形状
视觉几何包含有关该内容的border
、bounds
、position
、transform
(旋转、缩放或变换)、shadow
等
1.2.1 两种类型的坐标系
使用layer
对象开发过程中,我们会涉及到两套坐标系: 点坐标系 , 单位坐标系 。 其中点坐标系 和我们在用 UIKIt中的view
开发时,相差无几
1. 点坐标系
-
指定
layer
的大小和位置,分别使用bounds
和position
属性 -
定义
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
属性设置图层的内容,可以是CGImage
、UIImage
、UIColor
等类型 -
这个属性的类型被定义为id,意味着它可以是任何类型的对象
- 在这种情况下,可以给 contents 属性赋任何值,app 仍然能够编译通过
- 但是,在实践中,如果给 contents 赋的不是CGImage, 那么得到的图层将是空白的
-
事实上,真正要赋值的类型应该是CGImageRef,它是一个指向CGImage结构的指针。
-
UIImage有一个CGImage属性,它返回一个"CGImageRef",如果想把这个值直接赋值给CALayer 的 contents ,那将会得到一个编译错误。
- 因为CGImageRef并不是一个真正的 Cocoa对象,而是一个Core Foundation类型。
- 可以通过
__bridge
关键字转换。如果要 给图层的寄宿图赋值,你可以按照以下这个方法:
objclayer.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:设置图层的背景颜色。
swiftlet layer = CALayer() layer.frame = CGRect(x: 100, y: 100, width: 100, height: 100) layer.backgroundColor = UIColor.red.cgColor
-
borderColor 和 borderWidth:设置图层的边框颜色和宽度。
swiftlayer.borderColor = UIColor.blue.cgColor layer.borderWidth = 2.0
-
cornerRadius:设置图层的圆角半径。
swiftlayer.cornerRadius = 10.0
-
shadowColor、shadowOffset、shadowOpacity 和 shadowRadius:设置图层的阴影颜色、偏移、不透明度和半径。
swiftlayer.shadowColor = UIColor.gray.cgColor layer.shadowOffset = CGSize(width: 0, height: 3) layer.shadowOpacity = 0.5 layer.shadowRadius = 5.0
-
mask:设置图层的蒙版,用于裁剪图层内容。
swiftlet 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导致其内容被剪裁。
- 图层剪切蒙版的形状包括图层的角半径(如果已指定)。图 4-3显示的图层演示了该
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变换有效
-
CGAffineTransform
和CATransform3D
的异同- 与
CGAffineTransform
类似,CATransform3D
也是一个矩阵,但是和2x3的矩阵不同,CATransform3D
是一个可以在3维空间内做变换的4x4的矩阵。 - 和
CGAffineTransform
矩阵类似,Core Animation
提供了一系列的方法用来创建和组合 CATransform3D 类型的矩阵,和Core Graphics
的函数类似 - 但是3D的平移和旋转多出了一个
z
参数,并且旋转函数除了 angle 之外多出 了 x , y , z 三个参数,分别决定了每个坐标轴方向上的旋转:
objcCATransform3DMakeRotation(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 用于按比例缩放
X
和Y
的值来计算到底要离视角多远。 - 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
来绘制的图层子类。- 参考这篇文章了解:
矢量图形
和bitmap
的区别
- 参考这篇文章了解:
- 指定诸如颜色
color
和linewidth
线宽等属性,用 CGPath 来定义想要绘制的图形,最后CAShapeLayer 就自动渲染出来了CGPath
能表示的形状,CAShapeLayer
都可以绘制出来。- 换句话说CGPath可以限制CAShapeLayer的形状。
- CAShapeLayer有一个属性Path,将路径赋值给这个属性即可。
- 也可以用
Core Graphics
直接向原始的CALyer
的内容中绘制一个路径,相比之下,使用 CAShapeLayer 有以下一些优点:- 渲染快速:
- CAShapeLayer 使用了硬件加速,绘制同一图形会比用Core Graphics快很多
- 高效使用内存:
- 一个 CAShapeLayer 不需要像普通 CALayer 一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存
- 不会被图层边界剪裁掉:
- 一个 CAShapeLayer 可以在边界之外绘制。
- 你的图层路径不会像在使用Core Graphics的普通 CALayer 一样被剪裁掉。
- 不会出现像素化:
- 当你给 CAShapeLayer 做3D变换时,它不像一个有寄宿图 的普通图层一样变得像素化。
- 渲染快速:
一些应用场景:
-
- 作为遮罩:
-
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的形状
- 作为遮罩时不用设置颜色属性,
-
- 动画效果:
- 通过不断的改变
CAShapeLayer
的path从而达到动画的效果 - 可以做出核心动画难以实现的效果,比如
粘性动画
、单边的弹性下拉效果
、qq的粘性按钮效果
、正弦波浪线
等等,相当丰富,我这里提供几个链接
粘性动画
-
- 两个属性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]; }
-
- 虚线效果:
- 虚线效果只需设置
lineDashPattern
属性:self.shapLayer.lineDashPattern = @[@(4),@(4)];
- 数组中第一个4表示先画4个点的实线,第二4表示接着间隔4个点不画线\
-
- 二维码扫描框: 二维码的扫描框通常是中间矩形为透明,其余边框为带透明度的黑色
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
显示文字示例:objcCATextLayer *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代表着结束
startPoint
和endPoint
属性- 他们决定了渐变的方向。
- 这两个参数是以
单位坐标系
进行的定义,所以左上角坐标是{0, 0},右下角坐标 是{1, 1}
案例1:红黄绿色彩渐变:
-
从红到黄
最后到绿色的渐变。 -
locations 数组指定了0.0,0.25和0.5三个数值,这样这三个渐变就有点像挤在了左上角
swiftclass 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.6objc//创建复制图层容器 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
表示复制图层在被创建时产生的和上一个复制图层的位移objcCAReplicatorLayer *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的效果,其他几个属性用法类似
objcCAReplicatorLayer *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旋转变换,该方块旋转之后自己的坐标系也发生了同样角度的旋转(感觉是每个方块都有自己的坐标系),在旋转之后再要进行平移操作,那也是按照旋转之后的坐标系进行平移。 (上面的推断纯属个人判断,如有错误还望指正!)
objcCATransform3D 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
:渲染模式,控制着在视觉上粒子图片是如何混合的。objcNSString * const kCAEmitterLayerUnordered; NSString * const kCAEmitterLayerOldestFirst; NSString * const kCAEmitterLayerOldestLast; NSString * const kCAEmitterLayerBackToFront; NSString * const kCAEmitterLayerAdditive;
-
emitterMode
: 发射模式,这个字段规定了在特定形状上发射的具体形式是什么objckCAEmitterLayerPoints: 点模式,发射器是以点的形势发射粒子。 kCAEmitterLayerOutline:这个模式下整个边框都是发射点,即边框进行发射 kCAEmitterLayerSurface:这个模式下是我们边框包含下的区域进行抛洒 kCAEmitterLayerVolume: 同上
-
emitterShape
:规定了发射源的形状。objckCAEmitterLayerPoint:点形状,发射源的形状就是一个点,位置在上面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 注意
-
CAEmitterLayer
和CAEmitterCell
中 有相同的属性,他们控制相同的特性 -
若是相同属性都各自设置了值,粒子发射引擎在工作的时候,会把两个值相乘。作为这个属性的最终值来控制显示效果
-
相同属性如下:
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加载大图
CATiledLayer
是 Core Animation
框架中的一个特殊的 CALayer 子类,用于有效地显示大图
或者高分辨率的内容
。
它的作用是将大图分割成小块
,只在需要时才加载和显示
这些小块,以提高性能和内存效率。
原理和工作机制
- 分割大图:
CATiledLayer
会将一个大的图片或者内容分割成多个小的切片(tiles
)。 - 动态加载:
当用户浏览大图时,CATiledLayer
会动态地加载并显示用户所需要的切片,而不是一次性加载整张图片。 - 显示优化:
只有在需要时,CATiledLayer
才会加载和渲染切片,因此它能够在处理大尺寸图片或者高分辨率内容时,保持较低的内存占用和较好的性能表现。 - 多线程处理:
CATiledLayer
使用多线程机制来处理切片的加载和渲染,以提高用户体验和整体性能。
使用方法
- 创建
CATiledLayer
:
通过创建CATiledLayer
对象并将其添加到需要显示大图的视图中。 - 设置代理:
CATiledLayer
的代理对象需要实现drawLayer:inContext:
方法来绘制每个切片。 - 指定分辨率和缩放级别:
设置CATiledLayer
的levelsOfDetail
和levelsOfDetailBias
属性来控制切片的分辨率
和显示优先级
。 - 实现代理方法:
实现drawLayer:inContext:
方法,根据给定的rect
和context
绘制对应切片的内容。
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)。
- 使用场景:
- 可以根据具体的需求和性能要求来调整切片的尺寸。较大的切片尺寸可能会提高加载效率,但也会增加内存占用和渲染负担。
- 通常情况下,建议将切片尺寸设置为较小的值,以便在加载和显示时能够更好地控制内存使用和性能。
- 作用:
- levelsOfDetail
代码示例:
核心代码:
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
CAEAGLLayer
是 Core 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应用中,我们可能会用 NSTimer
或 CADisplayLink
周期性地每秒钟调用 -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 | 代理(animationDidStart 、animationDidStop ) |
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
,不能直接使用 - 要想创建动画对象,应该使用它的两个子类:
CABasicAnimation
和CAKeyframeAnimation
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控制了,并不会先闪现一下。
- 你点P所在的位置点不动,因为响应点击的是M。所以我们应该让P和M同步,如上代码
- 我们初始化一个view,它的状态为1,我们给它的layer加个动画,from是0,to是2,设置
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 并实现
-
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 可能需要更新
视图树
,相应地,图层树
也会被更新;
- 如:用户的点击操作,在此过程中 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 在物理层上完成了对图像的渲染
- Decode: 打包好的图层被传输到
- Display: 显示阶段。最终,GPU 通过
Frame Buffer
、视频控制器等相关部件,将图像显示在屏幕上。需要等 render 结束的下一个 RunLoop 才触发显示;
对上述步骤进行串联,它们执行所消耗的时间远远超过 16.67 ms,因此为了满足对屏幕的 60 FPS 刷新率的支持,需要将这些步骤进行分解,通过流水线的方式进行并行执行,如下图所示
4.2 Commit Transaction 发生了什么
一般开发当中能影响到的就是 Handle Events
和 Commit Transaction
这两个阶段,这也是开发者接触最多的部分。 Handle Events
就是处理触摸事件
等交互事件; 在 Core Animation
流水线中,app 调用 Render Server
前的最后一步 Commit Transaction
其实可以细分为 4 个步骤:
Layout
Display
Prepare
Commit
4.2.1 Layout(构建视图)
Layout 阶段主要进行视图构建和布局,具体步骤包括:
- 调用重载的
layoutSubviews
方法 - 创建视图,并通过
addSubview
方法添加子视图 - 计算视图布局,即所有的
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 操作使用
参考与推荐
- 01-iOS核心动画高级技巧(翻译官方文档)
- 02-官方文档