牛顿摆的实现

本文由我们团队的刚毕业的帅小伙真帅童鞋在学校写的一个小动画。发出来分享一下。


做了一个小动画,放上来请大家拍砖指教。

牛顿摆大家应该都不陌生的吧,其实用代码实现起来,挺简单的,下边我们就来实际的写一下这个动画。

�效果图

0 动画拆分

这个动画看起来并不是很难,由6根线,5个圆组成,其实拆分来看的话,其中中间的三根线和最上边的一根线是不用动的,做动画的只是两边的两条线和两个圆,其中,左边的动画来说,就是先摆动起来,然后回落,当落下来的时候,右边的开始向上摆动,然后回落,当落下来的时候,左边的开始摆动,以此循环。

也就是说,我们只需要控制两个动画,然后让其交替进行即可。

单个动画拆分

对于左边动画来说,是由两个动画组成的:线的动画,圆的动画

  • 线的动画:对于左边的额线来说,只是绕着上方的端点旋转了45°

  • 圆的动画:对于左边的圆来说,也只是沿着一个以左边线为长度的弧,走了45°

两者结合起来就形成了,线和球是在一起摆动的视觉效果,但其实在实现的过程中,他们是分开的

1 动画的实现

在该例子中,所有的图形都是由CAShaplayer绘制的。

1.1 绘制线

首选,我们需要绘制6条线,来构成牛顿摆的基本图形,下边我们来看代码,

CAShapeLayer * layer = [CAShapeLayer layer]; //创建一个layer
CGMutablePathRef path = CGPathCreateMutable(); //初始化一个路径
CGPathMoveToPoint(path, nil, startPoint.x, startPoint.y); //移动到开始的地方
CGPathAddLineToPoint(path, nil, endPoint.x, endPoint.y); //划线,画到结束的点
layer.path = path; // 指定layer的Path
layer.lineCap = kCALineCapRound; //设置线端点的形状
layer.lineWidth = self.lineWithd; //设置线宽
layer.strokeColor = [UIColor redColor].CGColor; //设置线的颜色 

这是绘制一条线我们所需要做的工作,其中,我们要指定这个线从哪开始,到哪结束,其中关于lineCap,这里有张图可以很好理解这是个什么玩意

Line Cap

其实我们可以封装一个方法循环来创建这些个线条,这样,我们的界面会看起来像这个样子

Line

1.2 绘制圆

同上边绘制线一样,我们需要绘制5个圆,看代码

    CAShapeLayer * cycleLayer = [CAShapeLayer layer]; //创建一个layer
    CGMutablePathRef path = CGPathCreateMutable(); //初始化一个路径
    CGPathAddArc(path, nil, center.x, center.y, radius, 0, M_PI * 2, YES); // 画一个圆
    cycleLayer.path = path; //指定layer的path
    cycleLayer.fillColor = [UIColor grayColor].CGColor; //填充layer的颜色

对于CGPathAddArc来说,参数分别是,路径,形变,圆心的X,圆心的Y,圆的半径,开始角度,结束角度,是否为顺时针

画完之后,我们的界面看起来像是这个样子

这样我们的牛顿摆就画完了,当然,你可以适当的加一些阴影开始这个形状看起来更有立体感。下边我们就可以来做动画了。

2 动画

就像上边我们所说的一样,我们拆开来做这些动画,首先我们先做左边的动画。

2.1.1 左边线的动画

左边的线的动画,就是让左边的线,围绕着上边的而端点,顺时针摆动45°,我们先来看代码。

_leftLineBaseAnimation = [CABasicAnimation animation]; //初始化一个动画
_leftLineBaseAnimation.keyPath = @"transform.rotation.z"; //动画运动的方式,现在指定的是围绕Z轴旋转
_leftLineBaseAnimation.duration = 0.4; //动画持续时间
_leftLineBaseAnimation.fromValue = [NSNumber numberWithFloat:0]; //开始的角度
_leftLineBaseAnimation.toValue = [NSNumber numberWithFloat:M_PI_4/2]; //结束的角度
_leftLineBaseAnimation.timingFunction =[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut]; //动画的运动方式,
_leftLineBaseAnimation.autoreverses = YES; //是否反向移动动画
_leftLineBaseAnimation.delegate = self; //动画的代理
_leftLineBaseAnimation.fillMode = kCAFillModeForwards;//动画结束后的状态

  • 首先我们先来看keyPath这个属性,这个是行是一个字符串,指定了你要让那个CALayer或CALayer的子类的某个可以做动画的属性的值,关于这个值,大家可以在这里

  • fromeValuetoValue都好理解,从一个值,到另一值,这里从0°到45°

  • 我们来看timingFunction这个属性,一图省千言

这里我们能够看出,也就是运动的速度曲线,先快后慢,先慢后快等等,我们也可以用贝塞尔曲线来定制我们自己的运动速度曲线,这样可以实现更加优美的动画。

  • autoreverses 这个就是是否反向动画,也就是,逆序在播放一遍动画,因为这里我们要的效果是先摆上去,然后在摆下来,正好是动画播放一遍然后在反向播放一遍,所以我们指定为YES。

  • fillMode 这个属性是提供了一个动画结束后的状态,是应该移除,还是继续保持,反向等等。

