核心动画(一)

核心动画知识导入

CoreAnimation框架是基于OpenGL ES 手机端/OpenGL PC端(iOS13开始为Metal)与CoreGraphics图像处理框架的一个跨平台的框架。

了解CoreAnimation
核心动画
  • CoreAnimation的封装核心就是去简化OpenGL图形处理,原因是OpenGL的学习成本是非常高的;CoreAnimation也可以用作Mac OS开发
  • Core Graphics 核心绘图
  • Graphics Hardware 图形加速硬件,这个图形硬件就是GPU芯片,GPU专门用来做计算,GPU并不是显卡,而是显卡需要GPU
  • iOS 13之后OpenGL更新为Metal,Metal只针对iOSMac OS系统;而OpenGL ES是可以针对整个嵌入式环境(安卓、黑莓等都可以使用)
GPUCPU的区别

CPU: 中央处理器(英文Central Processing Unit)是一台计算机的运算核心和控制核心;其功能主要是解释计算机指令以及处理计算机软件中的数据
GPU:图形处理器(英文Graphic Processing Unit)是一个专门的图形核心处理器;GPU是显卡的大脑,决定了该显卡的档次和大部分性能,同时也是2D显卡3D显卡的区别依据;2D显示芯片在处理3D图像和特效时主要依赖CPU的处理能力,称为软加速;3D显示芯片是将三维图像和特效处理功能集中在显示芯片内,也即所谓的硬件加速

主要区别如下:

  • CPU需要很强的通用性来处理各种不同的数据类型,同时又要逻辑判断又会引入大量的分支跳转中断的处理,这些都使得CPU的内部结构异常复杂;而GPU面对的则是类型高度统一的相互无依赖的大规模数据和不需要被打断纯净的计算环境
  • GPU采用了数量众多的计算单元超长的流水线,但只有非常简单的控制逻辑并省去了Cache,而CPU不仅被Cache占据了大量空间,而且还有有复杂的控制逻辑和诸多优化电路,相比之下计算能力只是CPU很小的一部分
核心动画的优点
  • 简单易用的高性能混合编程模型
  • 用类似于UIView一样,使⽤图层来创建复杂的编程接口,更加高效的使用
  • 轻量化的数据结构,它可以同时显示让上百个图层产⽣动画效果
  • 一套⾮常简单的动画接口,能让动画运⾏在独立的线程中,并可以独⽴于主线程之外
  • 一旦动画配置完成并启动,核⼼动画就能独立并完全控制相应的动画帧
  • 提⾼应用性能,应⽤程序只有当发生改变的时候才会重绘内容,使用Core Animation 可以不使⽤其他图形API,例如OpenGL来获取高效的动画性能.
  • 灵活的布局管理模型,允许图层相对同级图层的关系来设置属性的位置和⼤小

核心动画图层树结构

CoreAnimation核心动画的结构图
CoreAnimation分类

CAAnimation是所有动画对象的父类(抽象类,虚类),实现CAMediaTiming协议,负责控制动画的时间、速度和时间曲线等等,是一个抽象类。

核心动画类中可以直接使用的类有五个,其中CAAnimationCAPropertyAnimation是抽象类,不能直接使用。

  • CAAnimationGroup: 动画组,可以将很多种动画合并到一起,组成动画效果
  • CATransition: 转场动画效果
  • CAKeyframeAnimation: 关键帧动画效果;values: 一个NSArray对象;里面的元素称为关键帧(keyframe),动画对象会在指定的时间(duration)内,依次显示values数组中的每一个关键帧;简单理解为,很多动画帧执行
  • CABasicAnimation: 基础动画,简单常见的动画效果
  • CASpringAnimation: iOS9.0之后新增的弹簧效果动画,是CABasicAnimation的子类
CALayerUIView的区别
  • CALayer:继承于NSObject,所以不具备响应不能处理用户交互,负责绘制、渲染图形
  • UIView: 继承于UIResponder,所以可以进行事件响应,属性CALayer负责图形绘制与渲染;UIViewCALayer的delegate,可以实现一些简单的CALayer的方法,但要实现稍微复杂些的动画效果,就需要借助CALayer,如:阴影,圆角,带颜色的边框3D变换非矩形范围透明遮罩多级非线性动画等,这也就是开发者为什么要使用CALayer的原因
  • UIView是用来管理CALayerCALayer才是用来展示

疑问:苹果为什么要拆分成CALayerUIView两个类呢?
CoreAnimation是iOS与Mac OS共用的框架,而iOS与Mac OX两者的用户交互方式是不同的,iOS是通过手势触摸,Mac OX是通过键盘鼠标;为了兼容两者,单独把CALayer拆出来只用来绘制渲染图形

