CoreAnimatin-高级动画技巧

我们可以用属性或者keyframe做很多事情。但是有时候我们需要一起或者按照顺序执行多个动画,我们可以使用更高级的方式来同步这些动画的时序来将他们链接在一起。我们还可以用其他类型的动画对象来创建视觉过渡和其他有趣的动画效果

过渡(Transition)动画支持对layer可视的改变

顾名思义,过渡动画对象可以为layer创建一个过动画过渡视觉。过渡动画最常见的用途是以协调的方式设置layer的显示和领一个layer的消失。与基本动画不一样,过渡动画操作的是layer的缓存image来创建通过单独更改属性而无法完成的可视效果。标准的过渡类型包含显示,push,移动或者交叉淡入淡出动画。

要执行过渡动画,首先我们需要创建CATransition 对象,然后将对象添加到需要过渡需要的图层中。我们可以使用过渡对象执行要执行的过渡类型以及过渡动画的起点和终点。过渡对象允许我们指定动画时候要使用的开始和结束进度值。这些值允许我们在其中间出开始或者结束动画。

     UIView  * myView2 =[[UIView alloc]initWithFrame:self.bounds];
        [self addSubview:myView2];
        myView2.backgroundColor = [UIColor yellowColor];
        UIView   *myView = [[UIView alloc]initWithFrame:self.bounds];
        myView.backgroundColor = [UIColor redColor];
        CGPoint center = myView.center;
        center.x-=self.bounds.size.width;
        myView.center = center;
        [self addSubview:myView];
        CATransition* transition = [CATransition animation];
        transition.startProgress = 0;
        transition.endProgress = 1.0;
        transition.type = kCATransitionPush;
        transition.subtype = kCATransitionFromRight;
        transition.duration = 1.0;
        
        // Add the transition animation to both layers
        [myView.layer addAnimation:transition forKey:@"transition"];
        [myView2.layer addAnimation:transition forKey:@"transition"];
        

实现效果


过渡动画

自定义动画时间

时间是动画的重要组成部分,通过CoreAnimation我们可以用CAMediaTiming协议的方法和属性为动画指定精确的时间控制。两个CoreAnimation类实现了该协议。CAAnimation 类实现了该协议,以便我们在动画对象中指定计时信息。CALayer也实现了该协议,方便我们为隐式动画配置一些与时序相关的功能,不过Calyer包装这些动画的隐式事务对象一般情况下是提供的默认时序信息。

当我们考虑时间和动画的时候,了解layer动画如何随着时间变化很重要。每个layer都有自己的本地时间,用于管理动画计时。通常情况下,两个不同layer的本地时间很接近,我们可以给每个layer指定相同的时间值,用户可能不会感觉有任何变化。但是layer的本地时间可以通过其父类或者自己的时序参数进行修改。比如:更改layer的速度属性可能导致该layer以及sublayer 上的动画持续时间按照比例更改。

为了帮助我们确保时间值是恰当的,Calyer类定义了convertTime:fromLayer:和convertTime:toLayer:方法。我们可以通过这些方法将固定时间值转换为layer的本地时间,或者将时间值从一个layer转换到领一个layer。这些方法会影响layer图层的本地时间,这些方法会返回与其他图层一起使用的值。

CFTimeInterval localLayerTime = [myLayer convertTime:CACurrentMediaTime() fromLayer:nil];

一旦layer的本地时间有值后,我们可以使用改之更新动画对象或layer与时序有关的属性。使用这些计时属性,我们可能实现一些有趣的动画,如下:

  • 使用beginTime 属性设置动画的开始时间。通常,动画在下一个更新周期开始。但是我们可以使用beginTime参数将动画开始时间延迟几秒执行。将两个动画连接在一起的方法是将一个动画的开始时间设置为与另一个动画结束时间想匹配。

如果我们延迟动画开始时间,我们还需要将fillMode 属性设置成kCAFillModeBackwards。即使layer对象在layer tree中包含了一个不同的起始值,该模式也会使用layer展示动画的起始值。如果不适用此模式,我们可能看到动画在执行之前跳转到了最终值了。

  • autoreverses 属性使动画在指定的持续时间内执行,然后返回到动画的起始位置。我们可以将此属性与repeatCount属性组合使用,使动画循环执行。如果我们将repeatCount设置为1,动画停在起始位置,1.5停止在结束位置。

  • 用timeOffset属性与组动画一起使用可以在稍后的时间启动某些动画。

据我估计大家看到这里也是一脸懵逼。不如上代码大家看的更明确