2.1.2 左边圆的动画

我们来看左边圆的动画

CGMutablePathRef path  = CGPathCreateMutable(); // 创建一个路径
CGPathAddArc(path, nil, 0, 10, self.height - 20, M_PI_2, M_PI_2+M_PI_4/2, NO); //这里的圆心我们制定左边第一条线的上边的端点,长为线的长度,角度为45°
_leftCycleKeyframeAnimation = [CAKeyframeAnimation animation]; //创建一个动画
_leftCycleKeyframeAnimation.keyPath = @"position"; //我们要移动小球的位置,所以keyPath中我们要改变小球的position
_leftCycleKeyframeAnimation.path = path; //制定动画路径
_leftCycleKeyframeAnimation.duration = 0.4f; //动画持续时间
_leftCycleKeyframeAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; //动画的速度曲线
_leftCycleKeyframeAnimation.autoreverses = YES; // 是否反向运动
_leftCycleKeyframeAnimation.fillMode = kCAFillModeForwards; //动画结束后的状态
_leftCycleKeyframeAnimation.delegate = self;
[_leftCycleKeyframeAnimation setValue:@"left" forKey:@"left"]; //key-value,用于在代理中监听动画

关键帧动画,可以说是指定了一个动画路径,使得动画按照一个路径来进行动画,相对于基础动画来说,关键帧动画更加细腻,基础动画只能提供一个点到另一个点的动画,中间过程要系统自动补全,但关键帧动画可以完整的定制动画的运动轨迹,使得动画更加细腻平滑,效果也更加好看

右边的动画和左边的动画类似,详细的就不说了,贴一下代码。

右边线

_rightLineBaseAnimation = [CABasicAnimation animation];
_rightLineBaseAnimation.keyPath = @"transform.rotation.z";
_rightLineBaseAnimation.duration = 0.4;
_rightLineBaseAnimation.fromValue = [NSNumber numberWithFloat:0];
_rightLineBaseAnimation.toValue = [NSNumber numberWithFloat:-M_PI_4/2];
_rightLineBaseAnimation.timingFunction =[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut];
_rightLineBaseAnimation.autoreverses = YES;
_rightLineBaseAnimation.fillMode = kCAFillModeForwards;
_rightLineBaseAnimation.delegate = self;

右边圆

CGMutablePathRef path = CGPathCreateMutable();
CGPathAddArc(path, nil, 90, 10, self.height - 20, M_PI_2,M_PI_2-M_PI_4/2, YES);
_rightCycleKeyframeAnimation = [CAKeyframeAnimation animation];
_rightCycleKeyframeAnimation.keyPath = @"position";
_rightCycleKeyframeAnimation.path = path;
_rightCycleKeyframeAnimation.duration = 0.4f;
_rightCycleKeyframeAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
_rightCycleKeyframeAnimation.autoreverses = YES;
_rightCycleKeyframeAnimation.fillMode = kCAFillModeForwards;
_rightCycleKeyframeAnimation.delegate = self;
[_rightCycleKeyframeAnimation setValue:@"right" forKey:@"right"];

来来来,我们把左右动画结合起来,让左边动画先开始,然后结束了之后,在开始右边的动画,右边的动画结束了,在开始左边的动画,然后循环进行,那我们怎么能检测到动画结束呢,还记得我们在关键帧动画中设置的代理么,那么在动画结束后,代理方法- animationDidStop: finished: 会被调用,在这里,我们可以动过kvc的方式拿到我们在上述左右关键帧动画中设置的相关参数,然后来控制左右动画的运行,代码看起来是这个样子的

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
    if ([[anim valueForKey:@"left"] isEqualToString:@"left"]) {
        
        [self doRightAnimation];
    } else if([[anim valueForKey:@"right"] isEqualToString:@"right"])
    {
        [self doLeftAnimation];
    }
}

好了,现在动画写完了,我们来运行一下,看看效果怎么样,来,喝口水,深呼吸,按下command + r 盯着模拟器,然后出现了这么个玩意

这这这,这是啥,这是啥,有瑕疵啊

一定是有什么不对的地方,恩,一定有什么不对 ( ˙-˙ )

2.3 anchorPoint & bounds & position & frame的关系

上边之所以会出现这种情况,主要是我们没有认清楚 anchorPoint bounds position frame三者之间的关系导致的,可以从上述代码中看到,我们并没有设置四者中的任何一个值。但我们的动画,和这四个值,关系还是很大的,下边我们就来一一分析这四者之间的关系

  • anchorPoint

锚点,这是个什么玩意,举个例子,桌子上有一张纸,你用一个手指头按着他,当他不动的时候,他就在那里,不增不减,当你要旋转他的时候,那就要看你按住的那个点的位置。动画中的旋转也是这个样子,这个锚点,决定了你的旋转。

在这个图中我们可以看到,默认的锚点是(0.5,0.5),也就是layer的中心位置,当围绕着锚点旋转的时候,不同的锚点,旋转效果是不一样的

  • bounds

