iOS Core Animation Advanced Techniques学习笔记(5)

显示动画

属性动画

通过- (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key;方法,我们可以为一个图层添加一个显示动画。但是,当这个图层并不是一个视图的图层属性的实例时,动画会发生两次,一个是由我们设置的显示动画引起的,另一个这是因为图层的隐式动画。为了避免这种情况,我们需要在配置显示动画的时候指定它的代理,并实现代理方法-animationDidStop:finished:,在其中设置一个新的事务,并禁用图层行为。

在设置显示动画的过程中,还有一个问题就是在动画完成之后图层的状态又回到了动画之前的状态。这是因为在默认情况下,动画完成之后将彻底移除,不会在其超出时间后还修改呈现层的图层。一旦动画完成,呈现层的图层将回到模型层图层的值,而我们又没有修改图层属性的值,因此动画完成后图层显示的还是之前的状态。

这里有两种解决方案,一种是在设置动画后更新图层对应属性的值:

// Add explicit animation to a single layer
CGFloat red = rand() / (double)INT_MAX;
CGFloat green = rand() / (double)INT_MAX;
CGFloat blue = rand() / (double)INT_MAX;

CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"backgroundColor";
animation.toValue = (__bridge id)[UIColor colorWithRed:red green:green blue:blue alpha:1].CGColor;
animation.delegate = self;
[self.colorLayer addAnimation:animation forKey:nil];

self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1].CGColor;

另一种则是设置动画的 fillMode 属性为 kCAFillModeForward 以留在最终状态,并设置removedOnCompletion 为 NO 以防止它被自动移除:

// Add explicit animation to a single layer
CGFloat red = rand() / (double)INT_MAX;
CGFloat green = rand() / (double)INT_MAX;
CGFloat blue = rand() / (double)INT_MAX;

CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"backgroundColor";
animation.toValue = (__bridge id)[UIColor colorWithRed:red green:green blue:blue alpha:1].CGColor;
animation.delegate = self;

animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion = NO;

[self.colorLayer addAnimation:animation forKey:nil];

需要注意的是:如果将已完成的动画保持在 layer 上时,会造成额外的开销,因为渲染器会去进行额外的绘画工作。

还有很重要的一点就是,当我们创建好一个动画并添加给一个layer时就立刻复制了一份,因此这个动画是可以重复添加个多个layer的。

关键帧动画

CAKeyframeAnimation是另一种UIKit没有暴露出来但功能强大的类。和CABasicAnimation类似,CAKeyframeAnimation同样是CAPropertyAnimation的一个子类,它依然作用于单一的一个属性,但是和CABasicAnimation不一样的是,它不限制于设置一个起始和结束的值,而是可以根据一连串随意的值来做动画。

示例代码:

CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position.x";
animation.values = @[ @0, @10, @-10, @10, @0 ];
animation.keyTimes = @[ @0, @(1 / 6.0), @(3 / 6.0), @(5 / 6.0), @1 ];
animation.duration = 0.4;

animation.additive = YES;

[form.layer addAnimation:animation forKey:@"shake"];

在使用CAKeyframeAnimation做动画时需要注意的是CAKeyframeAnimation并不能把当前layer的值作为第一帧动画,这就导致动画开始时会突然跳到第一帧的值,再再动画完成后恢复到原来的值,所以为了动画的平滑效果,我们需要开始和结束时的关键帧来匹配当前属性的值。

CAKeyframeAnimation还有另外一种方式来指定动画,就是使用CGPath。path属性可以用一种直观的方式,使用Core Graphics函数定义运动序列来绘制动画。

示例代码:

- (void)viewDidLoad
{
    [super viewDidLoad];
    //create a path
    UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
    [bezierPath moveToPoint:CGPointMake(0, 150)];
    [bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];
    
    //add the ship
    CALayer *shipLayer = [CALayer layer];
    shipLayer.frame = CGRectMake(0, 0, 64, 64);
    shipLayer.position = CGPointMake(0, 150);
    shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
    [self.containerView.layer addSublayer:shipLayer];
    
    //create the keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 4.0;
    animation.path = bezierPath.CGPath;
    animation.rotationMode = kCAAnimationRotateAuto;

    [shipLayer addAnimation:animation forKey:nil];
}

