CALayer与动画

2020-03-01  本文已影响0人  Arthur澪

要熟练掌握动画操作必须先熟悉CALayer。

当利用drawRect:方法绘图的本质,就是绘制到了UIView的layer属性中。但是在Core Animation中,我们的操作直接面对CALayer。

如何控制动画的暂停?如何进行动画的组合? 这里就需要了解iOS的核心动画Core Animation(包含在Quartz Core框架中)。在iOS中核心动画分为几类:

基础动画、关键帧动画、动画组、转场动画

各个类的关系大致如下:

以上两个不能直接使用

基础动画、关键帧动画都属于属性动画,即:开发人员只需要设置属性的初始值和结束值就有动画效果,中间的过程动画(“补间动画”)由系统自动计算产生。

和基础动画不同的是,关键帧动画可以设置多个属性值,每两个属性中间的补间动画由系统自动完成。因此从这个角度而言,基础动画又可以看成是有2个关键帧的关键帧动画。

图层动画的本质,就是将图层内部的内容,转化为位图经硬件操作形成一种动画效果,其实图层本身并没有任何变化。所以,动画完成后图层就变回初始状态。

基础动画

在开发过程中很多情况下,通过基础动画就可以满足开发需求。如直接使用UIView的类方法animateWithDuration,其内部已经实现了动画代码。如果不使用UIView封装的方法,动画创建一般分为以下几步:

1、通过KeyPath创建动画并设置动画属性

CABasicAnimation *basicAnimation = [CABasicAnimation animationWithKeyPath:@"position"];

KeyPath指定所创建的动画是什么类型,如position是位置变化。KeyPath的值有:

2、设置动画属性初始值(可以省略)、结束值以及其他动画属性
3、给图层添加动画

简单例子,实现移动动画

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor lightGrayColor];
    
    //自定义一个图层
   _layer = [[CALayer alloc]init];
   _layer.bounds = CGRectMake(0, 0, 50, 70);
   _layer.position = CGPointMake(50, 150);
   _layer.contents = (id)[UIImage imageNamed:@"xxx"].CGImage;
    
    [self.view.layer addSublayer:_layer];
}

#pragma mark 点击事件
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    UITouch *touch=touches.anyObject;
    // 获取点击位置
    CGPoint location= [touch locationInView:self.view]
    //创建并开始动画
    [self translatonAnimation:location];
}

#pragma mark 移动动画
-(void)translatonAnimation:(CGPoint)location{
    //1.创建动画并指定动画属性
    CABasicAnimation *basicAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
    //2.设置动画属性初始值和结束值
//    basicAnimation.fromValue = [NSNumber numberWithInteger:50];//可以不设置
    basicAnimation.toValue = [NSValue valueWithCGPoint:location];
     
    //设置其他动画属性
    basicAnimation.duration = 2.0;//动画时间
    //设置重复次数,起到循环动画的效果
    //basicAnimation.repeatCount = 11; 
    //运行一次是否移除动画
    //basicAnimation.removedOnCompletion = NO;
     
    //3.添加动画到图层
    // 注意key相当于给动画进行命名,可使用此名称获取 该动画
    [_layer addAnimation:basicAnimation forKey:@"aaaaab"];
}

#pragma mark 旋转动画
-(void)rotationAnimation{
    //1.创建动画并指定动画属性
    CABasicAnimation *basicAnimation=[CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
     
    //2.设置动画属性初始值、结束值
//    basicAnimation.fromValue=[NSNumber numberWithInt:M_PI_2];
    basicAnimation.toValue=[NSNumber numberWithFloat:M_PI_2*3];
     
    //设置其他动画属性
    basicAnimation.duration=6.0;
    basicAnimation.autoreverses=true;//旋转后再旋转到原来的位置
     
    //4.添加动画到图层,注意key相当于给动画进行命名,以后获得该动画时可以使用此名称获取
    [_layer addAnimation:basicAnimation forKey:@"KCBasicAnimation_Rotation"];
}
//  动画开始时
-(void)animationDidStart:(CAAnimation *)anim{
    
}
// 动画完成时
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
    
}

由于图层动画的本质只是一个显示效果,并没有引起图层实际状态变化;其次对于非根图层,设置图层的可动画属性,会产生动画效果。由此产生一个问题,如:平移动画结束后,重新设置position让图层定在终点位置,而position是可动画属性,于是会重新从起始点运动到终点。即产生了两次动画。

解决方法,关闭图层隐式动画。这需要用到动画事务CATransaction

    //开启事务
    [CATransaction begin];
    //禁用隐式动画
    [CATransaction setDisableActions:YES];
    // 设置图层的位置为终点位置
    _layer.position = [[anim valueForKey:@"KCBasicAnimationLocation"] CGPointValue];
     
    //提交事务
    [CATransaction commit]; 