CALayerUIView的关系
CALayer与UIView的关系

每一个UIView上面都会有一个CALayer作为它的实例图层属性;我们添加的动画实际上是针对CALayer来做。

CALayer的树级关系一
CALayer的树级关系二

Layer Tree图层树(模型数)主要是设置一些属性

  • 模型树( layer tree):程序中接触最频繁,模型树的对象是模型对象,储存着动画的目标值;当你修改layer的属性时,便是通过模型树上的对象
  • 呈现树(presentation tree):包含正在运行中的动画的动态值,与模型树不同,呈现树始终存储着layer在屏幕当前的状态值,呈现树无法修改,只读;可以通过读取当前值,来做一些其他处理
  • 渲染树(render tree):执行实际的动画,为CoreAnimation私有

小结:动画的三个动作创建执行动画的CALayer创建动画添加动画

CALayer常用属性详解

动画案例准备工作:新建空工程CoreAnimation,在Main.storyboard文件拖入一个UIView,背景色配置成红色并进行关连,命名为redView

动画案例一
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *redView;
@property (nonatomic,strong) CALayer *layer;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 新建layer,并添加到self.view.layer上
    CALayer *layer = [CALayer layer];
    layer.frame = CGRectMake(100, 100, 100, 100);
    layer.backgroundColor = [UIColor greenColor].CGColor;
    _layer = layer;
    [self.view.layer addSublayer:layer];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    CABasicAnimation *animation = [CABasicAnimation animation];
    // 修改动画y的位置到600
    animation.keyPath = @"position.y";
    animation.toValue = @600;
    animation.duration = 1;
    [_redView.layer addAnimation:animation forKey:nil];
}

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

@end

运行工程并点击屏幕,我们会发现动画执行完毕后,又回到了初始位置,为什么会这样?
其实在执行动画的过程中会有两个图层:layer层presentation层,真正移动的是presentation层;动画开始时会先把layer层隐藏,让presentation层做动画;动画结束后presentation层就会移除,layer层再出现;原因是视图的layer层根本没有发生变化,动画结束就会恢复到原来的状态。

解决办法:设置animation的两个属性

//解决动画恢复到初始位置
//当动画完成后,不把presentation层从render树中移除(默认是移除的)
animation.removedOnCompletion = NO;
//当动画结束后,把layer层状态同步到presentation层;此时_redView的frame才会发生变化
animation.fillMode = kCAFillModeForwards;

CABasicAnimation相当于是一个数据模型,把该数据模型绑定到layer上面。

CABasicAnimation动画的fillMode属性介绍
  • kCAFillModeForwards:动画结束后,layer会一直保持动画最后的状态
  • kCAFillModeBackwards:动画开始前,只要将动画加入一个layerlayer便立即进入动画的初始状态并等待动画开始
  • kCAFillModeBothkCAFillModeForwardskCAFillModeBackwards两者的结合,开始前保持动画初始状态,结束后保持动画的最后状态
  • kCAFillModeRemoved:默认属性
动画案例二:隐式动画

疑问:上面红色图层动画结束恢复到原来的状态,恢复的过程给人的感觉是回弹动画,这就是隐式动画?

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    _layer.backgroundColor = [UIColor orangeColor].CGColor;
}

我们没有给_layer层添加动画,但是点击页面_layer层由绿色变成橙色的过程,有一种动画的效果,这就是隐式动画;隐式动画是由CoreAnimation框架帮我们做的,其默认动画时长是0.25秒,通过runloop来执行。上面添加的CABasicAnimation属于显式动画

动画案例三:修改隐式动画

如何修改系统的隐式动画呢?

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    //begin a new transaction
    [CATransaction begin];
    //设置隐式动画的时长
    [CATransaction setAnimationDuration:2.0];
    _layer.backgroundColor = [UIColor orangeColor].CGColor;
    //动画执行完成的回调
    [CATransaction setCompletionBlock:^{
        //添加转场动画
        //rotate the layer 90 degrees
        CGAffineTransform transform = self.layer.affineTransform;
        transform = CGAffineTransformRotate(transform, M_PI_2);
        self.layer.affineTransform = transform;

    }];
    [CATransaction commit];
}

CATransaction类没有属性,也没有实例方法;不能通过alloc init去创建,只能通过begincommit压栈出栈的方式来管理。

动画案例四:CALayer常用属性 - Contents
- (void)viewDidLoad {
    [super viewDidLoad];
    //除了UIImageView 能够显示图片,layer也可以加载图片
    UIImage *image = [UIImage imageNamed:@"test.png"];
    //不用CGImage的话,图片加载不出来
    self.view.layer.contents = (__bridge id)(image.CGImage);
    //填充方式
    self.view.contentMode = UIViewContentModeScaleAspectFit;
    self.view.layer.contentsGravity = kCAGravityResizeAspect;
}