虚拟属性

在layer中,有一种属性其实是不存在的,如:transform.rotationtransform.rotationtransform.rotation等。因为CATransform3D并不是一个对象,而是一个结构体,也没有符合KVC相关属性,它们实际上是CALayer用于处理动画变换的虚拟属性。

它们不能被直接使用。当对他们做动画时,Core Animation自动地根据通过CAValueFunction来计算的值来更新transform属性。

CAValueFunction用于把我们赋给虚拟的transform.rotation简单浮点值转换成真正的用于摆放图层的CATransform3D矩阵值。我们可以通过设置CAPropertyAnimationvalueFunction属性来改变,这将会覆盖默认的函数。

示例代码:

- (void)viewDidLoad
{
    [super viewDidLoad];
    //add the ship
    CALayer *shipLayer = [CALayer layer];
    shipLayer.frame = CGRectMake(0, 0, 128, 128);
    shipLayer.position = CGPointMake(150, 150);
    shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
    [self.containerView.layer addSublayer:shipLayer];
    
    //animate the ship rotation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform.rotation";
    animation.duration = 2.0;
    animation.byValue = @(M_PI * 2);
    [shipLayer addAnimation:animation forKey:nil];
}

动画组

CABasicAnimationCAKeyframeAnimation仅仅作用于单独的属性,而CAAnimationGroup可以把这些动画组合在一起。CAAnimationGroup是另一个继承于CAAnimation的子类,它添加了一个animations数组的属性,用来组合别的动画。

示例代码:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    //create a path
    UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
    [bezierPath moveToPoint:CGPointMake(0, 150)];
    [bezierPath addCurveToPoint:CGPointMake(300, 150) 
                   controlPoint1:CGPointMake(75, 0) 
                   controlPoint2:CGPointMake(225, 300)];
    
    //draw the path using a CAShapeLayer
    CAShapeLayer *pathLayer = [CAShapeLayer layer];
    pathLayer.path = bezierPath.CGPath;
    pathLayer.fillColor = [UIColor clearColor].CGColor;
    pathLayer.strokeColor = [UIColor redColor].CGColor;
    pathLayer.lineWidth = 3.0f;
    [self.containerView.layer addSublayer:pathLayer];
    
    //add a colored layer
    CALayer *colorLayer = [CALayer layer];
    colorLayer.frame = CGRectMake(0, 0, 64, 64);
    colorLayer.position = CGPointMake(0, 150);
    colorLayer.backgroundColor = [UIColor greenColor].CGColor;
    [self.containerView.layer addSublayer:colorLayer];
    
    //create the position animation
    CAKeyframeAnimation *animation1 = [CAKeyframeAnimation animation];
    animation1.keyPath = @"position";
    animation1.path = bezierPath.CGPath;
    animation1.rotationMode = kCAAnimationRotateAuto;
    
    //create the color animation
    CABasicAnimation *animation2 = [CABasicAnimation animation];
    animation2.keyPath = @"backgroundColor";
    animation2.toValue = (__bridge id)[UIColor redColor].CGColor;
    
    //create group animation
    CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];
    groupAnimation.animations = @[animation1, animation2]; 
    groupAnimation.duration = 4.0;
    
    //add the animation to the color layer
    [colorLayer addAnimation:groupAnimation forKey:nil];
}

过渡

属性动画只对layer的可动画属性起作用,如果要改变一个不可动画属性(如图片、文本),或者从层级关系中添加或移除图层,属性动画将不再起作用,这时,我们可以使用过渡来实现动画效果。

