原文出处: Airfei
源码下载:源码
最近在技术群里,有人发了一张带有动画效果的图片。觉得很有意思,便动手实现了一下。在这篇文章中你将会学到Core Animation显式动画中的关键帧动画、组合动画、CABasicAnimation动画。先上一张原图的动画效果。
点击此查看原图动画效果。
本文要实现的效果图如下:
实现的效果图.gif
把原动画gif动画在mac上使用图片浏览模式打开,我们可以看到动画每一帧的显示。从每一帧上的展示过程,可以把整体的动画进行拆分成两大部分。
第一部分(Part1)从初始状态变成取消状态(图片上是由横实线变成上线横线交叉的圆)。
第二部分(Part2)从取消状态变回初始状态。
下面我们先详细分析Part1是怎么实现的。根据动画图,把Part1再细分成三步。
Step1 : 中间横实线的由右向左的运动效果。这其实是一个组合动画。是先向左偏移的同时横线变短。先看一下实现的动态效果。
step1 Animation.gif
■ 向左偏移—使用基本动画中animationWithKeyPath
键值对的方式来改变动画的值。我们这里使用position.x
,同样可以使用transform.translation.x
来平移。
■ 改变横线的大小—使用经典的strokeStart
和strokeEnd
。其实上横线长度的变化的由strokeStart
到strokeEnd
之间的值来共同来决定。改变strokeEnd
的值由1.0到0.4,不改变strokeStart
的值。横线的长度会从右侧方向由1.0倍长度减少到0.4倍长度。参见示意图的红色区域。
stroke示意图.png
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
-(void) animationStep1{
//最终changedLayer的状态 _changedLayer.strokeEnd = 0.4; //基本动画,长度有1.0减少到0.4 CABasicAnimation *strokeAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"]; strokeAnimation.fromValue = [NSNumber numberWithFloat:1.0f]; strokeAnimation.toValue = [NSNumber numberWithFloat:0.4f]; //基本动画,向左偏移10个像素 CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"position.x"]; pathAnimation.fromValue = [NSNumber numberWithFloat:0.0]; pathAnimation.toValue = [NSNumber numberWithFloat:-10]; //组合动画,平移和长度减少同时进行 CAAnimationGroup *animationGroup = [CAAnimationGroup animation]; animationGroup.animations = [NSArray arrayWithObjects:strokeAnimation,pathAnimation, nil]; animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]; animationGroup.duration = kStep1Duration; //设置代理 animationGroup.delegate = self; animationGroup.removedOnCompletion = YES; //监听动画 [animationGroup setValue:@"animationStep1" forKey:@"animationName"]; //动画加入到changedLayer上 [_changedLayer addAnimation:animationGroup forKey:nil]; } |
Step2 : 由左向右的动画–向右偏移同时横线长度变长。看一下Step2要实现的动画效果。其思路和Step1是一样的。
step2 Animation.gif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
-(void)animationStep2 { CABasicAnimation *translationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"]; translationAnimation.fromValue = [NSNumber numberWithFloat:-10]; //strokeEnd:0.8 剩余的距离toValue = lineWidth * (1 - 0.8);
translationAnimation.toValue = [NSNumber numberWithFloat:0.2 * lineWidth ];
_changedLayer.strokeEnd = 0.8; CABasicAnimation *strokeAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"]; strokeAnimation.fromValue = [NSNumber numberWithFloat:0.4f]; strokeAnimation.toValue = [NSNumber numberWithFloat:0.8f];
CAAnimationGroup *animationGroup = [CAAnimationGroup animation]; animationGroup.animations = [NSArray arrayWithObjects:strokeAnimation,translationAnimation, nil]; animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; animationGroup.duration = kStep2Duration; //设置代理 animationGroup.delegate = self; animationGroup.removedOnCompletion = YES; [animationGroup setValue:@"animationStep2" forKey:@"animationName"]; [_changedLayer addAnimation:animationGroup forKey:nil]; } |
Step3: 圆弧的动画效果和上下两个横实线的动画效果。
画圆弧,首先想到是使用UIBezierPath
。画个示意图来分析动画路径。示意图如下:
step3 示意图.jpg
整个path路径是由三部分组成,ABC曲线
、CD圆弧
、DD′圆
。
使用UIBezierPath
的方法
1 |
- (void)appendPath:(UIBezierPath *)bezierPath; |
把三部分路径关联起来。详细讲解思路。
• ABC曲线
就是贝塞尔曲线,可以根据A、B、C三点的位置使用方法
1 2 3 4 5 |
//endPoint 终点坐标 controlPoint1 起点坐标 //controlPoint2 起点和终点在曲线上的切点延伸相交的交点坐标 - (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2; |
二次贝塞尔曲线示意图如下:
二次贝塞尔曲线.png
其中control point 点是从曲线上取 start point和end point 切点相交汇的所得到的交点。如下图:
control point .png
首先C点取圆上的一点,-30°。那么,
1 |
CGFloat angle = Radians(30); |
C点坐标为:
1 2 3 |
//C点 CGFloat endPointX = self.center.x + Raduis * cos(angle); CGFloat endPointY = kCenterY - Raduis * sin(angle); |
A点坐标为:
1 2 3 |
//A点 取横线最右边的点 CGFloat startPointX = self.center.x + lineWidth/2.0 ; CGFloat startPointY = controlPointY; |
control point 为E点:
1 2 3 |
//E点 半径*反余弦(30°) CGFloat startPointX = self.center.x + Raduis *acos(angle); CGFloat startPointY = controlPointY; |
• CD圆弧
的路径使用此方法确定
1 |
+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise; |
关于弧度问题,UIBezierPath的官方文档中的这张图:
弧度.jpg
StartAngle 弧度即C点弧度,EndAngel弧度即D点弧度。
1 2 |
CGFloat StartAngle = 2 * M_PI - angle; CGFloat EndAngle = M_PI + angle; |
• DD′圆
的路径和上面2一样的方法确定。
StartAngle 弧度即D点弧度,EndAngel弧度即D′点弧度。
1 2 |
CGFloat StartAngle = M_PI *3/2 - (M_PI_2 -angle); CGFloat EndAngle = -M_PI_2 - (M_PI_2 -angle); |
下面部分代码是所有path路径。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
UIBezierPath *path = [UIBezierPath bezierPath];
// 画贝塞尔曲线 圆弧 [path moveToPoint:CGPointMake(self.center.x + lineWidth/2.0 , kCenterY)];
CGFloat angle = Radians(30); //C点 CGFloat endPointX = self.center.x + Raduis * cos(angle); CGFloat endPointY = kCenterY - Raduis * sin(angle); //A点 CGFloat startPointX = self.center.x + lineWidth/2.0; CGFloat startPointY = kCenterY; //E点 半径*反余弦(30°) CGFloat controlPointX = self.center.x + Raduis *acos(angle); CGFloat controlPointY = kCenterY;
//贝塞尔曲线 ABC曲线 [path addCurveToPoint:CGPointMake(endPointX, endPointY) controlPoint1:CGPointMake(startPointX , startPointY) controlPoint2:CGPointMake(controlPointX , controlPointY)];
// (360°- 30°) ->(180°+30°) 逆时针的圆弧 CD圆弧 UIBezierPath *path1 = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.center.x,kCenterY) radius:Raduis startAngle:2 * M_PI - angle endAngle:M_PI + angle clockwise:NO]; [path appendPath:path1]; // (3/2π- 60°) ->(-1/2π -60°) 逆时针的圆 DD′圆 UIBezierPath *path2 = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.center.x,kCenterY) radius:Raduis startAngle:M_PI *3/2 - (M_PI_2 -angle) endAngle:-M_PI_2 - (M_PI_2 -angle) clockwise:NO];
[path appendPath:path2];
_changedLayer.path = path.CGPath; |
Path路径有了,接着实现动画效果。
圆弧的长度逐渐变长。我们还是使用经典的strokeStart
和strokeEnd
。但是圆弧是如何变长的呢?
(1) 初始圆弧有一段长度。
(2) 在原始长度的基础上逐渐变长,逐渐远离A点,同时要在D点停止。
(3) 长度逐渐变长,最终要在D与D′点交汇。
我们分别解决这个三个问题。
第一个问题,strokeEnd - strokeStart > 0
这样能保证有一段圆弧。
第二个问题,逐渐变长,意味着strokeEnd
值不断变大。远离A点意味着strokeStart
的值不断变大。在D点停止,说明了strokeStart
有上限值。
第三个问题,意味着strokeEnd
值不断变大,最终值为1.0。
这三个问题说明了一个问题,strokeEnd
和strokeStart
是一组变化的数据。
那么core animation 中可以控制一组值的动画是关键帧动画(CAKeyframeAnimation
)。
为了更准确的给出strokeEnd
和strokeStart
值,我们使用长度比
来确定。
假设我们初始的长度就是曲线ABC的长度。但是贝塞尔曲线长度怎么计算?使用下面方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
//求贝塞尔曲线长度 -(CGFloat) bezierCurveLengthFromStartPoint:(CGPoint)start toEndPoint:(CGPoint) end withControlPoint:(CGPoint) control { const int kSubdivisions = 50; const float step = 1.0f/(float)kSubdivisions;
float totalLength = 0.0f; CGPoint prevPoint = start;
// starting from i = 1, since for i = 0 calulated point is equal to start point for (int i = 1; i <= kSubdivisions; i++) { float t = i*step;
float x = (1.0 - t)*(1.0 - t)*start.x + 2.0*(1.0 - t)*t*control.x + t*t*end.x; float y = (1.0 - t)*(1.0 - t)*start.y + 2.0*(1.0 - t)*t*control.y + t*t*end.y;
CGPoint diff = CGPointMake(x - prevPoint.x, y - prevPoint.y);
totalLength += sqrtf(diff.x*diff.x + diff.y*diff.y); // Pythagorean
prevPoint = CGPointMake(x, y); }
return totalLength; } |
计算贝塞尔曲线所在的比例为:
1 |
CGFloat orignPercent = [self calculateCurveLength]/[self calculateTotalLength]; |
初始的strokeStart = 0
、strokeEnd = orignPercent
。
最终的stokeStart = ?
1 2 |
//结果就是贝塞尔曲线长度加上120°圆弧的长度与总长度相比得到的结果。 CGFloat endPercent =([self calculateCurveLength] + Radians(120) *Raduis ) / [self calculateTotalLength]; |
实现动画的代码为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
CGFloat orignPercent = [self calculateCurveLength] / [self calculateTotalLength]; CGFloat endPercent =([self calculateCurveLength] + Radians(120) *Raduis ) / [self calculateTotalLength];
_changedLayer.strokeStart = endPercent;
//方案1 CAKeyframeAnimation *startAnimation = [CAKeyframeAnimation animationWithKeyPath:@"strokeStart"]; startAnimation.values = @[@0.0,@(endPercent)];
CAKeyframeAnimation *EndAnimation = [CAKeyframeAnimation animationWithKeyPath:@"strokeEnd"]; EndAnimation.values = @[@(orignPercent),@1.0];
CAAnimationGroup *animationGroup = [CAAnimationGroup animation]; animationGroup.animations = [NSArray arrayWithObjects:startAnimation,EndAnimation, nil]; animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; animationGroup.duration = kStep3Duration; animationGroup.delegate = self; animationGroup.removedOnCompletion = YES; [animationGroup setValue:@"animationStep3" forKey:@"animationName"]; [_changedLayer addAnimation:animationGroup forKey:nil]; |
效果图为:
step3-1 Animation.gif
2.上下横线的动画效果。
此动画效果,需要使用transform.rotation.z
转动角度。
上横线转动的角度顺序为 0 -> 10° -> (-55°) -> (-45°)
这是一组数据,使用关键帧处理动画。
1 2 3 4 5 6 |
CAKeyframeAnimation *rotationAnimation1 = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation.z"]; rotationAnimation1.values = @[[NSNumber numberWithFloat:0], [NSNumber numberWithFloat:Radians(10) ], [NSNumber numberWithFloat:Radians(-10) - M_PI_4 ], [NSNumber numberWithFloat:- M_PI_4 ] ]; |
下横线转动的角度顺序为0 -> (-10°) -> (55°) -> (45°)
1 2 3 4 5 6 |
CAKeyframeAnimation *rotationAnimation2 = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation.z"]; rotationAnimation2.values = @[[NSNumber numberWithFloat:0], [NSNumber numberWithFloat:Radians(-10) ], [NSNumber numberWithFloat:Radians(10) + M_PI_4 ], [NSNumber numberWithFloat: M_PI_4 ] ]; |
你认为这么就结束了? 最终结束的动画如下:
step3-2 finished Animation.jpg
发现相交的直线没有居中,而是靠左显示。
向左平移,使用transform.translation.x
1 2 |
//平移量 CGFloat toValue = lineWidth *(1- cos(M_PI_4)) /2.0; |
即旋转角度又发生偏移量,使用组合动画。
上横线组合动画
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
//平移x CABasicAnimation *translationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"]; translationAnimation.fromValue = [NSNumber numberWithFloat:0]; translationAnimation.toValue = [NSNumber numberWithFloat:-toValue];
//角度关键帧 上横线的关键帧 0 - 10° - (-55°) - (-45°) CAKeyframeAnimation *rotationAnimation1 = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation.z"]; rotationAnimation1.values = @[[NSNumber numberWithFloat:0], [NSNumber numberWithFloat:Radians(10) ], [NSNumber numberWithFloat:Radians(-10) - M_PI_4 ], [NSNumber numberWithFloat:- M_PI_4 ] ];
CAAnimationGroup *transformGroup1 = [CAAnimationGroup animation]; transformGroup1.animations = [NSArray arrayWithObjects:rotationAnimation1,translationAnimation, nil]; transformGroup1.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; transformGroup1.duration = kStep3Duration; transformGroup1.removedOnCompletion = YES; [_topLineLayer addAnimation:transformGroup1 forKey:nil]; |
下横线组合动画
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//角度关键帧 下横线的关键帧 0 - (-10°) - (55°) - (45°) CAKeyframeAnimation *rotationAnimation2 = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation.z"]; rotationAnimation2.values = @[[NSNumber numberWithFloat:0], [NSNumber numberWithFloat:Radians(-10) ], [NSNumber numberWithFloat:Radians(10) + M_PI_4 ], [NSNumber numberWithFloat: M_PI_4 ] ];
CAAnimationGroup *transformGroup2 = [CAAnimationGroup animation]; transformGroup2.animations = [NSArray arrayWithObjects:rotationAnimation2,translationAnimation, nil]; transformGroup2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; transformGroup2.duration = kStep3Duration ; transformGroup2.delegate = self; transformGroup2.removedOnCompletion = YES; [_bottomLineLayer addAnimation:transformGroup2 forKey:nil]; |
Part1到此结束。最终效果图
Part1 animation.gif
Part2的思路和Part1思路是一样的。你可以参考代码自己思考一下。核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
-(void)cancelAnimation { //最关键是path路径
UIBezierPath *path = [UIBezierPath bezierPath]; //30度,经过反复测试,效果最好 CGFloat angle = Radians(30);
CGFloat startPointX = self.center.x + Raduis * cos(angle); CGFloat startPointY = kCenterY - Raduis * sin(angle);
CGFloat controlPointX = self.center.x + Raduis *acos(angle); CGFloat controlPointY = kCenterY;
CGFloat endPointX = self.center.x + lineWidth /2; CGFloat endPointY = kCenterY;
//组合path 路径 起点 -150° 顺时针的圆 path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.center.x,kCenterY) radius:Raduis startAngle:-M_PI + angle endAngle:M_PI + angle clockwise:YES];
//起点为 180°-> (360°-30°) UIBezierPath *path1 = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.center.x,kCenterY) radius:Raduis startAngle:M_PI + angle endAngle:2 * M_PI - angle clockwise:YES]; [path appendPath:path1];
//三点曲线 UIBezierPath *path2 = [UIBezierPath bezierPath];
[path2 moveToPoint:CGPointMake(startPointX, startPointY)];
[path2 addCurveToPoint:CGPointMake(endPointX,endPointY) controlPoint1:CGPointMake(startPointX, startPointY) controlPoint2:CGPointMake(controlPointX, controlPointY)];
[path appendPath:path2];
//比原始状态向左偏移5个像素 UIBezierPath *path3 = [UIBezierPath bezierPath]; [path3 moveToPoint:CGPointMake(endPointX,endPointY)]; [path3 addLineToPoint:CGPointMake(self.center.x - lineWidth/2 -5,endPointY)]; [path appendPath:path3];
_changedLayer.path = path.CGPath;
//平移量 CGFloat toValue = lineWidth *(1- cos(M_PI_4)) /2.0; //finished 最终状态 CGAffineTransform transform1 = CGAffineTransformMakeRotation(0); CGAffineTransform transform2 = CGAffineTransformMakeTranslation(0, 0); CGAffineTransform transform3 = CGAffineTransformMakeRotation(0);
CGAffineTransform transform = CGAffineTransformConcat(transform1, transform2); _topLineLayer.affineTransform = transform; transform = CGAffineTransformConcat(transform3, transform2); _bottomLineLayer.affineTransform = transform;
//一个圆的长度比 CGFloat endPercent = 2* M_PI *Raduis / ([self calculateTotalLength] + lineWidth);
//横线占总path的百分比 CGFloat percent = lineWidth / ([self calculateTotalLength] + lineWidth);
_changedLayer.strokeStart = 1.0 -percent;
CAKeyframeAnimation *startAnimation = [CAKeyframeAnimation animationWithKeyPath:@"strokeStart"]; startAnimation.values = @[@0.0,@0.3,@(1.0 -percent)];
//在π+ angle CAKeyframeAnimation *EndAnimation = [CAKeyframeAnimation animationWithKeyPath:@"strokeEnd"]; EndAnimation.values = @[@(endPercent),@(endPercent),@1.0];
CAAnimationGroup *animationGroup = [CAAnimationGroup animation]; animationGroup.animations = [NSArray arrayWithObjects:startAnimation,EndAnimation, nil]; animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; animationGroup.duration = kStep4Duration; animationGroup.delegate = self; animationGroup.removedOnCompletion = YES; [animationGroup setValue:@"animationStep4" forKey:@"animationName"]; [_changedLayer addAnimation:animationGroup forKey:nil];
//平移x CABasicAnimation *translationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"]; translationAnimation.fromValue = [NSNumber numberWithFloat:-toValue]; translationAnimation.toValue = [NSNumber numberWithFloat:0];
//角度关键帧 上横线的关键帧 (-45°) -> (-55°)-> 10° -> 0 CAKeyframeAnimation *rotationAnimation1 = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation.z"]; rotationAnimation1.values = @[[NSNumber numberWithFloat:- M_PI_4 ], [NSNumber numberWithFloat:- Radians(10) - M_PI_4 ], [NSNumber numberWithFloat:Radians(10) ], [NSNumber numberWithFloat:0] ];
CAAnimationGroup *transformGroup1 = [CAAnimationGroup animation]; transformGroup1.animations = [NSArray arrayWithObjects:rotationAnimation1,translationAnimation, nil]; transformGroup1.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; transformGroup1.duration = kStep4Duration; transformGroup1.removedOnCompletion = YES; [_topLineLayer addAnimation:transformGroup1 forKey:nil];
//角度关键帧 下横线的关键帧 (45°)-> (55°)- >(-10°)-> 0 CAKeyframeAnimation *rotationAnimation2 = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation.z"]; rotationAnimation2.values = @[[NSNumber numberWithFloat: M_PI_4 ], [NSNumber numberWithFloat:Radians(10) + M_PI_4 ], [NSNumber numberWithFloat:-Radians(10) ], [NSNumber numberWithFloat:0] ];
CAAnimationGroup *transformGroup2 = [CAAnimationGroup animation]; transformGroup2.animations = [NSArray arrayWithObjects:rotationAnimation2,translationAnimation, nil]; transformGroup2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; transformGroup2.duration = kStep4Duration; transformGroup2.delegate = self; transformGroup2.removedOnCompletion = YES; [_bottomLineLayer addAnimation:transformGroup2 forKey:nil];
} |
最终效果图:
finished animation.gif
本篇文章讲解结束!
代码点此链接下载:https://github.com/WZF-Fei/ZFChangeAnimation
http://cxy.liuzhihengseo.com/465.html
问啊-定制化IT教育平台,牛人一对一服务,有问必答,开发编程社交头条 官方网站:www.wenaaa.com 下载问啊APP,参与官方悬赏,赚百元现金。
QQ群290551701 聚集很多互联网精英,技术总监,架构师,项目经理!开源技术研究,欢迎业内人士,大牛及新手有志于从事IT行业人员进入!