上一篇 : iOS-CALayer (四)
前言:继续深入学习动画,主要描述图层时间、缓冲、定时器动画。
一、图层时间
CAMediaTiming 协议定义在一段动画内控制逝去时间的属性集合, CALayer 和 CAAnimation 都实现了这个协议,时间可以被任意基于一个图层或一段动画的类控制。
1.1 持续和重复
显式动画的 duration 是CAMediaTiming 的属性之一,duration 是 CFTimeInterval 的类型,类似于 NSTimeInterval 的一种双精度浮点类型,duration对将要进行的动画的一次迭代指定了时间。
CAMediaTiming 属性 repeatCount 代表动画重复迭代次数。
duration 和 repeatCount 默认都是0 ,表示0.25s 和 1次。
创建重复动画的另一种方式 是使用 repeatDuration 属性,他让动画重复指定时间,而不是指定次数。 设置 autoreverse 的属性是 BOOL 类型,每次在间隔交替循环过程中自动回放,利于播放一段连续非循环的动画。
//add the door
CALayer *doorLayer = [CALayer layer];
doorLayer.frame = CGRectMake(0, 0, 128, 256);
doorLayer.position = CGPointMake(150 - 64, 150);
doorLayer.anchorPoint = CGPointMake(0, 0.5);
doorLayer.contents = (__bridge id)[UIImage imageNamed: @"Door.png"].CGImage;
[self.containerView.layer addSublayer:doorLayer];
//apply perspective transform
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / 500.0;
self.containerView.layer.sublayerTransform = perspective;
//apply swinging animation
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation.y";
animation.toValue = @(-M_PI_2);
animation.duration = 2.0;
animation.repeatDuration = INFINITY;
animation.autoreverses = YES;
[doorLayer addAnimation:animation forKey:nil];
1.2 相对时间
Core Animation 时间是相对的,每个动画有自己描述的时间,可以独立加速、延迟或偏移。
- beginTime 指定动画开始之前的延迟时间,延迟从动画添加到可见图层一刻开始测量。
- speed 时间的倍数,默认1.0。数值减少会减慢图层/动画的时间,增加数值会加快速度。
- timeOffset 与beginTime类似,但和增加 beginTime 导致延迟动画不同,增加timeOffset 让动画快进到某一点。例如:1秒的动画,timeOffset 设置0.5,动画将从一半开始。timeOffset不受 speed 影响。
{
[super viewDidLoad];
//create a path
self.bezierPath = [[UIBezierPath alloc] init];
[self.bezierPath moveToPoint:CGPointMake(0, 150)];
[self.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 = self.bezierPath.CGPath;
pathLayer.fillColor = [UIColor clearColor].CGColor;
pathLayer.strokeColor = [UIColor redColor].CGColor;
pathLayer.lineWidth = 3.0f;
[self.containerView.layer addSublayer:pathLayer];
//add the ship
self.shipLayer = [CALayer layer];
self.shipLayer.frame = CGRectMake(0, 0, 64, 64);
self.shipLayer.position = CGPointMake(0, 150);
self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
[self.containerView.layer addSublayer:self.shipLayer];
//set initial values
[self updateSliders];
}
- (IBAction)updateSliders
{
CFTimeInterval timeOffset = self.timeOffsetSlider.value;
self.timeOffsetLabel.text = [NSString stringWithFormat:@"%0.2f", timeOffset];
float speed = self.speedSlider.value;
self.speedLabel.text = [NSString stringWithFormat:@"%0.2f", speed];
}
- (IBAction)play
{
//create the keyframe animation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position";
animation.timeOffset = self.timeOffsetSlider.value;
animation.speed = self.speedSlider.value;
animation.duration = 1.0;
animation.path = self.bezierPath.CGPath;
animation.rotationMode = kCAAnimationRotateAuto;
animation.removedOnCompletion = NO;
[self.shipLayer addAnimation:animation forKey:@"slide"];
}
1.3 fillMode
removeOnCompletion = NO; 动画将在结束的时候依然保持之前的状态。
动画开始之前和动画结束之后,被设置动画的属性会是什么?
- 1.属性和动画没被添加之前保持一致,模型图层定义的值。
- 2.保持动画开始之前的那一帧,或动画结束之后的那一帧。
fillMode 是 NSString 类型,参数如下:
- kCAFillModeForwards ,默认值,当动画不播放时候就显示图层模型指定值。
- kCAFillModeBackwards
- kCAFillModeBoth
- kCAFillModeRemoved
使用fillMode 需要removeOnCompletion = NO;,需要给动画中添加非空的键。
1.4 层级关系时间
对CALayer 或 CAGroupAnimation 调整 duration 和 repeatCount/repeatDuration 属性并不会影响到子动画。但是beginTime,timeOffset 和 speed 属性将会影响到子动画。
在层级关系中,beginTime指定了父图层开始动画(或者组合关系中的父动画)和对象将要开始自己动画之间的偏移。调整CALayer和CAGroupAnimation的speed属性将会对动画以及子动画速度应用一个缩放的因子。
1.5 全局时间和本地时间
CoreAnimation有一个全局时间的概念,也就是所谓的马赫时间。当设备休眠的时候马赫时间会暂停,也就是所有的CAAnimations(基于马赫时间)同样也会暂停。
CFTimeInterval time = CACurrentMediaTime();
每个 CALayer 和 CAAnimation 实例都有自己本地时间的概念,是恩局父图层/动画层级中的 beginTime,timeOffset 和 speed 属性计算的。
转换不同图层之间的本地时间:
可以用来同步不同图层之间的beginTime,timeOffset 和 speed 。
- (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l;
- (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;
1.6 暂停、倒回、快进
给图层添加动画,实际上是给动画对象做一个不可改变的拷贝。对原始动画对象属性改变对真是的动画没有作用。如果使用 animationForKey 检索图层正在进行的动画可以返回正确的动画对象,但是修改属性将会抛出异常。
如果移除图层正在进行的动画,图层将会急速返回动画之前的状态。但如果在动画移除之前拷贝呈现图层到模型图层,动画将会看起来暂停在那里。但是不好的地方在于之后就不能再恢复动画了。
可以利用CAMediaTiming来暂停图层本身。如果把图层的speed设置成0,它会暂停任何添加到图层上的动画。设置speed大于1.0将会快进,设置成一个负值将会倒回动画。
暂停动画:speed=0;不能对正在进行的动画使用。
通过增加主窗口图层的speed,可以暂停整个应用程序的动画。这对UI自动化提供了好处,我们可以加速所有的视图动画来进行自动化测试(注意对于在主窗口之外的视图并不会被影响,比如UIAlertview)。在app delegate设置如下进行验证:
self.window.layer.speed = 100;
1.7 手动动画
timeOffset 可以手动控制动画进程。通过设置 speed=0,可以禁用动画的自然播放,timeOffset 显示动画序列。可以使用手势来手动控制动画。
- (void)viewDidLoad
{
[super viewDidLoad];
//add the door
self.doorLayer = [CALayer layer];
self.doorLayer.frame = CGRectMake(0, 0, 128, 256);
self.doorLayer.position = CGPointMake(150 - 64, 150);
self.doorLayer.anchorPoint = CGPointMake(0, 0.5);
self.doorLayer.contents = (__bridge id)[UIImage imageNamed:@"Door.png"].CGImage;
[self.containerView.layer addSublayer:self.doorLayer];
//apply perspective transform
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / 500.0;
self.containerView.layer.sublayerTransform = perspective;
//add pan gesture recognizer to handle swipes
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] init];
[pan addTarget:self action:@selector(pan:)];
[self.view addGestureRecognizer:pan];
//pause all layer animations
self.doorLayer.speed = 0.0;
//apply swinging animation (which won't play because layer is paused)
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation.y";
animation.toValue = @(-M_PI_2);
animation.duration = 1.0;
[self.doorLayer addAnimation:animation forKey:nil];
}
- (void)pan:(UIPanGestureRecognizer *)pan
{
//get horizontal component of pan gesture
CGFloat x = [pan translationInView:self.view].x;
//convert from points to animation duration //using a reasonable scale factor
x /= 200.0f;
//update timeOffset and clamp result
CFTimeInterval timeOffset = self.doorLayer.timeOffset;
timeOffset = MIN(0.999, MAX(0.0, timeOffset - x));
self.doorLayer.timeOffset = timeOffset;
//reset pan gesture
[pan setTranslation:CGPointZero inView:self.view];
}
@end
设置动画然后每次显示一帧,用移动手机设置动画的 Transform 更简单。
对于甚至更复杂的情况,多个图层动画组,相对实时计算每个图层的属性,修改Transform简单多了。
二、缓冲
Core Animation 用缓冲使动画移动平滑更自然。
2.1 动画速度
动画实际上是一段时间内的变化,变化随着特定的速度进行,计算公司:
velocity = change / time
变化指的是物体移动的距离,时间指动画持续时长。
2.2 CAMediaTimingFunction
Core Animation 的 timingFunction 是CAMediaTimingFunction类的对象。
CATransaction 的 +setAnimationTimingFunction: 方法,可以改变隐式动画的计时函数。
创建CAMediaTimingFunction
简单方式调用 timingFunctionWithName: 的构造方法。
传入的常量:
- kCAMediaTimingFunctionLinear ,默认变量,创建线性的计时函数
- kCAMediaTimingFunctionEaseIn ,慢慢加速然后突然停止方法
- kCAMediaTimingFunctionEaseOut ,全速开始,慢慢减速停止,削弱效果
- kCAMediaTimingFunctionEaseInEaseOut,慢慢加速,慢慢减速过程,UIView 动画默认方式
- kCAMediaTimingFunctionDefault,加速和减速构成稍微缓慢,隐式动画默认效果
- (void)layerTest{
_imgLayer = [CALayer layer];
_imgLayer.frame = CGRectMake(50, 50, 100, 100);
_imgLayer.backgroundColor = [UIColor orangeColor].CGColor;
_imgLayer.position = CGPointMake(_imgLayer.bounds.size.width*0.5+25, _imgLayer.bounds.size.width*0.5+25);
[self.view.layer addSublayer:_imgLayer];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self createCATransaction:touches];
}
- (void)createCATransaction:(NSSet<UITouch *> *)touches {
[CATransaction begin];
[CATransaction setAnimationDuration:1.0];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
_imgLayer.position = [[touches anyObject] locationInView:self.view];
[CATransaction commit];
}
效果图.gif
2.3 UIView 的动画缓冲
UIKit 动画支持缓冲方式。但与CAMediaTimingFunction 有紧密联系。options 参数如下:
- UIViewAnimationOptionCurveEaseInOut ,默认值 kCAMediaTimingFunctionDefault
- UIViewAnimationOptionCurveEaseIn
- UIViewAnimationOptionCurveEaseOut
- UIViewAnimationOptionCurveLinear
修改上述示例代码,同样效果
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[UIView animateWithDuration:1.0 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
_imgLayer.position = [[touches anyObject] locationInView:self.view];
} completion:NULL];
}
2.4 缓冲和关键帧动画
CAKeyframeAnimation 用于对每次动画步骤指定不同的计时函数的属性是 timingFunction(NSArray)。但指定函数的个数要等于 keyframes 数组的元素个数减一。这是描述每一帧之间动画速度的函数。
需要一个函数的数组告诉动画不停的重复每个步骤,而不是在整个动画序列只做一次缓冲,简单的使用包含多个相同函数拷贝的数组即可。
优化改变 CALayer 颜色的示例
- (IBAction)changeColor
{
//create a keyframe animation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"backgroundColor";
animation.duration = 2.0;
animation.values = @[
(__bridge id)[UIColor blueColor].CGColor,
(__bridge id)[UIColor redColor].CGColor,
(__bridge id)[UIColor greenColor].CGColor,
(__bridge id)[UIColor blueColor].CGColor ];
//add timing function
CAMediaTimingFunction *fn = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn];
animation.timingFunctions = @[fn, fn, fn];
//apply animation to layer
[self.colorLayer addAnimation:animation forKey:nil];
}
2.5 自定义缓冲函数
CAMediaTimingFunction同样有另一个构造函数,一个有四个浮点参数的+functionWithControlPoints::::。该方法可以创建一个自定义的缓冲函数,来匹配动画。
2.6 三次贝塞尔曲线
CAMediaTimingFunction 函数主要原则是在于把输入的时间转换成起点和终点之间成比例的改变。
线性缓冲函数图像
曲线的斜率表示速度,斜率改变表示加速。CAMediaTimingFunction使用三次贝塞尔曲线的函数,可以产出指定缓冲函数的子集。
三次贝塞尔曲线,通过四个点定义,第一个点=起点,第四个点=终点,中间点叫做控制点,控制曲线的形状,贝塞尔曲线的控制点其实是位于曲线之外的点,曲线并不一定要穿过控制点,可以类比成曲线的磁铁。
三次贝塞尔缓冲函数CAMediaTimingFunction有-getControlPointAtIndex:values:的方法,用来检索曲线的点,但是使用它我们可以找到标准缓冲函数的点,然后用UIBezierPath和CAShapeLayer来把它画出来。
CAMediaTimingFunction *function = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut];
//get control points
CGPoint controlPoint1, controlPoint2;
[function getControlPointAtIndex:1 values:(float *)&controlPoint1];
[function getControlPointAtIndex:2 values:(float *)&controlPoint2];
//create curve
UIBezierPath *path = [[UIBezierPath alloc] init];
[path moveToPoint:CGPointZero];
[path addCurveToPoint:CGPointMake(1, 1)
controlPoint1:controlPoint1 controlPoint2:controlPoint2];
//scale the path up to a reasonable size for display
[path applyTransform:CGAffineTransformMakeScale(200, 200)];
//create shape layer
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.lineWidth = 4.0f;
shapeLayer.path = path.CGPath;
[self.view.layer addSublayer:shapeLayer];
//flip geometry so that 0,0 is in the bottom-left
self.view.layer.geometryFlipped = YES;
标准CAMediaTimingFunction曲线
实现效果微弱,迅速上升,最后缓冲到终点的曲线。
[CAMediaTimingFunction functionWithControlPoints:1 :0 :0.75 :1];
2.7 复杂动画曲线
无法用三次贝塞尔曲线描述的反弹动画实现上述动画方法:
- CAKeyframeAnimation 创建动画,分割过程,每个过程使用自己的计时函数完成。
- 使用定时器逐帧更新实现动画。
2.8 关键帧缓冲
在缓冲曲线中对每个显著点创建一个关键帧,然后应用缓冲吧每段曲线链接起来,使用 keyTimes 指定关键帧的时间偏移,由于每次返单时间都会减少,关键帧不会均匀分布。
- (void)creatBall{
_imgLayer = [CALayer layer];
_imgLayer.backgroundColor = [UIColor orangeColor].CGColor;
_imgLayer.cornerRadius = 25;
_imgLayer.frame = CGRectMake(100, 25, 50, 50);
_imgLayer.position = CGPointMake(150, 50);
[self.view.layer addSublayer:_imgLayer];
[self ballAnimation];
}
- (void)ballAnimation{
_imgLayer.position = CGPointMake(150, 50);
CAKeyframeAnimation *keyAnima = [CAKeyframeAnimation animation];
keyAnima.keyPath = @"position";
keyAnima.duration = 1.0;
keyAnima.delegate = self;
keyAnima.values = @[
[NSValue valueWithCGPoint:CGPointMake(150, 50)],
[NSValue valueWithCGPoint:CGPointMake(150, 300)],
[NSValue valueWithCGPoint:CGPointMake(150, 150)],
[NSValue valueWithCGPoint:CGPointMake(150, 300)],
[NSValue valueWithCGPoint:CGPointMake(150, 200)],
[NSValue valueWithCGPoint:CGPointMake(150, 300)],
[NSValue valueWithCGPoint:CGPointMake(150, 250)],
[NSValue valueWithCGPoint:CGPointMake(150, 300)]
];
keyAnima.timingFunctions = @[
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn],
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut],
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn],
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut],
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn],
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut],
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn]
];
keyAnima.keyTimes = @[@0.0, @0.3, @0.5, @0.7, @0.8, @0.9, @0.95, @1.0];
self.imgLayer.position = CGPointMake(150, 300);
[self.imgLayer addAnimation:keyAnima forKey:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self ballAnimation];
}
效果图.gif
上述实例流程自动化
自动化需要做两件事:
- Q1:自动把任意属性动画分割成多个关键帧
- Q2:数学函数表示弹性动画
A1:需要复制Core Animation的插值机制。这是一个传入起点和终点,然后在这两个点之间指定时间点产出一个新点的机制。对于简单的浮点起始值,公式如下(假设时间从0到1):
value = (endValue – startValue) × time + startValue;
一旦我们可以用代码获取属性值动画的起始值之间的任意插值,就可以吧动画分割成许多独立的关键帧,然后产出一个线性的关键帧动画。
插入的值创建关键帧动画
float interpolate(float from, float to, float time)
{
return (to - from) * time + from;
}
- (id)interpolateFromValue:(id)fromValue toValue:(id)toValue time:(float)time
{
if ([fromValue isKindOfClass:[NSValue class]]) {
//get type
const char *type = [fromValue objCType];
if (strcmp(type, @encode(CGPoint)) == 0) {
CGPoint from = [fromValue CGPointValue];
CGPoint to = [toValue CGPointValue];
CGPoint result = CGPointMake(interpolate(from.x, to.x, time), interpolate(from.y, to.y, time));
return [NSValue valueWithCGPoint:result];
}
}
//provide safe default implementation
return (time < 0.5)? fromValue: toValue;
}
- (void)animate
{
//reset ball to top of screen
self.ballView.center = CGPointMake(150, 32);
//set up animation parameters
NSValue *fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
NSValue *toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
CFTimeInterval duration = 1.0;
//generate keyframes
NSInteger numFrames = duration * 60;
NSMutableArray *frames = [NSMutableArray array];
for (int i = 0; i < numFrames; i++) {
float time = 1 / (float)numFrames * i;
[frames addObject:[self interpolateFromValue:fromValue toValue:toValue time:time]];
}
//create keyframe animation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position";
animation.duration = 1.0;
animation.delegate = self;
animation.values = frames;
//apply animation
[self.ballView.layer addAnimation:animation forKey:nil];
}
缓冲进入缓冲退函数示例:
float quadraticEaseInOut(float t)
{
return (t < 0.5)? (2 * t * t): (-2 * t * t) + (4 * t) - 1;
}
弹球使用函数:
float bounceEaseOut(float t)
{
if (t < 4/11.0) {
return (121 * t * t)/16.0;
} else if (t < 8/11.0) {
return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0;
} else if (t < 9/10.0) {
return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0;
}
return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0;
}
关键帧实现自定义缓冲函数:
- (void)animate
{
//reset ball to top of screen
self.ballView.center = CGPointMake(150, 32);
//set up animation parameters
NSValue *fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
NSValue *toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
CFTimeInterval duration = 1.0;
//generate keyframes
NSInteger numFrames = duration * 60;
NSMutableArray *frames = [NSMutableArray array];
for (int i = 0; i < numFrames; i++) {
float time = 1/(float)numFrames * i;
//apply easing
time = bounceEaseOut(time);
//add keyframe
[frames addObject:[self interpolateFromValue:fromValue toValue:toValue time:time]];
}
//create keyframe animation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position";
animation.duration = 1.0;
animation.delegate = self;
animation.values = frames;
//apply animation
[self.ballView.layer addAnimation:animation forKey:nil];
}
网友评论