iOS - 核心动画初探

作者: Lcr111 | 来源:发表于2022-11-17 16:35 被阅读0次

前言

最近项目需要用到核心动画来解决几个小图标的简单动画,所以抽了点时间对iOS核心动画Core Animation 做了一个简单的探索和实践。本文章记录一下学习过程中的重要信息以及简单使用方法。

一、基础理论

基本概念

核心动画(Core Animation)基本概念可总结一下几点:

  • 简单易⽤的⾼性能混合编程模型
  • ⽤类似于视图⼀样,使⽤图层来创建复杂的编程接⼝
  • 轻量化的数据结构,它可以同时显示让上百个图层产⽣动画效果
  • ⼀套⾮常较简单的动画接⼝,能让动画运⾏在独⽴的线程中,并可以独⽴于主线程之外
  • ⼀旦动画配置完成并启动,核⼼动画就能独⽴并完全控制相应的动画帧.
  • 提⾼应⽤性能.应⽤程序只有当发⽣改变的时候才会重绘内容. 使⽤Core Animation 可以不使⽤其他图形API,例如OpenGL 来获取⾼效的动画性能
  • 灵活的布局管理模型,允许图层相对同级图层的关系来设置属性的位置和⼤⼩

简单说,核心动画就是一组非常强大的动画处理API,要注意的是,Core Animation是直接作用在CALayer上的,并非UIViewUIView与CALayer的关系
层级结构如下图所示:

核心动画001
结构上:核心动画是基于OpenGL(iOS13开始为metal)与CoreGraphics图像处理框架的一个跨平台的框架,在CoreAnimation中大部分的动画都是通过Layer层来实现的,通过CALayer,我们可以组织复杂的层级结构。

相关类的组织架构

那么核心动画下又有哪些类呢?我们在使用的时候又是用的哪些类来完成我们的需求呢?


核心动画002
  • CAAnimation是所有动画对象的父类(抽象类,虚类),继承自NSOject基类,实现CAMediaTiming协议,负责控制动画的时间、速度和时间曲线等等,是一个抽象类,不能直接使用。
  • CATransition又称转场动画,是CAAnimation的子类,可以直接使用,主要用于为图层提供移入/移出屏幕的动画效果,常见的应用是UINavigationController在控制器之间切换动画。
  • CAPropertyAnimation是一个抽象类,不可直接使用。不能直接用于实现CALayer动画操作,但是它的类定义中增加用于设置CALayer可被实现动画的属性keyPath。
  • CABasicAnimation基础动画,在指定可动画属性后,动画会按照预定的参数持续一定时间由初始值变换为终点值。相对于下方的CAKeyframeAnimation来说,CABasicAnimation只记录两个帧变化,一个开始帧,一个结束帧。
  • CASpringAnimation是可实现弹簧效果的动画实现类,属于CABasicAnimation子类,可直接使用。
  • CAKeyframeAnimation是可以在layer层按照一组关键帧执行的动画。通俗将是高级一点的基础动画,可将一组想管理的帧放入数组,分别设置时间范围动画范围来实现更为丰富的动画效果。
  • CAAnimationGroup是可以将一组动画效果结合起来,应用于一个layer上,实现多种动画效果同时进行,解决一些复杂场景的动画效果。

既然CoreAnimation 中大部分动画都是作用于layer层的,那么layer层又有哪些layer呢?

核心动画003
CALayer 作为众多的layer的父类,下面的众多layer基本满足了基本动画效果的需求。CALayer自身也可直接使用在一些基本的展示需求上。

隐式动画(CATransaction)/显式动画

    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<UITouch *> *)touches withEvent:(UIEvent *)event
{
    _layer.backgroundColor = [UIColor orangeColor].CGColor;
}

