手把手教你使用Core animation 做动画

来源:Airfei

链接:http://www.jianshu.com/p/1e2b8ff3519e

最近在技术群里,有人发了一张带有动画效果的图片。觉得很有意思,便动手实现了一下。在这篇文章中你将会学到Core Animation显式动画中的关键帧动画、组合动画、CABasicAnimation动画。先上一张原图的动画效果。

本文要实现的效果图如下:

把原动画gif动画在mac上使用图片浏览模式打开,我们可以看到动画每一帧的显示。从每一帧上的展示过程,可以把整体的动画进行拆分成两大部分。

第一部分(Part1)从初始状态变成取消状态(图片上是由横实线变成上线横线交叉的圆)。

第二部分(Part2)从取消状态变回初始状态。

下面我们先详细分析Part1是怎么实现的。根据动画图,把Part1再细分成三步。

Step1 : 中间横实线的由右向左的运动效果。这其实是一个组合动画。是先向左偏移的同时横线变短。先看一下实现的动态效果。

向左偏移—使用基本动画中animationWithKeyPath键值对的方式来改变动画的值。我们这里使用position.x,同样可以使用transform.translation.x来平移。

改变横线的大小—使用经典的strokeStart和strokeEnd。其实上横线长度的变化的由strokeStart到strokeEnd之间的值来共同来决定。改变strokeEnd的值由1.0到0.4,不改变strokeStart的值。横线的长度会从右侧方向由1.0倍长度减少到0.4倍长度。参见示意图的红色区域。

-(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是一样的。

-(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。画个示意图来分析动画路径。示意图如下:

整个path路径是由三部分组成,ABC曲线、CD圆弧、DD′圆。

使用UIBezierPath的方法

- (void)appendPath:(UIBezierPath *)bezierPath;

把三部分路径关联起来。详细讲解思路。

• ABC曲线就是贝塞尔曲线,可以根据A、B、C三点的位置使用方法

//endPoint 终点坐标 controlPoint1 起点坐标

//controlPoint2 起点和终点在曲线上的切点延伸相交的交点坐标

- (void)addCurveToPoint:(CGPoint)endPoint

         controlPoint1:(CGPoint)controlPoint1

controlPoint2:(CGPoint)controlPoint2;

二次贝塞尔曲线示意图如下:

其中control point 点是从曲线上取 start point和end point 切点相交汇的所得到的交点。如下图:

首先C点取圆上的一点,-30°。那么

CGFloat angle = Radians(30);

C点坐标为:

//C点

   CGFloat endPointX = self.center.x + Raduis * cos(angle);

   CGFloat endPointY = kCenterY - Raduis * sin(angle);

A点坐标为:

//A点 取横线最右边的点

   CGFloat startPointX = self.center.x + lineWidth/2.0 ;

   CGFloat startPointY = controlPointY;

control point 为E点:

//E点 半径*反余弦(30°)

   CGFloat startPointX = self.center.x + Raduis *acos(angle);

   CGFloat startPointY = controlPointY;

• CD圆弧的路径使用此方法确定

(instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;

关于弧度问题,UIBezierPath的官方文档中的这张图:

StartAngle 弧度即C点弧度,EndAngel弧度即D点弧度。

CGFloat StartAngle = 2 * M_PI - angle;

CGFloat EndAngle = M_PI + angle;

• DD′圆的路径和上面2一样的方法确定。

StartAngle 弧度即D点弧度,EndAngel弧度即D′点弧度。

CGFloat StartAngle = M_PI *3/2 - (M_PI_2 -angle);

CGFloat EndAngle = -M_PI_2 - (M_PI_2 -angle);

下面部分代码是所有path路径。

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的长度。但是贝塞尔曲线长度怎么计算?使用下面方法:

//求贝塞尔曲线长度

-(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;

}

计算贝塞尔曲线所在的比例为:

CGFloat orignPercent = [self calculateCurveLength]/[self calculateTotalLength];

初始的strokeStart = 0、strokeEnd = orignPercent。

最终的stokeStart = ?

//结果就是贝塞尔曲线长度加上120°圆弧的长度与总长度相比得到的结果。

CGFloat endPercent =([self calculateCurveLength] + Radians(120) *Raduis ) / [self calculateTotalLength];

实现动画的代码为

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];

效果图为:

阅读 18148 投诉

写留言

 

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

推荐阅读更多精彩内容