欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Core Animation

程序员文章站 2022-03-16 17:11:52
...

原文地址:Core Animation Programming Guide

概述

Core Animation是一套作用于app的视图和其他可视化的元素的系统,为视图的展示提供了更好的支持,并且还能支持视图的内容进行动画。Core Animation通过将视图的内容缓存到位图中,提供给图形硬件进行直接操作来达到优化的目的。 大多数时候我们都会使用Core Animation来改变视图或者其他可视的对象,比方说改变某个视图的位置,大小或者是透明度等等属性。

Layer为视图绘制和动画提供了基础

Layer对象是3D空间中的2D表面,同时也是Core Animation的核心。跟view一样,layers负责管理几何,内容等属性。但是和view不同的地方在于layer不会定义它们自己的样子。也就是说,layer对象基本上只会负责位图信息相关的属性。位图是view绘制自身时得到的或者是某个指定的固定的图像。因此,主要的layers会被认为是一个model对象。

基于layer的绘制模型

大部分的layer的工作是获取app提供的视图内容并将其缓存进位图当中,而不会负责图形内容的绘制。当我们改变layer的属性时,实际上,我们改变的是跟layer对象相关联的状态信息。在触发动画的时候,Core Animation会将layer的位图和状态信息发送到图形硬件中,然后图形硬件就会根据新的信息来渲染位图。在硬件层面操作位图的话,会比通过软件来操作快得多

因为图形硬件操作的是一个全局的位图对象,所以基于layer的绘制会跟传统上基于视图的绘制方法有所不同。在基于视图的绘制方法中,改变视图本身通常会触发视图的drawRect:方法来重绘视图的内容。这种方法的消耗相对较高,因为重绘过程中是在主线程调用CPU来完成的。而Core Animation则是避免了这种消耗。

虽然Core Animation会尽可能多的使用缓存内容,但是我们仍是需要提供初始化的内容和更新的信息。更多的细节会在下面的章节中阐述。

基于Layer的动画

layer对象的数据和状态信息与当前屏幕上layer的内容是解耦开来的。这种解耦的方式使得Core Animation可以通过动画来进行更新。

在动画过程中,Core Animation会在硬件中一帧一帧地完成工作。我们所需要做的,就只是提供起始和结束状态的信息。除此之外,我们也可以指定时间还有动画相关的参数。

Layer对象负责自身几何信息

layer对象的工作之一就是管理可视化内容的几何信息。几何信息包括内容的边界,位置和是否被旋转,缩放,变换等。跟view一样,layer也有framebounds属性。不过layer也有view所没有的属性anchorPoint,用于定义操作所将要发生的点。

Layers使用两套坐标系统

Layer使用基于点的坐标系统和单位坐标系统来指定内容的位置信息。具体使用哪套系统则是根据传入的信息来决定的。当传入的值是直接映射到屏幕的坐标系或者与另一个layer对象相对的属性时,使用基于点的坐标系。当传入的值不需要与屏幕坐标系进行绑定时使用单位坐标系,因为该值会跟其他值进行相关联。比方说layer的anchorPoint属性指定了layer自身的边界相关的点,这个属性使用的就是单位坐标系。

不过比较常用的还是基于点的坐标系,因为我们会经常使用layer的boundsposition等属性。bounds定义了layer自身的坐标系和屏幕中layer的大小。position定义了layer对象在父类坐标系上的位置。虽然layer有frame属性,但是因为frame是从boundsposition中派生出来的,所以在layer对象上使用会比较少。

layer的boundsframe所规定的矩形位置会根据不同的平台而有所不同,如下图所示:

锚点是少数几个需要使用单位坐标系来进行描述的属性之一。Core Animation使用单位坐标系来表示那些在layer的大小发生变化时,会随着变化的属性。

所有坐标系中的值都被指定为浮点数。

锚点会影响几何操作

layer的几何操作会根据layer的锚点来进行操作。在对position或者是transform属性进行操作时,锚点的影响会越发明显。

下图展示了锚点的变化如何影响位置属性。虽然layer并没有在父类的边界中进行移动,但是锚点的移动却改变了它的position属性:

下图则是展示了锚点的改变如何影响layer的变换。当在layer对象上使用旋转变换时,旋转将锚点作为中心点来进行变换。

Layers的三维操作