- (instancetype)initWithFrame:(CGRect)frame{
    if (self = [super initWithFrame:frame]) {
        MYLayer * layer = [MYLayer layer];
        layer.position= CGPointMake(150, 150);
        layer.bounds = self.bounds;
        layer.backgroundColor = [UIColor redColor].CGColor;
        [self.layer addSublayer:layer];
        self.anLayer = layer;
           CABasicAnimation* theAnim = [CABasicAnimation animationWithKeyPath:@"position"];
            theAnim.fromValue = [NSValue valueWithCGPoint:self.layer.position];
            theAnim.toValue = [NSValue valueWithCGPoint:CGPointMake(0, 0 )];
            theAnim.duration = 5;
            [layer addAnimation:theAnim forKey:@"AnimateFrame"];
      
        UIButton * button = [UIButton buttonWithType:UIButtonTypeCustom];
        button.backgroundColor = [UIColor blueColor];
        button.frame =CGRectMake(0, 0, 100, 50);
        [button addTarget:self action:@selector(button:) forControlEvents:UIControlEventTouchDown];
        [self addSubview:button];
    }
    return self;
}
-(void)button:(UIButton *)button{
    static BOOL ispause = YES;
    if (ispause) {
        ispause = NO;
        [self pauseLayer:self.layer];
    }else{
        ispause = YES;
        [self resumeLayer:self.layer];
    }
}

-(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;
}
延迟执行动画

convertTime:fromLayer: 这个函数将layer的本地时间更改为系统的时间。
layer的speed 设置为0 代表不执行动画了
layer.timeOffset 这里没用,只是用来记录下当前时间而已。
resumeLayer 的时候,我们需要在layer暂停的时间继续执行。因此,我们需要用当前时间减去暂停时间。
我们用layer的 beginTime 跳过暂停的时间继续执行。动画不变。

(事务)Transactions 可以让我们改变动画参数

我们对layer的所有改变都是transaction的一部分。CATransation类管理动画的创建以及分组和动画的执行。在大多数情况下,我们不需要创建自己的transaction。每次我们想layer中添加显示或者隐式动画,CoreAnimation都会自动创建隐式transation。不过我们也可以创建显式transation来更精确的管理我们的动画。

我们用CATransaction类中的方法创建transaction,开启一个显式事务需要调用CATransaction类中的begin方法,结束需要调用CATransaction类的commit方法。在这两个方法中间,就是我们需要改变的的内容。

[CATransaction begin];
theLayer.zPosition=200.0;
theLayer.opacity=0.0;
[CATransaction commit];

我们使用transations的主要原因之一是在显式事务的范围内,可以更改持续时间,计时功能和其他参数。我们也可以用block捕获动画完成,以便在动画组参数完成时通知我们。
更改动画参数需要使用setValue:forKey: 方法transaction修改字典中响应的key对应的值。
例如:将默认持续时间更改为10秒。具体看下面例子

[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:10.0f]
                 forKey:kCATransactionAnimationDuration];
// Perform the animations
[CATransaction commit];

我们可以嵌套使用transaction。如果我们将一个transaction嵌入另一个transaction,只需要重新调用begin方法。每个begin必须调用commit方法相对应。只有在最外层transactiont提交后,CoreAnimation才会执行关联动画。其实就是一个压栈操作了。当栈空之后才开始进行动画。
官方代码

[CATransaction begin]; // Outer transaction
// Change the animation duration to two seconds
[CATransaction setValue:[NSNumber numberWithFloat:2.0f]
                forKey:kCATransactionAnimationDuration];
theLayer.position = CGPointMake(0.0,0.0);
[CATransaction begin]; // Inner transaction
// Change the animation duration to five seconds
[CATransaction setValue:[NSNumber numberWithFloat:5.0f]
                 forKey:kCATransactionAnimationDuration];
// Change the zPosition and opacity
theLayer.zPosition=200.0;
theLayer.opacity=0.0;
[CATransaction commit]; // Inner transaction
[CATransaction commit]; // Outer transaction

这种写法有啥用呢?
这样我们就可以给每个属性设定不同的动画执行时间了。看具体代码

- (instancetype)initWithFrame:(CGRect)frame{
    if (self = [super initWithFrame:frame]) {
        MYLayer * layer = [MYLayer layer];
        layer.position= CGPointMake(150, 150);
        layer.bounds = self.bounds;
        layer.backgroundColor = [UIColor redColor].CGColor;
        [self.layer addSublayer:layer];
        self.anLayer = layer;
        UIButton * button = [UIButton buttonWithType:UIButtonTypeCustom];
        button.backgroundColor = [UIColor blueColor];
        button.frame =CGRectMake(0, 0, 100, 50);
        [button addTarget:self action:@selector(button:) forControlEvents:UIControlEventTouchDown];
        [self addSubview:button];
    }
    return self;
}
-(void)button:(UIButton *)button{
  
    [CATransaction begin];
    [CATransaction setValue:[NSNumber numberWithFloat:2.0f]
                     forKey:kCATransactionAnimationDuration];
    self.anLayer.position =CGPointZero;
    [CATransaction begin];
    [CATransaction setValue:[NSNumber numberWithFloat:5]
                     forKey:kCATransactionAnimationDuration];
    self.anLayer.opacity=0;
    [CATransaction commit]; // Inner transaction
    [CATransaction commit]; // Outer transaction
}
animation.gif

给动画增加透视效果