运行上面代码,我们发现在点击屏幕时,layer的背景颜色不是立刻变成目标颜色,而是有一个渐变的动画效果,这就是我们常说的隐式动画,之所以叫隐式是因为我们只是修改了一个属性的值,没有指定任何动画,更没有定义动画如何执行,但是系统自动产生了动画.如果是改变position的话,效果更为明显。
CATransactionCore Animation 中的事务类,在 iOS 的图层中、图层的每个改变都是事务的一部分,CATransaction 可以对多个 layer 的属性同时进行修改,同时负责批量的把多个图层树的修改作为一个原子更新到渲染树。隐式事务是基于 CALayer的,任何对于 CALayer 属性的修改都是隐式事务、这样的事务会在 runloop 中被提交。
CATransaction 事务类分为隐式事务和显式事务。
那么相对的显示动画是怎样的呢?
CAAnimation就是显式动画了,UIView禁止了隐式动画,但是CAAnimation可以为UIView的layer添加显式动画,显式动画并没有修改属性的值,只是执行动画而已,因此还需要主动修改属性.
当然直观点我们还是调用 CATransaction 的 begin 和 commit 方法来实现显示事务。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //begin a new transaction
    [CATransaction begin];
    //set the animation duration to 1 second
    [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;

    }];
    //commit the transaction
    [CATransaction commit];
}

二、使用案例

下面我将演示部分在学习中好的例子代码,方便学习。

CATransform3D

CATransform3D 的数据结构定义了一个同质的三维变换(4x4 CGFloat值的矩阵),用于图层的旋转缩放偏移歪斜和应用的透视
图层的2个属性指定了变换矩阵:transformsublayerTransform

  • transform : 是结合 anchorPoint(锚点)的位置来对图层和图层上的子图层进行变化。
  • sublayerTransform:是结合anchorPoint(锚点)的位置来对图层的子图层进行变化,不包括本身。

CATransform3DIdentity 是单位矩阵,该矩阵没有缩放,旋转,歪斜,透视。该矩阵应用到图层上,就是设置默认值。
CATransform3D 又是一个结构。他有自己的一个公式,可以进行套用。

struct CATransform3D
{
CGFloat m11(x缩放), m12(y切变), m13(旋转), m14();
CGFloat m21(x切变), m22(y缩放), m23() , m24();
CGFloat m31(旋转) , m32( ) , m33() , m34(透视效果,要操作的这个对象要有旋转的角度,否则没有效果。正直/负值都有意义);
CGFloat m41(x平移), m42(y平移), m43(z平移) , m44();
};

CATransform3DEqualToTransform(CATransform3D a, CATransform3D b):判断两个CATransform3D结构体对象是否相同。
CATransform3DMakeTranslation (CGFloat tx, CGFloat ty, CGFloat tz):分别为对应轴上的平移大小,z轴的话,越小越往屏幕里面走,越大越往屏幕出来。返回一个CATransform3D类型的结构体对象。
CATransform3D CATransform3DTranslate (CATransform3D t, CGFloat tx, CGFloat ty, CGFloat tz);:可理解为在t的变化基础上叠加一个平移效果。
CATransform3DMakeScale (CGFloat sx, CGFloat sy, CGFloat sz):物体所有坐标分别在对应轴上面的放大缩小多少。
CATransform3DScale (CATransform3D t, CGFloat sx, CGFloat sy, CGFloat sz):同上,也是一个在原油变化t基础上叠加的一个放大缩小效果。
CATransform3DMakeRotation (CGFloat angle, CGFloat x, CGFloat y, CGFloat z):对物体在对应轴上的旋转角度做相应的设置。
CATransform3DRotate (CATransform3D t, CGFloat angle, CGFloat x, CGFloat y, CGFloat z):同上,也是一种叠加效果。