每一个layer都有两个变换矩阵。CALayertransform属性指定了你所想要应用的变换,包括layer自身以及其sublayer。一般情况下,在想要修改layer自身的时候,你就可以通过transform属性来进行修改。举个例子,你可以使用它来进行缩放,旋转或者改变位置。而sublayerTransform属性则定义了sublayer上的变换,通常用于添加一个透视的视觉效果。

变换通过矩阵运算来获取新的坐标值。由于Core Animation的值能够使用三维平面来进行表示,所以每个坐标点可以通过一个4*4的矩阵来进行变换。

在Core Animation中,变换可以使用CATransform3D类来进行表示。不过好在Core Animation提供了一系列复杂的函数来创建缩放,翻转,旋转等变换用的矩阵,所以我们不需要直接来操作这个复杂的矩阵运算。

下图中是一些常用的变换使用的矩阵。任何坐标与单位矩阵相乘的话,会得到与原来坐标相同的结果。对于其他变换的话,坐标的改变取决于矩阵。

Layer树能反映出动画状态的不同视点

使用Core Animation的app有三个集合的layer对象。每一集合对于屏幕上的内容而言都是不同的角色:

  • model layer tree中的对象与app的交互是最多的。该树中的对象是保存了目标值的model对象。当我们更改layer的属性时,使用的就是此树中的对象之一
  • presentation tree包含了动画进行中的值。presentation tree中的值代表的是当前屏幕上的值。我们不能够修改此树中的值。不过我们可以通过此树来获取当前的动画值。Presentation Tree强调的是当前的状态。
  • render tree的对象负责执行实际的动画,并且对于Core Animation而言是private的

每个集合的layer对象跟views一样,都是由分层结构构成的。实际上,当app允许所有view使用layer的时候,每棵树会根据view的分层结构来初始化自身的结构。但是,app可以根据需要添加另外的layer对象,也就是说,layer可以不跟view相关的layer的层次结构进行关联。我们可以通过这样的规则,来对app的显示进行优化。

对于layer tree中的每个对象,都能在当前显示的内容和render tree中找到对应的对象。像之前所说的,app主要使用的是model layer tree的对象进行工作,有时也会访问下presentation tree的对象。在访问model layer tree中的对象的presentationLayer属性时会返回presentation tree中的对象的对象。也就可以通过这个属性来获取进行动画过程中的当前值。

重点:只能在动画进行过程中来访问presentation tree中的对象。

Layers和Views的关系

Layers并不是view的替代品,即我们不能只通过layer对象来创建可视化接口。Layer为view的创建提供了基础。值得一提是,layer使得view对象的绘制更有效率,能够使view内容“动起来”并且在动画过程中保持较高帧率。不过,layer不能做的事情也有很多。Layer并不能处理用户事件,绘制内容,参与响应链等等等等。所以,view对象的存在是必要的。

在iOS中,每个view都有一个相应的layer对象,不过在OS X中则需要决定哪个view应该有对应的layer。iOS中,所有的views都是属于layer-backed(层支持)

Note: 对于layer-backed的view来说,推荐做法是尽可能在view上而不是layer上进行操作。在iOS中,view只是围绕layer对象的薄封装,因此你所指定的发生在layer上的操作也能通过view对象来进行。不过也有一些情况会使得在layer上进行操作变换时获取不到期望结果

除了与view对应的layer对象外,我们也可以创建独立的layer对象。比方说,如果我们想要在不同的地方使用同一张图片的话,我们可以只加载一次图片,然后将它与某个独立的layer对象进行关联,再将这些对象添加到layer tree中。这样的话,每个layer都与源图进行关联,而不会在内存中创建图片的copy。

设置Layer对象

Layer对象是Core Animation的核心。Layer管理着app的可视的内容并提供了可修改的选项。在iOS上已经自动提供了对layer的支持。

改变View相关的Layer对象

Layer-backed的视图会默认创建一个CALayer的实例且大部分情况下并不需要修改layer的类型。虽然如此,Core Animation仍针对了不同的情况提供了不同类型的layer。选择不同的class可以提高性能。比方说,CATiledLayer可以用于展示大图片。

改变UIView使用的Layer类型

你可以通过重写layerClass方法来返回其他类型的layer对象。大部分iOS的view会创建CALayer对象并将其作为内容的备份。对于大部分视图来说,使用CALayer就能满足我们的需求。不过我们还是可以根据实际来进行选择,比方说:

  • 通过Metal或者是OpenGL ES来进行内容绘制的话,可以使用CAMetalLayer或者是CAEAGLLayer
  • 有对应的layer类可以提供更好的性能体验
  • 想要使用Core Animation提供的功能,比方说粒子发射器和复制器