更好的做法是,首先必须设置fromValue,其次在动画开始前设置动画position为终点位置(当然也必须关闭隐式动画)。

核心动画的运行有一个“媒体时间”的概念。
假设将一个旋转动画设置旋转一周用时60秒,那么当动画旋转90度后媒体时间就是15秒。如果此时要将动画暂停,只需让媒体时间偏移量设置为15秒即可,并把动画运行速度设置为0,使其停止运动。
类似的,如果又过了60秒后需要恢复动画(此时媒体时间为75秒),这时只要将动画开始开始时间设置为:当前媒体时间75秒减去暂停时的时间(也就是之前定格动画时的偏移量)15秒(开始时间=75-15=60秒),那么动画就会重新计算60秒后的状态再开始运行,与此同时将偏移量重新设置为0并且把运行速度设置1。
这个过程中真正起到暂停动画和恢复动画的其实是动画速度的调整,媒体时间偏移量以及恢复时的开始时间设置主要为了让动画更加连贯。

动画暂停针对的是图层而不是图层中的某个动画。
要做无限循环的动画,动画的removedOnCompletion属性必须设置为NO,否则运行一次动画就会销毁。

关键帧动画

关键帧动画就是,在动画控制过程中,开发者指定主要的动画状态,至于各个状态间动画如何进行,则由系统自动运算补充(每两个关键帧之间系统形成的动画称为“补间动画”)。这种动画的好处就是开发者不用逐个控制每个动画帧,而只要关心几个关键帧的状态即可。

关键帧动画开发分为两种形式:一种是通过设置不同的属性值来控制,另一种是通过绘制路径。后者优先级高于前者,如果设置了路径则属性值就不再起作用。

1.通过属性值

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
      
    _layer=[[CALayer alloc]init];
    _layer.bounds=CGRectMake(0, 0, 50, 50);
    _layer.position=CGPointMake(50, 150);
    _layer.anchorPoint=CGPointMake(0.5, 0.6);//设置锚点
    _layer.contents=(id)[UIImage imageNamed:@"xxx"].CGImage;
    [self.view.layer addSublayer:_layer];
    
    [self translationAnimation_values];
}
 
 - (void)translationAnimation_values {
     //1.创建关键帧动画并设置动画属性
     CAKeyframeAnimation* keyFrameAnimation =[CAKeyframeAnimation animationWithKeyPath:@"position"];

     //2.设置关键帧,这里有四个关键帧
     NSValue* key1 = [NSValue valueWithCGPoint:self.layer.position];//必须有初始值
     NSValue* key2 = [NSValue valueWithCGPoint:CGPointMake(80, 220)];
     NSValue* key3 = [NSValue valueWithCGPoint:CGPointMake(45, 320)];
     NSValue* key4 = [NSValue valueWithCGPoint:CGPointMake(75, 420)];
     keyFrameAnimation.values = @[key1,key2,key3,key4];
     //设置其他属性
     keyFrameAnimation.duration = 4;
     //keyFrameAnimation.beginTime = CACurrentMediaTime() + 2;//设置延迟2秒执行
     keyFrameAnimation.keyTimes = @[@(1/4.0),@(1.5/4),@(2.25/4),@1.0];

     //3.添加动画到图层,添加动画后就会行动画
     [self.layer addAnimation:keyFrameAnimation forKey:@"myAnimation"];

 }

效果是,各个点直接的运动是直线。

2.通过绘制路径

- (void)translationAnimation_path {
    //1.创建关键帧动画并设置动画属性
    CAKeyframeAnimation* keyFrameAnimation =[CAKeyframeAnimation animationWithKeyPath:@"position"];

    //2.设置路径  贝塞尔曲线
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathMoveToPoint(path, NULL, self.layer.position.x, self.layer.position.y);
    CGPathAddCurveToPoint(path, NULL, 160, 380, -30, 200, 55, 400);//绘制二次贝塞尔曲线
    keyFrameAnimation.path = path;
    CGPathRelease(path);
 
    keyFrameAnimation.duration = 3.0;
//    keyFrameAnimation.beginTime = CACurrentMediaTime() + 2;//设置延迟2秒执行
 
    //3.添加动画到图层,添加动画后就会行动画
    [self.layer addAnimation:keyFrameAnimation forKey:@"myAnimation"];

}

看起来动画不会那么生硬了,但是这里需要注意,对于路径类型的关键帧动画系统是从描绘路径的位置开始路径,直到路径结束。如果上面的路径不是贝塞尔曲线,而是矩形路径,那么它会从矩形的左上角开始运行,顺时针一周回到左上角;如果指定的路径是一个椭圆,那么动画运行的路径是从椭圆右侧开始(0度)顺时针一周回到右侧。