使用例子如下:

    CATransform3D tranform = CATransform3DIdentity;
    tranform.m34 = -1.0f/500.0;
    tranform = CATransform3DRotate(tranform, M_PI_4, 0, 1, 0);
    self.layerView.layer.transform = tranform;
    self.layerView.layer.doubleSided = NO;

    CATransform3D perspective = CATransform3DIdentity;
    perspective.m34 = -1.0/500.0;
    self.containerView.layer.sublayerTransform = perspective;

    CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
    CATransform3D transform2 = CATransform3DMakeRotation(M_PI_4, 0, -1, 0);
   // CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 1, 0);

    self.layerView1.layer.transform = transform1;
    self.layerView2.layer.transform = transform2;
核心动画004
值得注意的是,tranform.m34 = -1.0f/500.0;这句代码,意思是设置投影矩阵。这样子layer在父layer里面观察起来才有立体感。

CAEmitterLayer 粒子动画

CAEmitterLayer 发射的粒子并不是杂乱无章的,我们可以设置它发射粒子时的位置、几何图形等,通过以下属性去配置:

  • emitterPosition:决定发射源的中心点,比如CGPointMake(self.view.bounds.size.width * 0.5, -10)屏幕宽的一半,就是屏幕顶部中间,往上方10个单位发射粒子。
  • emitterSize:决定发射源的大小。
  • emitterShape:它表示粒子从什么形状发射出来,它并不是表示粒子自己的形状,是一个枚举类型。
    1. kCAEmitterLayerPoint:点形状,发射源的形状就是一个点,位置在emitterPosition所在的rect中心点。
    2. kCAEmitterLayerLine:线形状,发射源的形状是一条线,如下所示:
      核心动画005
    3. kCAEmitterLayerRectangle:矩形状,发射源的形状是一个矩形,就是上面那个emitterPosition坐在的矩形。
    4. kCAEmitterLayerCuboid:立体矩形形状(3D),发射源是一个立体矩形。
    5. kCAEmitterLayerCircle:圆形形状,发射源是一个圆形,形状为矩形包裹的那个原,二维的。
    6. kCAEmitterLayerSphere:立体圆形(3D),三维的图形,同样需要设置z方向数据,不设置同上二维一样。
  • emitterMode:发射模式,这个字段是决定发射出来的粒子是以什么具体形式发射出来的。
    1. kCAEmitterLayerPoints:点模式,发射器是以点的形式发射粒子。比如圆心,球心,一个按钮中心点等往四周发射粒子。
    2. kCAEmitterLayerOutline:轮廓模式,从形状的边界发射粒子。
    3. kCAEmitterLayerSurface:表面模式,从形状的表面上发射粒子。
    4. kCAEmitterLayerVolume:3D发射效果,比如从一个球心往外发射粒子。结合CAEmitterCell的属性emissionLongitudemissionLatitude以及emissionRange`控制粒子从这样一个弧度范围发射出来。
      CAEmitterLayer其余属性就不一一介绍了,CAEmitterCell的属性官方文档也不难理解。
      新创建一个自定义按钮LCRButton 继承自UIButton:
- (void)awakeFromNib{
    [super awakeFromNib];
    //设置粒子效果
    [self setupExplosion];
}
- (instancetype)initWithFrame:(CGRect)frame{
    self = [super initWithFrame:frame];
    if (self) {
        [self setupExplosion];
    }
    return self;
}
//设置粒子
- (void)setupExplosion{
    // 1. 粒子
    CAEmitterCell * explosionCell = [CAEmitterCell emitterCell];
    explosionCell.name = @"explosionCell";
    //透明值变化速度
    explosionCell.alphaSpeed = -1.f;
    //alphaRange透明值范围
    explosionCell.alphaRange = 0.10;
    //生命周期
    explosionCell.lifetime = 1;
    //生命周期range
    explosionCell.lifetimeRange = 0.1;
    //粒子速度
    explosionCell.velocity = 40.f;
    //粒子速度范围
    explosionCell.velocityRange = 10.f;
    //缩放比例
    explosionCell.scale = 0.08;
    //缩放比例range
    explosionCell.scaleRange = 0.02;
    //粒子图片
    explosionCell.contents = (id)[[UIImage imageNamed:@"spark_red"] CGImage];
    // 2.发射源
    CAEmitterLayer * explosionLayer = [CAEmitterLayer layer];
    [self.layer addSublayer:explosionLayer];
    self.explosionLayer = explosionLayer;
    //发射院尺寸大小
    self.explosionLayer.emitterSize = CGSizeMake(self.bounds.size.width + 40, self.bounds.size.height + 40);
    //emitterShape表示粒子从什么形状发射出来,圆形形状
    explosionLayer.emitterShape = kCAEmitterLayerCircle;
    //emitterMode发射模型,轮廓模式,从形状的边界上发射粒子
    explosionLayer.emitterMode = kCAEmitterLayerOutline;
    //renderMode:渲染模式
    explosionLayer.renderMode = kCAEmitterLayerOldestFirst;
    //粒子cell 数组
    explosionLayer.emitterCells = @[explosionCell];
}
-(void)layoutSubviews{
    // 发射源位置
    self.explosionLayer.position = CGPointMake(self.bounds.size.width * 0.5, self.bounds.size.height * 0.5);
    [super layoutSubviews];
}
/**
 * 选中状态 实现缩放
 */
- (void)setSelected:(BOOL)selected{
    [super setSelected:selected];
    // 通过关键帧动画实现缩放
    CAKeyframeAnimation * animation = [CAKeyframeAnimation animation];
    // 设置动画路径
    animation.keyPath = @"transform.scale";
    if (selected) {
        // 从没有点击到点击状态 会有爆炸的动画效果
        animation.values = @[@1.5,@2.0, @0.8, @1.0];
        animation.duration = 0.5;
        //计算关键帧方式:
        animation.calculationMode = kCAAnimationCubic;
        //为图层添加动画
        [self.layer addAnimation:animation forKey:nil];
        // 让放大动画先执行完毕 再执行爆炸动画
        [self performSelector:@selector(startAnimation) withObject:nil afterDelay:0.25];
    }else{
        // 从点击状态normal状态 无动画效果 如果点赞之后马上取消 那么也立马停止动画
        [self stopAnimation];
    }
}
// 没有高亮状态
- (void)setHighlighted:(BOOL)highlighted{
    [super setHighlighted:highlighted];
}
/**
 * 开始动画
 */
- (void)startAnimation{
    // 用KVC设置颗粒个数
    [self.explosionLayer setValue:@1000 forKeyPath:@"emitterCells.explosionCell.birthRate"];
    // 开始动画
    self.explosionLayer.beginTime = CACurrentMediaTime();
    // 延迟停止动画
    [self performSelector:@selector(stopAnimation) withObject:nil afterDelay:0.15];
}
/**
 * 动画结束
 */
- (void)stopAnimation{
    // 用KVC设置颗粒个数
    [self.explosionLayer setValue:@0 forKeyPath:@"emitterCells.explosionCell.birthRate"];
    //移除动画
    [self.explosionLayer removeAllAnimations];
}
- (void)drawRect:(CGRect)rect {
}
核心动画006

可见,上图需求类似于QQ点赞动画效果。

CABasicAnimation

基础动画,实现基本的两个帧之前的动画效果:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.rootLayer = [CALayer layer];
    // 应用透视转换
    CATransform3D transform = CATransform3DMakePerspective(1000);
    self.rootLayer.sublayerTransform = transform;
    self.rootLayer.frame = self.view.bounds;
    [self.view.layer addSublayer:self.rootLayer];
    
    //颜色数组
    NSArray *colors = @[[UIColor colorWithRed:0.263 green:0.769 blue:0.319 alpha:1.000], [UIColor colorWithRed:0.990 green:0.759 blue:0.145 alpha:1.000], [UIColor colorWithRed:0.084 green:0.398 blue:0.979 alpha:1.000]];
    
    //添加3个图层
    [self addLayersWithColors:colors];
    
    [self performSelector:@selector(rotateLayers) withObject:nil afterDelay:1.0];
}

- (void)addLayersWithColors:(NSArray *)colors {
    //遍历颜色数组,创建图层
    for (UIColor *color in colors) {
        //创建图层
        CALayer *layer = [CALayer layer];
        //颜色
        layer.backgroundColor = color.CGColor;
        //大小
        layer.bounds = CGRectMake(0, 0, 200, 200);
        //位置
        layer.position = CGPointMake(160, 190);
        //透明度
        layer.opacity = 0.80;
        //圆角
        layer.cornerRadius = 10;
        //边框颜色
        layer.borderColor = [UIColor whiteColor].CGColor;
        //边框宽度
        layer.borderWidth = 1.0;
        //阴影offset ,默认(0,3)
        layer.shadowOffset = CGSizeMake(0, 2);
        //用于创建阴影的模糊半径。默认值为3。可动画的
        layer.shadowOpacity = 0.35;
        //阴影颜色
        layer.shadowColor = [UIColor darkGrayColor].CGColor;
        //是否光栅化
        layer.shouldRasterize = YES;
        //添加图层
        [self.rootLayer addSublayer:layer];
    }
}
- (void)rotateLayers {
    
    //创建基本动画以围绕Y轴和Z轴旋转
    CABasicAnimation *transformAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
    transformAnimation.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
    transformAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(DEGREES_TO_RADIANS(85), 0, 1, 1)];
    transformAnimation.duration = 1.5;
    //自动翻转
    transformAnimation.autoreverses = YES;
    //重复次数
    transformAnimation.repeatCount = HUGE_VALF;
    
    //定义动画步调的计时函数
    transformAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    
    int tx = 0;
    // 循环浏览子图层并附加动画
    for (CALayer *layer in [self.rootLayer sublayers]) {
        //为图层添加动画
        [layer addAnimation:transformAnimation forKey:nil];
        
        // 创建要沿X轴平移的动画
        CABasicAnimation *translateAnimation = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
        translateAnimation.fromValue = [NSValue valueWithCATransform3D:layer.transform];
        translateAnimation.toValue = [NSNumber numberWithFloat:tx];
        translateAnimation.duration = 1.5;
        translateAnimation.autoreverses = YES;
        translateAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
        translateAnimation.repeatCount = HUGE_VALF;
        [layer addAnimation:translateAnimation forKey:nil];
        tx += 35;
    }
}
核心动画007

可见,CABasicAnimation 中的fromValue和toValue控制着动画具体从什么状态走到什么状态。那如果需要动画效果丰富一些呢?我们就需要用到CAKeyframeAnimation来实现多帧之间变化的动画效果了。

CAKeyframeAnimation

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    CAKeyframeAnimation *anim = [CAKeyframeAnimation animation];
    anim.keyPath = @"transform.rotation";
    anim.values = @[@angleToRadians(-3),
                     @angleToRadians(5),
                     @angleToRadians(-3),
                     ];
    
    anim.autoreverses = YES;
    anim.speed = 2;
    anim.duration = 1;
    anim.repeatCount = MAXFLOAT;
    [_pigView.layer addAnimation:anim forKey:nil];
}
核心动画008

CAKeyframeAnimation除了可以设置按帧变化的模式,还可以设置path属性,可根据UIBezierPath对象的CGPath属性为轨迹完成动画效果。

//1.定义贝塞尔曲线
    UIBezierPath *path = [UIBezierPath bezierPath];
    //使用方法moveToPoint:去设置初始线段的起点
    [path moveToPoint:CGPointMake(20, 200)];
    //设置EndPoint & Control Point
    [path addCurveToPoint:CGPointMake(300, 200) controlPoint1:CGPointMake(100, 100) controlPoint2:CGPointMake(200, 300)];
    
    //CAShapeLayer 使用shapeLayer 可以更高效的渲染图形.并且不使用drawRect方法
    CAShapeLayer *shapeLyaer = [CAShapeLayer layer];
    //路径
    shapeLyaer.path = path.CGPath;
    //填充颜色
    //shapeLyaer.fillColor = [UIColor blueColor].CGColor;
    shapeLyaer.fillColor = nil;
    shapeLyaer.strokeColor = [UIColor redColor].CGColor;
    //为子图层添加贝塞尔曲线图
    [self.view.layer addSublayer:shapeLyaer];
    
    //添加🚗图层
    CALayer *carLayer = [CALayer layer];
    carLayer.frame = CGRectMake(15, 200-18, 36, 36);
    carLayer.contents = (id)[UIImage imageNamed:@"car"].CGImage;
    carLayer.anchorPoint = CGPointMake(0.5, 0.8);
    [self.view.layer addSublayer:carLayer];
    
    //创建关键帧动画
    CAKeyframeAnimation *anim = [CAKeyframeAnimation animation];
    //路径
    anim.keyPath = @"position";
    //path
    anim.path = path.CGPath;
    //时长
    anim.duration = 4.0;
    //rotationMode
    anim.rotationMode = kCAAnimationRotateAuto;
    //为汽车图层添加动画
    [carLayer addAnimation:anim forKey:nil];
核心动画009

CAAnimationGroup 动画组

动画组可实现layer的一组动画整体效果,或者时间上实现连续性的动画效果(一个动画接着前面一个动画)。

// 数字跳动
- (void)labelDanceAnimation:(NSTimeInterval)duration {
    //透明度
    CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
    opacityAnimation.duration = 0.4 * duration;
    opacityAnimation.fromValue = @0.f;
    opacityAnimation.toValue = @1.f;

    //缩放
    CAKeyframeAnimation *scaleAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
    scaleAnimation.duration = duration;
    //给values属性给了一个数组。这什么意思呢?CABasicAnimation是指定两个状态,而我们的CAKeyframeAnimation则是指定多个状态,动画放大倍数也的确按照我的规划放大缩小了.
    scaleAnimation.values = @[@3.f, @1.f, @1.2f, @1.f];
    //keyTimes属性指定的是当前状态节点到初始状态节点的时间占动画总时长的比例。若果不设置keyTimes则匀速播放
    //keyTimes对应了values的变化.
    scaleAnimation.keyTimes = @[@0.f, @0.16f, @0.28f, @0.4f];
    //是否在播放完成后移除
    scaleAnimation.removedOnCompletion = YES;
    //播放结束后的状态--保持结束时状态
    scaleAnimation.fillMode = kCAFillModeForwards;
    //动画组
    CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
    //添加动画(opacityAnimation透明度动画,scaleAnimation缩放动画)
    animationGroup.animations = @[opacityAnimation, scaleAnimation];
    //动画时长
    animationGroup.duration = duration;
    //是否在播放结束后移除
    animationGroup.removedOnCompletion = YES;
    //播放结束后的状态--保持结束时的状态
    animationGroup.fillMode = kCAFillModeForwards;
    //在字体label图层添加动画组.
    [self.numberLabel.layer addAnimation:animationGroup forKey:nil];
}
- (IBAction)clickAction:(id)sender {
    //danceCount累积
    self.danceCount++;
    //添加字体动画,动画时长:0.4
    [self labelDanceAnimation:0.4];
    //修改label上的数字
    self.numberLabel.text = [NSString stringWithFormat:@"+  %tu",self.danceCount];
}
核心动画010

例子可见,数字的增加同时带有缩放及透明度变化的效果,将两个动画效果天假到动画组中,运用于layer。

总结

以上是本人在学习核心动画过程中所涉及到的一部分例子,篇幅有限,可能有些更重要的知识点没讲到,旺评论区指出,一起学习,一起进步。

相关文章

网友评论

    本文标题:iOS - 核心动画初探

    本文链接:https://www.haomeiwen.com/subject/fioyxdtx.html