改变layer的类的做法很直接:

+ (Class)layerClass {
  return [CAMetalLayer class];
}
复制代码

所需要做的就只是重写layerClass,返回所需要的Layer类即可。在展示出来之前,view会调用layerClass方法并使用其返回值来创建新的layer对象。一旦创建成功,view的layer对象就不会改变。

Layer类的使用场景

Class 使用场景
CAEmitterLayer 用于实现基于Core Animation的粒子发射系统。CAEmitterLayer负责粒子的生成和起点
CAGradientLayer 用于绘制颜色梯度
CAMetalLayer 使用Metal来为渲染曾设置和可绘制纹理
CAEAGLLayer/CAOpenGLLayer OpenGL ES或者是OpenGL时使用
CAReplicatorLayer 在想要使用自动复制多个sublayer时使用。
CAScrollLayer 用于管理多个sublayer组成的大大的可滚动区域
CAShapeLayer 用于绘制立方贝塞尔样条。Shape layers一般用于绘制基于路径的shapes。会在主线程渲染shape并缓存
CATextLayer 渲染字符串
CATiledLayer 用于将大图分割成多张小图时使用
CATransformLayer 渲染3D layer层次
QCCompositionLayer 渲染Quartz Composer(OS X)

Layer的内容

Layer的内容由包含着想要在屏幕上展示的位图组成。你可以通过下面三种方式来提供位图:

  • 将image对象直接赋值给layer的contents属性(用于layer的内容不会发生变化的情况下)
  • 分配一个delegate对象给layer,并让delegate来绘制(用于layer的内容偶尔会发生变化的情况)
  • 定义layer的子类并重写其中一个绘制方法来提供内容(用于创建自定义的layer子类,或者是想自定义绘制方法)

只有在自己创建layer对象时,才需要考虑为layer提供内容。如果你的app只包含了layer-backed的view时,你并不需要考虑这方面的内容。

使用图片作为内容

Layer实际上上只是管理位图图片的容器,所以我们可以将image直接赋值给layer的contents属性。Layer对象会直接使用我们所提供的图片,并不会去创建自身对image的副本。这种做法,在app需要在多个不同位置使用同一张图时,可以有效降低内存消耗。

传递给layer的image必须是CGImageRef类型的对象。当分配图片时,需要记住的是要根据设备的分辨率来进行图片的调整。对于Retina屏的设备来说,需要调整image的contentsScale属性。

通过Delegate来提供layer内容

如果layer的内容是动态变化的,你可以使用delegate对象来提供,更新内容。在展示过程中,layer会调用delegate方法来获取所需的内容:

  • 如果delegate对象实现了displayLayer:方法,则该方法负责创建位图并将位图分配给layer的contents属性。
  • 如果delegate实现了drawLayer:inContext:方法,Core Animation创建位图,创建绘制位图的图形上下文,然后会调用delegate的方法来填充位图。delegate方法所需要做的就是将位图绘制到给定的上下文当中。

delegate对象必须实现displayLayer:或者是drawLayer:inContext:的其中之一。如果两个方法都实现了的话,就只会调用displayLayer:方法

- (void)displayLayer:(CALayer *)theLayer {
  // Check the value of some state property
  if (self.displayYESImage) {
    theLayer.contents = [someHelperObject loadStateYESImage];
  } else {
    theLayer.contents = [someHelperObject loadStateNoImage];
  }
}
复制代码

如果不想使用预先渲染好的图片或者是一个helper对象来创建位图的话,delegate就需要通过drawLayer:inContext:方法来提供内容:

- (void)drawLayer:(CALayer *)theLayer inContext:(CGContextRef)theContext {
  CGMutablePathRef thePath = CGPathCreateMutable();

  CGPathMoveToPoint(thePath, NULL, 15.0f, 15.f);
  CGPathAddCurveToPoint(thePath, NULL, 15.f, 250.0f, 295.0f, 250.0f, 295.0f, 15.0f);
  
  CGContextBeginPath(theContext);
  CGContextAddPath(theContext, thePath);
  
  CGContextSetLineWidth(theContext, 5);
  CGContextStrokePath(theContext);

  CFRelease(thePath);
}
复制代码