keyTimes
各个关键帧的时间控制。前面使用values设置了四个关键帧(默认每两帧之间的间隔为:8/(4-1)秒)。通过keyTimes可以设置各帧之间的间隔。keyTimes中存储的是时间占用比例点,如设置keyTimes的值为0.0,0.5,0.75,1.0(必须转换为NSNumber),也就是说1到2帧运行到总时间的50%,2到3帧运行到总时间的75%,3到4帧运行到8秒结束。

caculationMode
动画计算模式。还拿上面keyValues动画举例,之所以1到2帧能形成连贯性动画,是因为动画模式是连续的(值为kCAAnimationLinear,这是默认值);而如果指定了动画模式为kCAAnimationDiscrete离散的,那么你会看到动画从第1帧经过8/3秒直接到第2帧,中间没有任何过渡。
其他动画模式还有:
kCAAnimationPaced(均匀执行,会忽略keyTimes)、
kCAAnimationCubic(平滑执行,对于位置变动关键帧动画运行轨迹更平滑)、
kCAAnimationCubicPaced(平滑均匀执行)。

动画组

实际开发中,单一属性的运动情况比较少,但恰恰属性动画每次进行动画设置时一次只能设置一个属性进行动画控制(基础动画、关键帧动画都是如此),这样一来要做一个复合运动的动画就必须创建多个属性动画进行组合。对于一两种动画的组合或许处理容易,但对于更多动画的组合控制就很麻烦。

动画组 就是基于这样一种情况而产生的。

动画组是一系列动画的组合,凡是添加到动画组中的动画都受控于动画组,这样一来各类动画公共的行为就可以统一进行控制而不必单独设置,而且放到动画组中的各个动画可以并发执行,共同构建出复杂的动画效果。

#pragma mark - 添加基础旋转动画
- (CABasicAnimation*)rotationAnimation {
    CABasicAnimation* basicAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
    CGFloat toValue = M_PI_2*3;

    basicAnimation.toValue = [NSNumber numberWithFloat:toValue];
    basicAnimation.autoreverses = YES;
    basicAnimation.repeatCount = HUGE_VAL;
    basicAnimation.removedOnCompletion = NO;

    [basicAnimation setValue:[NSNumber numberWithFloat:toValue] forKey:@"ZXBaiscAnimationProperty_toValue"];
    return basicAnimation;
} 

#pragma mark - 添加关键帧移动动画
- (CAKeyframeAnimation*)translationAnimation {
    CAKeyframeAnimation* keyframeAnim = [CAKeyframeAnimation animationWithKeyPath:@"position"];
    CGPoint endPoint = CGPointMake(55, 400);
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathMoveToPoint(path, NULL, self.layer.position.x, self.layer.position.y);
    CGPathAddCurveToPoint(path, NULL, 160, 280, -30, 300, endPoint.x, endPoint.y);
    keyframeAnim.path = path;
    CGPathRelease(path);
    [keyframeAnim setValue:[NSValue valueWithCGPoint:endPoint] forKey:@"ZXKeyFrameAnimationProperty_endPosition"];

    return keyframeAnim;
} 


#pragma mark - 创建动画组
- (void)groupAnimation {
    //1.创建动画组
    CAAnimationGroup* animationGroup = [CAAnimationGroup animation];

    //2.设置组中的动画和其他属性
    CABasicAnimation* basicAnimation = [self rotationAnimation];
    CAKeyframeAnimation* keyFrameAnimation = [self translationAnimation];
    animationGroup.animations = @[basicAnimation,keyFrameAnimation];

    animationGroup.delegate = self;
    animationGroup.duration = 10.0;//设置动画时间,如果动画组中动画已经设置过动画属性则不再生效
    animationGroup.beginTime = CACurrentMediaTime() + 5;//延迟5秒执行

    //3.给图层添加动画
    [self.layer addAnimation:animationGroup forKey:nil];
} 


#pragma mark - 代理方法
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
    CAAnimationGroup* animationGroup = (CAAnimationGroup*)anim;
    CABasicAnimation* basicAnimation = (CABasicAnimation*)animationGroup.animations[0];
    CAKeyframeAnimation* keyFrameAnimation = (CAKeyframeAnimation*)animationGroup.animations[1];
    CGFloat toValue = [[basicAnimation valueForKey:@"ZXBaiscAnimationProperty_toValue"] floatValue];
    CGPoint endPoint = [[keyFrameAnimation valueForKey:@"ZXKeyFrameAnimationProperty_endPosition"] CGPointValue];

    [CATransaction begin];
    [CATransaction setDisableActions:YES];
}

上一篇下一篇

猜你喜欢

热点阅读