The bounds rectangle is the origin and size of the layer in its own coordinate space. When you create a new standalone layer, the default value for this property is an empty rectangle, which you must change before using the layer. The values of each coordinate in the rectangle are measured in points.

官方文档是这么说的,bounds这个属性想必大家都不陌生,我们把position和frame看完,在说他们的关系

  • position

The layer’s position in its superlayer’s coordinate space. Animatable.

The value of this property is specified in points and is always specified relative to the value in the anchorPoint property. For new standalone layers, the default position is set to (0.0, 0.0). Changing the frame property also updates the value in this property.

也就是说,position是这个layer在superLayer中的位置,创建一个layer的时候,默认值是0 和frame anchorPoint都有关系,我们在来看frame

  • frame

The frame rectangle is position and size of the layer specified in the superlayer’s coordinate space. For layers, the frame rectangle is a computed property that is derived from the values in thebounds, anchorPoint and position properties. When you assign a new value to this property, the layer changes its position and bounds properties to match the rectangle you specified. The values of each coordinate in the rectangle are measured in points.

官方文档说,这个属性也是在superlayer‘s坐标系中的位置,对于layer来说,其是一个计算属性,由bounds anchorPoint position 计算而来,当指定一个frame的时候,系统会重新计算position和bounds来改变layer的位置,所以说,frame bounds anchorPoint position 这几个值是有非常紧密的联系的,下边我们就来探究一下,究竟有什么联系

我们回过头先看anchorPoint的第一张图

第一组数据

frame = (40, 60, 120, 80);

bounds = (0, 0, 120, 80);

anchorPoint = (0.5, 0.5);

position = (100, 100);

第二组数据

frame = (40, 60, 120, 80);

bounds = (0, 0, 120, 80);

anchorPoint = (0, 0);

position = (40, 60);

上边说,四者是有联系的,而且frame和position都是在父坐标系中的位置,而bounds的宽和高是和frame的宽高一样的,那就很好解释了

frame.x = position.x - archonPoint.x * bounds.size.with

frame.y = position.y - archonPoint.y * bounds.size.height

那我们这样的想法对不对呢,我们还是写代码来看一下

CALayer * aLayer = [CALayer layer];
aLayer.backgroundColor = [UIColor redColor].CGColor;
aLayer.position = CGPointMake(200, 100);
aLayer.anchorPoint = CGPointMake(0.2, 0.2);
aLayer.bounds = CGRectMake(0, 0, 100, 150);
[self.view.layer addSublayer:aLayer];
NSLog(@"%@",NSStringFromCGRect(aLayer.frame));

当我们这样写的时候,根据上边的公式,应该能计算出来,frame.x = 180 frame.y = 70。那结果到底对不对呢,运行一下结果还真是。其实在日常开发过程中,我们一般都是先写好布局,然后设置锚点,在做动画,这样的时候,一运行就会发现,到处各种界面元素到处乱飞,根本就不是想要结果,这个时候,我们可以这么做,先设置锚点,然后在重设一遍frame,这样系统就能自动计算出position,这样的话,极大的简化了我们在动画中的布局问题

就拿上边的竖线摆动的话,我们可以看出,左边的动画和左边的圆,简单的话我们都可以直接把frame设置成(0, 0, superLayer.width, superLayer.height) 这样的话,我们想怎么运动,直接设置相应的锚点即可。

self.leftLine.anchorPoint = CGPointMake(0.1, 0);
self.leftLine.frame = CGRectMake(0, 0, self.width, self.height);
    
self.leftCycle.anchorPoint = CGPointMake(0, 0.9);
self.leftCycle.frame = CGRectMake(0, 0, self.width, self.height);
    
self.rightLine.anchorPoint = CGPointMake(0.9, 0);
self.rightLine.frame = CGRectMake(0, 0, self.width, self.height);
    
self.rightCycle.anchorPoint = CGPointMake(0.9, 0.9);
self.rightCycle.frame = CGRectMake(0, 0, self.width, self.height);

我们重新设置一遍anchorPointframe 把他们放在我们想要的位置,以及想要的锚点上,我们再来试一下

�效果图

嗯,终于达到了我们想要的结果。

总结

其实,只要理解了 anchorPoint bounds position frame的关系,把我们想要做动画的控件摆放在合理的位置,在加上强大的CAAnimation,把复杂的动画拆分来看,在把简单的动画组合起来,我们可以做出很多很酷炫的动画。

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

推荐阅读更多精彩内容

  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 5,094评论 5 13
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 8,462评论 6 30
  • 转载:http://www.jianshu.com/p/32fcadd12108 每个UIView有一个伙伴称为l...
    F麦子阅读 6,153评论 0 13
  • 原文来自:http://www.cnblogs.com/benbenzhu/p/3615516.html?utm_...
    小如99阅读 431评论 0 0
  • 最爱的西瓜,再加上我最喜欢的苏打,就变成了西瓜苏打。今天只画了西瓜,是因为苏打不会画。为什么不会画?因为很复杂。为...
    西瓜苏打阅读 148评论 0 0