在layer-backed的view上使用自定义内容的话,应该重写view的方法来实现自定义内容的绘制。layer-backed的view会将自己作为layer的delegate,并且实现所需的delegate方法,且不能更改其配置。因此,在view上实现自定义内容的话,需要重写drawRect:方法

通过子类来提供内容

如果想要通过继承Layer类来实现自己的layer的话,可以重写layer的方法来实现绘制。当继承时,我们可以使用下面的任一方法来绘制内容:

  • 重写display方法并将其设置为layer的contents
  • 重写drawInContext:方法并将其绘制到指定的上下文中

使用哪一种方法则是取决于你想要在绘制过程中获取到多少的控制权了。display方法是更新layer内容的入口,所以重写该方法可以在绘制过程中获得完全的控制权。同时也意味着你需要负责创建CGImageRef对象来分配给contents。如果你仅仅想要绘制内容的话,重写drawInContext:就能满足你的需求了

内容调整

当我们将图片分配image给contents的时候,layer的contentGravity属性会决定在当前的边界下图片的布局位置。默认情况下,如果图片比当前边界大或者小的时候,layer对象会在可接受的范围内缩放图片。如果layer的比例跟image的不一致的话,可能会导致图片被扭曲。因此我们可以通过contentsGravity来调整。contentsGravity属性的值被分为两类:

  • 基于位置的重力常量允许我们将图片固定在指定的边角,这种做法不会对图片进行缩放

从上图可以看出,除了kCAGravityCenter这个常量外,其他常量都会将image固定在对应的边边角角上,kCAGravityCenter则将图片进行居中操作。这些常量不会对图片进行缩放处理。图片会按照原来的大小进行渲染。不过当图片大于layer的边界时,边界外的会被截掉,当小于layer边界时,小的那部分就会显示layer的背景。

  • 基于缩放的重力常量允许我们对图片进行缩放

所有常量会对图片进行缩放调整,常量之间的区别在于它们处理图片的比例值。默认情况下的值是kCAGravityResize

高分辨率图片

Layer并不会从当前的屏幕中获取到分辨率相关的信息。仅仅是保存指向位图的指针并将位图以合适的像素进行展示。如果你将图片设置为layer的contents的话,你必须通过contentsScale的设置来告诉Core Animation当前图片的分辨率。默认值是1.0,在Retina中的话是2.0

只有当你将位图设置为layer的时候才有需要更改contentsScale的值。

调整Layer的可视化风格和模样

Layer对象已经创建了诸如边界和背景颜色等装饰用的属性。

Layer的背景和边界

layer对象除了本身的内容以外,还可以拥有背景和边界。背景颜色会在layer的内容和边界渲染之前进行渲染,如下图所示:

myLayer.backgroundColor = [UIColor greenColor].CGColor;
myLayer.borderColor = [UIColor blackColor].CGColor;
myLayer.borderWidth = 3.0;
复制代码

如果你将layer的背景颜色设置为不透明的颜色的话,考虑将layer的不透明属性设置为YES。这样做的话可以在屏幕渲染的时候提高性能并且去除了layer的备份中alpha通道的信息。

圆角半径

可以为layer添加圆角半径来创建圆角矩形的效果。因为圆角半径使用的是transparency mask, 所以并不会影响layer的内容显示,除非layer的maskToBounds属性被设置为YES。不过,圆角半径会印象layer的背景颜色和边界。

要在layer上使用圆角半径的话,设置cornerRadius属性即可。

内置阴影

CALayer包含了阴影属性的配置。在layer对象中,你可以控制阴影的颜色,根据layer的内容,透明度和形状等进行改变。

默认情况下,layer的阴影的透明度值是0,也就是阴影是隐藏的。通过改变透明度的值来触发Core Animation对阴影的绘制。因为阴影默认情况下是直接绘制在layer下方,因此在绘制之前我们可以进行偏移量的修改。

在添加阴影的时候,阴影会变成layer内容的一部分,并且会扩展layer的边界矩形。也正是因为如此,所以如果将maskToBounds设置为YES的时候,阴影的效果会根据边缘进行裁剪。

动画

通过Core Animation我们可以很容易地为layer对象创建复杂的动画效果。

简单的动画