Contents属性是id类型,原因是在Mac OS系统上Contents属性对CGImageNSImage都会起作用,image.CGImage实际上赋值的是CGImageRef类型,CGImageRef指向的是CGImage结构,需要进行桥接处理,开发中如果需要设置背景图可以使用layerContents是id类型就可以直接在layercontents上面加载一张图片。

  • CALayer常用属性 - contentsScale
self.view.layer.contentsScale = [[UIScreen mainScreen] scale];

当用代码设置contents图片时,要⼿动设置图层的contentsScale属性,避免Retina屏幕显示错误。

  • CALayer常用属性 - makeToBounds
    makeToBounds属性类似于UIView中的clipsToBounds属性,含义:是否显示超出边界的内容?

  • CALayer常用属性 - contentsRect

  1. contentsRect不是按点来计算的,而是按照单位坐标
  2. OpenGL的坐标系横向是从-1到1,纵向从1到-1的过程,center坐标就是{0, 0},而手机都是长方形的,所以OpenGL会把单位坐标系转换为设备坐标系,不同设备有不同的坐标;
点- 像素 - 单位区分
contentsRect属性

contentsRectcontentsGravity属性要灵活很多,contentsGravity属性只能展示图片固定的位置与大小,而contentsRect可以展示图片的任意内容(只要把单位坐标计算好即可);如果contentsGravity不能满足我们的需求时,可以使用contentsRect属性。

CALayer中HitTest属性的实际使用

下面我们来了解一下UIViewCALayer图层几何

图层几何

frame是相对于父视图的坐标,bounds是从视图自身出发即内部坐标centerposition相当于父视图上面的一个锚点

锚点

锚点就是视图中的center属性position属性,实际上就是一个坐标,锚点anchorPointlayer的属性(即position)。

  • CALayer常用属性 - ZPosition
ZPosition

手机开发是基于二维平面的,并不是一个优秀的三维图形显示载体,但是手机中会出现一些立体的粒子效果,于是CALayer就提供了一个属性ZPosition,也就意味着CALayer是三维的;OpenGLMetal默认是一个3D图形api,默认坐标系是三维坐标系,在描述平面图形的时候,z坐标是0;核心动画把z坐标单独摘出来就是ZPosition;呈现粒子的时候就必须要用到ZPosition

动画案例准备工作:新建空工程CoreAnimation2,在Main.storyboard文件拖入两个UIView,背景色配置成橙色红色并进行关连,命名为view1view2,两个view的层级关系如下

两个view的层级
// ViewController.h文件
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view1.layer.zPosition = 1.0;
}

运行工程,我们发现橙色view出现在了红色view上面,原因是什么呢?
在平面图形上面z轴默认值是0,这里把橙色viewz轴值设置为1,就意味着把橙色view放在了上面;修改了zPosition属性的值,就更改了深度缓冲区-深度测试,这里主要与深度缓冲区有关,本质并不是修改图层的层级关系

  • CALayer常用属性 -Hit Testing

动画案例准备工作:新建空工程CoreAnimation3,在Main.storyboard文件拖入一个UIView,背景色配置成红色并进行关连,命名为layerView,如下图所示

CoreAnimation3工程
#import "ViewController.h"

@interface ViewController ()
@property (strong, nonatomic) UIView *layerView;
@property (strong,nonatomic) CALayer *blueLayer;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //1.Create sublayer
    self.blueLayer = [CALayer layer];
    self.blueLayer.frame = CGRectMake(0, 0, 100, 100);
    self.blueLayer.backgroundColor = [UIColor blueColor].CGColor;
    [self.layerView.layer addSublayer:self.blueLayer];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    //1.get touchu position relative to main view(获取相对于主视图的触摸位置)
    CGPoint point = [[touches anyObject]locationInView:self.view];
    //2.get touched layer
    CALayer *layer = [self.layerView.layer hitTest:point];
    //3.get layer using using hitTest
    if(layer == self.blueLayer)
    {           
        NSLog(@"Inside Blue layer");
    } else if (layer == self.layerView.layer) {
        NSLog(@"Inside Red layer");
    }
}

@end

// 运行工程,查看打印日志,成功获取点击的layer层
2022-09-23 22:25:56.044062+0800 CoreAnimation3[22331:14483795] Inside Blue layer
2022-09-23 22:25:59.342807+0800 CoreAnimation3[22331:14483795] Inside Red layer

CALayer不能响应事件,但是CALayerHit Testing属性能够获取到点击的图层。