app可以在三维空间操作layer,单为了简单。CoreAnimation使用平行投影显示图层,改投影基本上将场景展示位二维平面。这会让具有不同深度的layer拥有相同的大小。不过我们可以通过修改layer的变换矩阵来实现透视效果。

修改透视效果,我们需要修改sublayer的transfrom矩阵,通过将相同的透视信息应用于sublayer,修改父类可简化我们代码。这样也能确保透视能正确的应用于不同平面中彼此重叠的兄弟layer

 MYLayer * layer = [MYLayer layer];
        layer.position= CGPointMake(150, 150);
        layer.bounds = self.bounds;
        layer.backgroundColor = [UIColor redColor].CGColor;
        [self.layer addSublayer:layer];
        self.anLayer = layer;
        self.anLayer.zPosition =-100;
        CATransform3D perspective = CATransform3DIdentity;
        perspective.m34 = -1.0/300; 
        self.layer.sublayerTransform = perspective;
        UIButton * button = [UIButton buttonWithType:UIButtonTypeCustom];
        button.backgroundColor = [UIColor blueColor];
        button.frame =CGRectMake(0, 0, 100, 50);
        [button addTarget:self action:@selector(button:) forControlEvents:UIControlEventTouchDown];
        [self addSubview:button];

变化前

变化后

如何提高动画的执行效率

核心动画是提高基于应用程序的动画的帧率的好方法,但是它的使用并不能保证性能的提高。

小窍门

有几种方法可以layer层实现更高效。但是,与任何此类优化一样,在尝试优化之前,您应该始终测量代码的当前性能。这为您提供了一个基线,可以用来确定优化是否有效。

尽可能使用不透明的layer

将layer的opaque属性设置为YES可以让核心动画知道它不需要为图层维护alpha通道。没有alpha通道意味着合成器不需要将layer的内容与背景内容混合,这节省了渲染的时间。然而,。如果直接将图像分配给层的内容属性,则无论不透明属性中的值如何,都会保留该图像的alpha通道。该属性对layer还是很关键的,对view有影响,layer 也可以理解为bitimap图的代表吧。如果你直接给layer的contents属性直接赋值,这里需要注意的是,image的alpha的alpha通道是还存在的,即使我们设置了layer的alpha通道,也没有用啦。

用CAShapeLayer对象做path绘制

CAShapeLayer 类在组合时间里会将path渲染成bitmap图来作为layer的content。其优点是该layer总是以最佳可能的分辨率绘制路径,但是这种优势是以额外的呈现时间为代价的。如果您提供的路径复杂,则对该路径进行光栅化(光栅化是把顶点数据转换为片元的过程,具有将图转化为一个个栅格组成的图象的作用,特点是每个元素对应帧缓冲区中的一像素)可能过于昂贵。如果层的大小经常变化(因此必须频繁地重绘),则绘制所花费的时间量会加在一起,并且成为性能瓶颈。

缩短绘制layer时间的一种方法是将复杂形状分解成较简单的形状。使用更简单的路径并在合成器中将多个CAShapeLayer对象层叠在一起,可以比绘制一个大型的复杂路径快得多。这是因为绘图操作发生在CPU上,而合成发生在GPU上。然而,与此性质的任何简化一样,潜在的性能增益取决于内容。因此,在优化之前测量代码的性能尤其重要,这样您就有了用于比较的基线。

相同的layer设置显式内容

如果我们使用相同的image给不同的layer设置图像,我们应该直接加载image,将该image直接设置给layer的contents。直接将image赋值给layer的contents,可以防止为该layer分配后备的存储内存(减少内存使用)。这样,该layer可以使用我们提供的layer作为后备存储。当多个layer使用相同的image,这意味着所有的这些layer共享相同的内存,而不是他们自己给自己分配的同乡副本。

始终将layer的size设置为整数值

为了获取最佳结果,我们应该将layer对象的长宽设置为整数值。虽然我们指定bounds用的是float类型,但是layer的边界最终是用来创建bitmap图的。coreAnimation最终是使用整数值来创建和管理后备存储和别的layer信息。(也就是说用float类型还需要进行将float类型转换成int类型的过程)

如果可以,用异步渲染layer

我们委托在drawLayer:inContext:method的方法和view的drawRect:方法中进行的绘制通常情况下都是发生在主线程上。然而,在某些情况下,同步绘制内容可能无法提供最佳性能。如果您注意到动画执行得不好,可以尝试启用层上的drawsAsynchronously属性将这些操作移动到后台线程。如果这样做,请确保绘图代码是线程安全的。

向layer添加阴影的时候指定阴影路径

CoreAnimation确定引用的形状是很耗费性能的。不要让CoreAnimation去确认阴影的形状,我们应该显示的执行阴影的形状。当我们为给属性执行路径时,CoreAnimation就能使用该形状进行绘制并且缓存阴影效果。对于形状不改变或者很少改变的layer,这样能减少CoreAnimation的渲染而极大的提供其性能。

你可能感兴趣的:(CoreAnimatin-高级动画技巧)