我们可以根据实际的需求来显式或隐式执行动画。隐式动画使用默认的时间以及动画属性来执行动画,而显式动画则需要配置动画对象。因此,一般情况下,隐式动画能够满足我们的基本需求。

Important: 虽然有时候我们可以通过Core Animation接口来使layer-backed的view执行动画,但是这种做法需要较多的步骤

要触发一个隐式动画的话,仅需要更新layer对象的属性即可。当我们修改了layer tree中的layer对象时,我们马上能得到对象的变化响应。但是,layer对象的外观并不会马上改变。Core Animation会使用你的改变作为触发点来创建动画的执行。

下面的代码会使Core Animation创建动画对象并且将动画的执行添加到下一次更新中:

theLayer.opacity = 0.0;
复制代码

要创建显式动画的话,可以通过CABasicAnimation对象并使用该对象来配置动画参数。我们可以在将动画对象添加到layer之前设置动画的起始和结束值,动画时长,或者是改变其他参数:

CABasicAnimation *fadeAnim = [CABasicAnimation animationWithKeyPath:@"opacity"];
fadeAnim.fromValue = [NSNumber numberWithFloat:1.0];
fadeAnim.toValue = [NSNumber numberWithFloat: 1.0];
fadeAnim.duration = 1.0;
[theLayer addAnimation:fadeAnim forKey:@"opacity"];

theLayer.opacity = 0.0;
复制代码

Tip: 在创建显式动画时,推荐的做法是设置好fromValue的值。如果没有指定该值的话,Core Animation会使用layer的属性的当前值作为起始值。如果你已经更新过属性值的话,可能最后动画得到的结果并不如你所愿

跟通过更新layer的数据的隐式动画不同,显式动画并不会修改layer tree中的值。显式动画仅会生成动画。在动画结束时,Core Animation会从layer中移除动画对象并且根据当前的数值重绘layer。如果想要显式动画的修改是动态的,则必须更新layer的属性。

不管是隐式还是显式动画,正常情况下都会在当前的run loop结束时开始执行,因此当前的线程必须有一个run loop,以便执行动画。如果给layer添加多个动画对象时,动画会在同一时间执行。

使用关键帧动画来修改layer属性

当基于属性的动画更改属性值时,CAKeyframeAnimation类的对象会让你通过一系列线性或者非线性的值来进行动画的调整。关键帧动画由一系列目标数据和对应的时间点组成。可以通过数组来存储数据和时间参数来实现简单的配置。比方说要更改layer的位置,你可以根据设置的路径来执行动画变化:

CGMutablePathRef thePath = CGPathCreateMutable();
CGPathMoveToPoint(thePath, NULL, 74.0, 74.0);
CGPathAddCurveToPoint(thePath, NULL, 74.0, 500.0,
                                                                    320.0, 500.0,
                                                                    320.0, 74.0);
CGPathAddCurveToPoint(thePath, NULL, 320.0, 500.0,
                                                                    566.0, 500.0,
                                                                    566.0, 74.0);
CAKeyframeAnimation *theAnimation;

// Create the animation object, specifying the position property as the key path.
theAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
theAnimation.path = thePath;
theAnimation.duration = 5.0;

// Add the animation to the layer
[theLayer addAnimation:theAnimation forKey:@"position"];
复制代码

指定关键帧的值

关键帧的值是关键帧动画中最重要的部分。这些值定义了动画的行为。主要的方式是通过数组来进行关键帧值的赋值。

当指定某个数组时,数组中的元素的类型取决于想要修改的那个属性的数据类型。当然你也可以直接添加对象,不过这些对象必须再被添加之前转化为id类型,所有标量或者结构体必须被封装成对象体:

  • 对于CGRect类型的属性,将每个矩形封装为NSValue对象
  • 对于layer的变换的属性,将每个CATransform3D矩阵封装成为NSValue对象。
  • 对于borderColor的话,将每个CGColorRef数据类型转化为id类型
  • CGFloat的话,封装成NSNumber
  • 当layer的contents属性需要进行动画时,数组中的类型需要是CGImageRef

指定关键帧动画的时间函数