为了创建一个过渡动画,我们将使用CATransition,同样是另一个CAAnimation的子类,和别的子类不同,CATransition有一个type和subtype来标识变换效果。type属性是一个NSString类型,用来设置过渡动画效果,可以被设置成如下类型:

  • kCATransitionFade
  • kCATransitionMoveIn
  • kCATransitionPush
  • kCATransitionReveal

通过subtype我们可以控制动画的方向,它提供了如下四种类型:

  • kCATransitionFromRight
  • kCATransitionFromLeft
  • kCATransitionFromTop
  • kCATransitionFromBottom

示例代码:

- (IBAction)switchImage
{
    //set up crossfade transition
    CATransition *transition = [CATransition animation];
    transition.type = kCATransitionFade;
    
    //apply transition to imageview backing layer
    [self.imageView.layer addAnimation:transition forKey:nil];
    
    //cycle to next image
    UIImage *currentImage = self.imageView.image;
    NSUInteger index = [self.images indexOfObject:currentImage];
    index = (index + 1) % [self.images count];
    self.imageView.image = self.images[index];
}

隐式过渡

对于视图关联的图层,或者是其他隐式动画的行为,过渡的特性依然是被禁用的,但是对于我们自己创建的图层,这意味着对图层contents图片做的改动都会自动附上淡入淡出的动画。

对图层树的动画

CATransition并不作用于指定的图层属性,这就是说我们可以在即使不能准确得知改变了什么的情况下对图层做动画,例如,在不知道UITableView哪一行被添加或者删除的情况下,直接就可以平滑地刷新它,或者在不知道UIViewController内部的视图层级的情况下对两个不同的实例做过渡动画。

在这里,我们只要将过渡动画添加到被影响图层的superlayer,就实现了对整个图层树添加过渡动画。

UITabBarController添加过渡动画,示例代码:

#import "AppDelegate.h"
#import "FirstViewController.h" 
#import "SecondViewController.h"
#import 
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
    UIViewController *viewController1 = [[FirstViewController alloc] init];
    UIViewController *viewController2 = [[SecondViewController alloc] init];
    self.tabBarController = [[UITabBarController alloc] init];
    self.tabBarController.viewControllers = @[viewController1, viewController2];
    self.tabBarController.delegate = self;
    self.window.rootViewController = self.tabBarController;
    [self.window makeKeyAndVisible];
    return YES;
}

- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController
{
    //set up crossfade transition
    CATransition *transition = [CATransition animation];
    transition.type = kCATransitionFade;
    
    //apply transition to tab bar controller's view
    [self.tabBarController.view.layer addAnimation:transition forKey:nil];
}

@end

在动画过程中取消动画

移除指定动画:

- (void)removeAnimationForKey:(NSString *)key;

移除所有动画:

- (void)removeAllAnimations;

图层时间

CAMediaTiming协议

CAMediaTiming协议定义了在一段动画内用来控制逝去时间的属性的集合,CALayer和CAAnimation都实现了这个协议,所以时间可以被任意基于一个图层或者一段动画的类控制。

持续和重复

duration:设置动画迭代一次的时间

repeatCount:设置动画迭代次数

动画时长 = duration * repeatCount

repeatDuration:设置动画重复时长(当时此属性值与duration的值一致时动画不重复)

autoreverses:完成一次动画迭代后,自动后退到图层原始状态

相对时间

beginTime:设置动画开始延迟时间

speed:设置动画播放倍数,默认为1.0

timeOffset:设置动画播放位置(0~1)

fillMode

fillMode用来设置动画开始前和结束后的这段时间动画属性的值,它是一个NSString类型,可以接受如下四种常量:

  • kCAFillModeForwards
  • kCAFillModeBackwards
  • kCAFillModeBoth
  • kCAFillModeRemoved

注意,如果要图层保持动画结束时的状态,除了要设置fillMode,还需要将removeOnCompletion设置为NO。另外,将已完成的动画保持在layer上,会造成额外的开销,因为渲染器需要进行额外的绘制工作。所以,最好是在为layer添加动画的时候设置一个key,这样就可以在不需要动画的时候将它从layer上移除。