hitTest方法介绍
// point : 在接收器的局部坐标系(界)中指定的点
// event : 系统保证调用此方法的事件。如果从事件处理代码外部调用此方法,则可以指定nil
// returnValue : 视图对象是当前视图和包含点的最远的后代。
//               如果点完全位于接收方的视图层次结构之外,则返回nil
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event

不止CALayer中有hitTest方法,UIView中同样有hitTest方法;UIView中该方法的作用:在视图的层次结构中寻找一个最合适的view来响应触摸事件;该方法会被系统调用,调用时如果返回nil,即事件被丢弃,否则返回最合适的view来响应事件。

  • Hit Test调用顺序
touch -> UIApplication -> UIWindow -> UIViewController.view -> subViews -> ....-> 合适的view
  • 事件的传递顺序,与Hit Test调用顺序刚好相反
 view -> superView ...- > UIViewController.view -> UIViewController ->UIWindow -> UIApplication -> 丢弃事件

说明:

  1. 首先由view来尝试处理事件,如果处理不了,事件将被传递到父视图superView
  2. superView也尝试处理事件,如果处理不了,继续传递给它的父视图UIViewController.view
  3. UIViewController.view尝试处理事件,如果处理不了,把该事件传递给UIViewController
  4. UIViewController尝试处理事件,如果处理不了,把事件传递给UIWindow
  5. 主窗口UIWindow尝试来处理事件, 如果处理不了,将传递给应用单例UIApplication
  6. 如果UIApplication也处理不了,该事件将被丢弃
UIViewHit Test底层实现思路

常见的hitTest不实现的四种情况(即view不响应事件情况)

  • view.userInteractionEnabled = NO;
  • view.hidden = YES;
  • view.alpha < 0.05;
  • view 超出 superview 的 bounds;
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    //1.是否响应事件的必然性条件
    if (self.userInteractionEnabled == NO || self.alpha < 0.05 || self.hidden == YES)
    {
        return nil;
    }
    
    //2.touch的point在self.bounds内
    if ([self pointInside:point withEvent:event])
    {
        for (UIView *subView in self.subviews)
        {
            //进行坐标转化
            CGPoint coverPoint = [subView convertPoint:point fromView:self];
            // 调用子视图的 hitTest 重复上面的步骤。找到了,返回hitTestview ,没找到返回有自身处理
            UIView *hitTestView = [subView hitTest:coverPoint withEvent:event];
            if (hitTestView)
            {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}

Hit Testing应⽤场景-⼦视图超出⽗视图范围,点击没有响应;如果需求是让超出父视图的范围也能点击,代码实现如下

⼦视图超出⽗视图范围
实现思路

仿射变换数学原理讲解

图层变换
  • 刚体变换:只有物体的位置(平移变换)和朝向(旋转变换)发生改变,而形状不变;刚性变换是最一般的变换。
  • 仿射变换:仿射变换具有两个旋转因子两个缩放因子,因此具有6个自由度。不具有保角性和保持距离比的性质,但是原图平行线变换后仍然是平行线。仿射变换主要包括平移变换旋转变换缩放变换(也叫尺度变换)、倾斜变换(也叫错切变换、剪切变换、偏移变换)、翻转变换,有六个自由度。
  • 投影变换:是最一般的线性变换,有8个自由度;射影变换保持重合关系和交比不变。但不会保持平行性。即它会使得仿射变换产生非线性效应
仿射变换
投影的两种类型

对于复杂的立体图形,我们要想平移是非常困难的;但是通过仿射变换,我们就可以很容易的实现,可以对立体图形的任意顶点进行平移,实现代码如下图所示:

对立体图形进行平移

核心动画官方文档地址

下面我们介绍官方文档中的几个矩阵
矩阵

上图展现了常见的transformations的矩阵配置;任何乘以identity矩阵的coordinate将不会变化,当乘以其他矩阵时,coordinate的变化和矩阵每个分量都有关;例如,沿着X轴平移,我们需要提供非零的 tx 分量并让tytz为0;对于旋转操作,我们应该提供合适的 sinecosine 值。

  • Identity:单元矩阵
  • Translate:平移
  • Scale:缩放
  • Rotate around X axis:围绕X轴旋转,X值不变;这里的四维原因是OpenGL ES/Metal中描述顶点除了围绕XYZ轴,还有一个W缩放因子。
  • Rotate around Y axis:围绕Y轴旋转,Y值不变
  • Rotate around Z axis:围绕Z轴旋转,Z值不变
围绕任意轴旋转

围绕任意轴旋转中参数n表示向量(x,y,z),第二个参数表示角度

你可能感兴趣的:(核心动画(一))