关键帧动画的时间和速率相对基本动画来说则复杂得多。不过我们也能通过以下的属性来进行控制:

  • calculationMode定义了计算动画时间的算法,有以下这些值:
    • 线性和立方体动画 - kCAAnimationLinearkCAAnimationCubic。根据提供的时间信息来生成动画。
    • 节奏动画 - kCAAnimationPacedkCAAnimationCubicPaced。不会依赖keyTimes或者timingFUnctions参数的信息,而是隐式计算出一个常量速率进行动画。
    • 离散动画 - kCAAnimationDiscrete。从一个关键帧改变时并不会有任何插值出现。根据keyTimes属性来计算,忽略timingFunction
  • keyTimes属性指定了每个关键帧的时间。只有当calculationMode被设定为kCAAnimationLinear, kCAAnimationDiscrete或者是kCAAniamtionCubic时生效
  • timingFunctions指定了关键帧段的时间曲线

如果你想要控制动画的时间,则可以使用kCAAnimationLinear或者是kCAAnimationCubickeyTimestimingFunctions属性。timingFunctions允许你实现ease-in或者是ease-out的动画曲线。如果没指定的话,动画就是线性的。

停止显式动画的执行

动画一般不执行完毕的话是不会停下来的,但是我们可以通过以下方法来使其停止:

  • 移除layer中的一个动画对象。调用removeAnimationForKey:来移除,key不能为nil
  • 调用removeAllAnimations来移除layer中所有动画对象。这个方法会立马移除所有动画,并且使用layer当前的信息来重绘。

Note:不能直接移除layer的隐式动画

当移除动画对象时,Core Animation会根据当前信息来重绘layer,因为当前值一般都是动画结束时的值,所以会导致layer立马跳到结束状态。如果想保持停止动画时的最后的那个状态,则可以使用presentation tree来获取相关的信息并进行layer tree中的值的设置。

多个变化一起的动画

如果想要在一个layer上同时执行多个动画对象,可以将动画对象使用CAAnimationGroup封装成一个group,会大大简化操作。

// Animation 1
CAKeyframeAnimation *widthAnim = [CAKeyframeAnimation animationWithKeyPath:@"borderWidth"];
NSArray *widthValues = [NSArray arrayWithObjects:@1.0, @10.0, @5.0, @30.0, @0.5, @15.0, @2.0, @50.0, @0.0, nil];
widthAnim.values = widthValues;
widthAnim.calculationMode = kCAAnimationPaced;

// Animation 2
CAKeyframeAnimation *colorAnim = [CAKeyframeAnimation animationWithKeyPath:@"borderColor"];
NSArray *colorValues = [NSArray arrayWithObjects:(id)[UIColor greenColor].CGColor, (id)[UIColor redColor].CGColor, (id)[UIColor blueColor].CGColor, nil];
widthAnim.values = colorValues;
widthAnim.calculationMode = kCAAnimationPaced;

// Animation group
CAAnimationGroup *group = [CAAnimationGroup animation];
group.animations = [NSArray arrayWithObjects:colorAnim, widthAnim, nil];
group.duration = 5.0;

[myLayer addAnimation:group forKey:@"BrderChanges"];
复制代码

监测动画结束

Core Animation允许我们检测动画的开始和结束。有两种不同的方式可以获取到动画的状态:

  • 使用setCompletionBlock:来为当前的过渡添加一个block。当过渡中所有的动画完成时,过渡会执行你设定的block
  • 设定CAAnimation对象的delegate并实现animationDidStart:animationDidStop:finished方法

如果想要连接两个动画并且其中一个在另外一个结束时才开始执行的话,则不要使用动画的通知。而是使用beginTime属性来设置每个动画的开始的时刻。

Layer-Backed 视图的动画

如果layer是属于layer-backed的视图的话,推荐的做法是通过UIKit提供的接口来实现动画效果。

iOS中修改layer的规则

因为iOS的视图都对应着一个layer,UIView类本身也会从layer对象中直接获取大部分数据。因此,对layer的修改也会自动映射到view的对象上。这就意味着你可以使用Core Animation或者是UIView接口来完成这些修改。

如果想要使用Core Animation的类来初始化动画,则必须将Core Animation的调用放到基于view的block当中。默认情况下,UIView是禁止layer的动画的,但是会在动画block中重新启用。

[UIView animateWithDuration:1.0 animations:^{
  myView.layer.opacity = 0.0;

  CABasicAnimation *theAnim = [CABasicAnimation animationWithKeyPath:@"position"];
  theAnim.fromValue = [NSValue valueWithCGPoint:myView.layer.position];
  theAnim.toValue = [NSValue valueWithCGPoint:myNewPosition];
  theAnim.duration = 3.0;
  [myView.layer addAnimation:theAnim forKey:@"AnimateFrame"];
}];
复制代码