层级关系时间

在图层树中每个图层都有一个特定的坐标系统来定义该图层和它的父图层之间的关系,动画时间同样也是如此。每个图层和动画都有一套基于它父视图的层级关系概念上的时间。改变一个layer的时间将会影响该图层和它的子图层的动画,但不会对它的父图层产生影响。这点同样适用于由CAAnimationGroup组成的动画。

调整CALayerCAAnimationGroupdurationrepeatCountrepeatDuration属性不会对子图层的动画产生影响,而调整beginTime, timeOffsetspeed 属相将会对子视图的动画也产生影响。

全局时间和局部时间

在Core Animation当中,有着全局时间和局部时间的概念。全局时间以设备时间为标准,通过如下方法可以获得:

CFTimeInterval time = CACurrentMediaTime();

局部时间是每个layer的时间体系,当beginTime,timeOffsetspeed属性发生改变时,局部时间可能会和全局时间不一致。通过以下方法可以在转换局部时间:

- (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l; 
- (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;

暂停,倒放和快进

当将一个动画的speed属性设为0,动画将暂停,但是这不会对正在动画中的layer产生影响,因为当我们将一个动画添加到一个layer上时,是将动画copy然后赋值给layer。我们可以通过-animationForKey:获取到正在动画的layer真正的动画,但是,即使获取到了正确的动画,我们依然不能通过直接修改动画的speed属性来让动画暂停。

当然,我们也可以在动画被移除钱获取presentation layer的属性值然后赋值给model layer。但是这样做有一个缺点就是,在之后想要再继续动画将会很复杂。

暂停动画正确地姿势是利用层级关系时间。也就是调整动画所属layer的speed属性。

如果要调整整个app的动画速度,我们只需要如此这般:

self.window.layer.speed = 100;

手动动画

通过layer的timeOffset属性和UIPanGestureRecognizer手势,我们可以很容易地实现一个手动控制的动画。


缓冲

动画速度

CAMediaTimingFunction

通过设置CAAnimationtimingFunction属性,我们可以改变动画的缓冲函数:

animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

如果要改变隐式动画的缓冲函数,我们可以使用CATransaction+setAnimationTimingFunction:方法:

[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];

创建CAMediaTimingFunction最简单的方法是调用+timingFunctionWithName:的构造方法。这里传入如下几个常量之一:

  • kCAMediaTimingFunctionLinear
  • kCAMediaTimingFunctionEaseIn
  • kCAMediaTimingFunctionEaseOut
  • kCAMediaTimingFunctionEaseInEaseOut
  • kCAMediaTimingFunctionDefault

UIView的动画缓冲

UIKit的动画也同样支持这些缓冲方法的使用,尽管语法和常量有些不同,为了改变UIView动画的缓冲选项,给options参数添加如下常量之一:

  • UIViewAnimationOptionCurveEaseInOut
  • UIViewAnimationOptionCurveEaseIn
  • UIViewAnimationOptionCurveEaseOut
  • UIViewAnimationOptionCurveLinear

示例代码:

[UIView animateWithDuration:5
                      delay:1
                    options:UIViewAnimationOptionCurveEaseInOut
                 animations:^{
    view.layer.backgroundColor = [UIColor redColor].CGColor;
}
                 completion:NULL];

缓冲和关键帧动画

CAKeyframeAnimation有一个NSArray类型的timingFunctions属性,我们可以用它来对每次动画的步骤指定不同的计时函数。但是指定函数的个数一定要等于keyframes数组的元素个数减一,因为它是描述每一帧之间动画速度的函数。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,408评论 4 371
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,690评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,036评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,726评论 0 221
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,123评论 3 296
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,037评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,178评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,964评论 0 213
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,703评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,863评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,333评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,658评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,374评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,195评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,988评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,167评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,970评论 2 279

推荐阅读更多精彩内容