高级动画技巧

除了上面的基本动画之外,还有其他技巧可以使得我们的动画变得生动有趣

过渡动画支持layer可变性的改变

从名称我们可以看出,过渡动画对象会创建可见的动画。最常见的使用方式就是协调一个layer的消失和另一个layer的显式。与基于属性的动画不同,过渡动画想要利用layer的缓存图像来创建效果是很难做到的。标准的过渡类型有翻转,移动等等。

要执行一个过渡动画,需要创建一个CATransition对象并将其添加到layer中。使用过渡的对象来指定来类型和开始,结束点。

CATransition *transition = [CATransition animation];
transition.startProgress = 0;
transition.endProgress = 1.0;
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromRight;
transition.duration = 1.0;

[myView1.layer addAnimation:transition forKey:@"transition];
[myView2.layer addAnimation:transition forKey:@"transition];

myView1.hidden = YES;
myView2.hidden = NO;
复制代码

当两个layer涉及到同样的过渡动画时,可以使用同一个过渡动画对象。

自定义动画的时间

时间是动画中重要的一部分,通过CAMediaTiming协议的属性我们可以为动画指定精确的时间信息。有两个Core Animation的类遵循了这个协议。CAAnimation允许你指定时间信息,CALayer允许你为你的隐式动画配置相关的时间参数。

在考虑时间和动画时,需要了解layer对象对于时间参数的使用。每个layer都有自己的local时间来管理动画时间。一般情况下,两个不同的layer的本地时间相差不大,所以我们可以指定同一个值,用户也不会察觉出区别。但是,layer的本地时间可以被其父layer或者是其timing参数修改。比方说,改变layer的speed会改变layer的动画时长.

为了帮助你确定layer的时间,CALayer定义了convertTime:fromLayer:convertTime:toLayer:两个方法,你可以使用这连个方法来将时间参数和layer的时间进行互相转换。比方下面的代码,就是用于获取layer的当前本地时间

CFTimeInterval lacalLayerTime = [myLayer convertTime:CACurrentMediaTIme() fromLayer:nil];
复制代码

一旦获取了layer的本地时间,就可以使用它来更新时间相关的属性:

  • beginTime用于设置动画开始的时间。正常情况下,动画会在下一次更新周期的时候开始执行。我们可以使用beginTime来延迟动画开始的时间。可以用来链接两个动画,在其中一个动画结束的时候开始另外一个 如果延迟了开始时间,则应该将fillMode设置为kCAFillModeBackwards。使用这个模式layer会展示动画的开始的值,不管在layer树中是不是有另外一个不同的值。如果没设置的话,动画会一下子跳到结束的值。(这里应该是理解为开始动画前设置的值吧)

  • autoreverses使动画在指定时长内执行,并且执行完毕之后会回到起始状态。一般会和repeatCount属性一起配套使用

  • timeOffset用于动画组

动画的暂停和继续

要暂停动画的话,可以采用CAMediaTiming协议并将layer的动画速度设置为0.0.

- (void)pauseLayer:(CALayer *)layer {
  CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer: nil];
  layer.speed = 0.0;
  layer.timeOffset = pausedTime;
}

- (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;
}
复制代码

显式事务允许改变动画参数

CATransaction类负责管理动画的创建和群组,并且会在合适的时间开始执行动画。在大多数情况下,你不需要创建自己的事务。Core Animation会在我们给layer添加动画的时候自动为我们创建隐式事务。当然,我们也可以创建。

要开始一个新的事务的话,我们需要调用begin这个类方法,结束则调用commit方法。在两个方法调用之间改变想要改变的参数:

[CATransaction begin];
theLayer.zPosition = 200.0;
theLayer.opacity = 0.0;
[CATransaction commit];
复制代码

除了在事务中我们可以改变时长,时间函数和其他参数意外,我们也可以将一个block赋值给事务,以便在动画组执行完毕之后执行。要改变动画参数的话,我们需要根据相对应属性的key,调用setValue:forKey方法来进行修改:

[CATransaction begin];
[CATransaction setValue:[NSNumber numerWithFloat:10.0f] forKey:kCATransactionAnimationDuration];
[CATransaction commit];
复制代码

我们也能嵌套使用:

[CATransaction begin]; // Outer transaction

[CATransaction setValue:[NSNumber numberWithFloat:2.0f] forKey:kCATransactionAnimationDuration];
theLayer.position = CGPointMake(0.0, 0.0);

[CATransaction begin]; // Inner transaction
[CATransaction setValue:[NSNumber numberWithFloat:5.0f] forKey:kCATransactionAnimationDuration];
theLayer.zPosition = 200.0;
theLayer.opacity = 0.0;

[CATransaction commit];
[CATransaction commit];
复制代码

给动画添加视角

除了平面视角以外,我们也可以在立体的三维视角中实现自己的动画效果。在改变场景的视角时,我们需要修改layer的父layer的sublayerTransform矩阵,来对其所有的sublayer都使用相同的视角。

CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / eyePosition;

myParentLayer.sublayerTransform = perspective;
复制代码

配置完父类layer,就可以改变子layer的zPosition属性来改变子类layer的视角了

改变layer的默认行为

Core Animation通过使用action对象来实现隐式动画效果。Action对象是遵循了CAAction协议并且定义了相关行为的对象。所有的CAAnimation对象都实现了CAAction协议。

自定义Action对象

要创建自己的action对象,需要实现CAAction协议,实现runActionForKey:object:arguments:方法。在这个方法中,使用相关的信息来执行你所想要的动作。

在定义对象的时候,必须制定以哪种方式来触发动作。动作的触发器定义了用于注册动作的key。action对象必须由以下情景进行触发:

  • layer的其中一个属性被修改。
  • layer变得可见或者被添加到layer结构中,表明这种动作的key是kCAOnOrderIn
  • layer被从结构中移除,对应的key是kCAOnOrderOut
  • layer在转换的动画当中,对应的key是kCATransition

Action对象必须安装在layer中

在执行动作之前,layer必须要找到对应的action对象。如果在layer中发生了某个事件,layer会调用actionForKey:来查找key值对应的action对象。

Core Animation根据以下步骤来寻找:

  1. 如果layer有delegate,且该delegate实现了actionForLayer:forKey:方法,layer会调用该方法,delegate必须返回下面任一值:
  • 返回给定key的action对象
  • 如果不处理该动作的话,返回nil
  • 想要停止搜寻,返回NSNull对象
  1. layer会查找layer的actions字典中的每个key
  2. layer会在style字典中查找
  3. 调用defaultActionForKey:
  4. 指定Core Animation定义的隐式动画

如果我们提供了action对象的话,layer会停止它的搜索并且指定返回的action对象。当找到action对象时,layer会调用对象的runActionForKey:object:arguments:方法来执行动作。如果我们定义的动作是CAAnimation类的实例的话,我们可以使用默认方法的实现来执行。如果是遵循CAAction的对象的话,则必须调用我们自己的方法实现。

安装action对象的位置取决于我们修改layer的意图:

  • 对于想要在指定的情况下使用该action,又或者是layer已经有了delegate对象的话,提供delegate并实现actionForLayer:forKey:方法
  • 对于没有delegate的layer对象的话,将action添加到actions属性中
  • 对于已经和其他属性进行关联的action,将action添加到style
  • 对于那些属于layer的基础行为的action,需要继承layer,并重写defaultActionForKey:方法

下面的代码演示了怎样为layer的delegate提供一个action

- (id<CAAction>)actionForLayer:(CALayer *)theLayer forKey:(NSString *)theKey {
  CATransition *theAnimation = nil;  // CATransition 的父类是 CAAnimation
  
  if ([theKey isEqualToString:@"contents"]) {
    theAnimation = [[CATransition alloc] init];
    theAnimation.duration = 1.0; 
    theAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
    theAnimation.type = kCATransitionPush;
    theAnimation.subtype = kCATransitionFromRight;
}
return theAnimation;
复制代码

使用CATransaction类来暂时禁掉操作

[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
[aLayer removeFromSuperLayer];
[CATransaction commit];
复制代码

根据上面的得出的UIView和Layer的关系:

  1. UIView和Layer都有自己的结构层次,有framebounds等属性
  2. 在iOS中,Layer作为UIView的内容备份存在
  3. Layer不能响应用户操作,因为响应操作的类都是属于UIResponder类,所以UIView可以
  4. UIView上实现的动画效果的话,实际上都是在Layer上完成的

转载于:https://juejin.im/post/5a30e2ac6fb9a04525781c6d