A GUIDE TO IOS ANIMATION 2.0
A_Guide_to_iOS_Animation_2.0
User Manual:
Open the PDF directly: View PDF
.
Page Count: 139
| Download | |
| Open PDF In Browser | View PDF |
给
更好的你
ii
终于有时间更新第二版了
如果你还能看到这段话
那真是真爱了
我在屏幕这边给您鞠一躬
你们的支持是本书继续更新的润滑剂
自打编写之初我就坚持
这必须是一本精致的、视觉优先的电子书
每个人心灵深处都有一种创造欲
享受那种从自己手底打磨出一件美好事物的愉悦的感觉
这本书的创作过程就是这样
更好的第二版 送给更好的你
iiii
创作历程
本书的第一版是我业余时间完成的。当时我还在锤子科技上
班。上班一族都有体会,尤其是作为软件工程师这一群体,白天
已经对着电脑写了一天代码,晚上回到家出于本能是拒绝碰代码
的。外加美食和美剧的诱惑,导致在创作第一版的 72 天里,我
有半个月的时间因偷懒而没有推进写书的进度。更疲劳的是,我
需要花费很长时间制作篇幅并不长的素材,几百个纯手工制作的
素材也是这本书最大耗时之处。但好在,我挺过来了。
第二版。第二版原本打算在 12 月份更新的。但还是拖了 2
个月。当时我已经去了另一家公司,然而巨大的工作压力让我呆
了 2 个月就毅然辞职了,这才给第二版的更新带来了时间上的
可能性。
在这本电子书中,我力图通过自己的尝试向所有软件工程师
亦或所有有意出书的朋友证明:如果你有想法,赶紧行动吧。坦
率地讲,一开始我认定写书这种事情离我太远,自己何德何能去
指导别人。但后来我意识到,其实我们所处的社会就是一个分享
的社会。没有人是天生就会写书的,作家亦是如此,更何况作为
一个非文字工作者的软件工程师。如今我已迈出这一步,相信你
也可以。你的能力永远超乎你想象(不是红牛软广)。
KittenYang
iii
iii
目录
第一章:序言
感谢
创作历程
第二章:玩转贝塞尔曲线
KYAnimatedPageControl
GooeySlideMenu
QQ 未读气泡的拖拽交互
LiquidLoader
· · · · · · · · ·
· · · · · · · · ·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
ii
iii
6
13
25
28
第三章:Core Animation
模仿 Twitter 启动动画
· · · · · · · · · 35
圆圈遮罩转场动画
· · · · · · · · · 44
任意位置圆圈放大转场动画 · · · · · · · · · 50
Game Center 起泡晃动效果
· · · · · · · · · 50
图片弹跳切换动画
· · · · · · · · · 51
下载按钮动画
· · · · · · · · · 56
一个 loading 动画
· · · · · · · · · 65
第四章:动画中的数学
InteractiveCard
锤子邮件下拉刷新动画
模仿 tvOS 卡片悬浮扭动效果
· · · · · · · · · 71
· · · · · · · · · 76
· · · · · · · · · 84
第五章:自定义属性动画
粘性菜单
· · · · · · · · · 91
第六章:其他效果
重力回弹的锁屏界面
· · · · · · · · · 107
UIKitDynamics
· · · · · · · · · 114
下雪效果
· · · · · · · · · 122
点赞水花溅起效果
· · · · · · · · · 125
3则 CAReplicatorLayer loading 动画· · · · · · · · ·128
iv
2
玩转贝塞尔曲线
5
⾸先⾮常感谢你能购买正版。你们的⽀持是我给我最⼤的动⼒。
提笔写下第⼀个字,我犹豫了很久,犹豫
该以什么⽅式,什么内容开头。毕竟在我看
来,这是⼀个具有颇具仪式感的时刻,这意味
着这件将贯穿我⼀⽣的事情总算起步了。写书
和写博客的感觉完全不⼀样。⼀本完整的书籍需要考虑更多整体上的连贯性,
语⾔的组织,内容的合理编排来引导读者很顺畅地进⼊你的世界,⽽不像博
客,可以⼀篇⼀篇硬切。好在,⼀切都已踉跄着起步了,虽谈不上万事俱备,
但好⽍已经出发。在这个出发的地⽅,我想留下⼀句话,⽅便时刻提醒我⾃
⼰:「不要因为⾛得太远,就忘了当初为什么出发」。
第⼀章,我们先来聊聊贝塞尔曲线。贝塞尔曲线的发明
⼈是法国雷诺汽车的⼯程师⽪埃尔·贝塞尔。当年他把
贝塞尔曲线应⽤在了雷诺汽车的设计上。贝塞尔曲线
的出现可以说对计算机图形学的发展产⽣了巨⼤的推
动作⽤。我们现在得以在电脑上使⽤ Flash , IIlstrator ,
6
CoralDRAW 和 Photoshop 上制作优美的图形,这其中都离不开贝塞尔曲线
的功劳。
原 理 铺 垫 : 给 定 n + 1 个 数 据 点 , p 0( x 0 , y 0) . . .
p n( x n , y n) ,
⽣ 成 ⼀ 条 曲 线 , 使 得 该 曲 线 与 这 些 点 所 连 结 的 折 线 相 近。
在数学中,这属于逼近问题。在⼏
何中,可以形象地理解为先⽤折线
段连接这些数据点,勾勒出图形的⼤致
轮廓,然后再⽤光滑的曲线去尽可能接
近地拟合这条折线。
在 本 章 的 第 ⼀ 节 中 , 我 将 以 KYAnimatedPageControl 为 例 , 向 你 介 绍
贝塞尔曲线在实际⽣产中的应⽤。你可以通过点击下⽅的
M OVIE 2.1, 查 看 这 个 d e m o
M OVIE 2.1 ⼩球拖拽形变效果
的最终效果。
初看这个效果,直观的感受就是⼩
球发⽣了形变。所以⼀个可⾏的做法
是:我们⽤四条贝塞尔曲线「拼」出这
7
代码来⾃ KYAnimatedPageControl
个⼩球的形状,注意是「拼」出,⽽不是⼀下⼦完整地画出来。有了这
四条单独的曲线,然后,我们只需要单独控制每条贝塞尔曲线的形状,
实 时 调 ⽤ l a y e r 的 [self setNeedsDisplay] 以 重 绘 (void)drawInContext:(CGContextRef)ctx ⽅ 法 , 就 可 以 间 接 地 实 现 控
制⼩球形状的⽬的了。
K EYNOTE 2.1 如何「拼」出⼩球
如 左 侧 K EYNOTE 2.1 所 ⽰ , ⼩ 球 由 弧
AB,弧BC,弧CD,弧DA 组成。
其中 弧AB 是⼀条由 A,B 加两个控制
点 C1,C2 ⼀共四个点绘制的三次贝塞
尔曲线。其他弧线段同理。
⼩球由四段 1/4 圆弧「拼」成,连接完成之后向内填
充颜⾊。
为了⽅便传达理念,我以 Keynote 的
形式展⽰了这⼀思路。你可以逐帧点
击 下 ⽅ 的 K EYNOTE 2.2 进 ⾏ 预 览 。 双
Keynote 2.1 ⼩球各点运动轨迹
指 Pinch 可以放⼤观看。
那么问题来了,这些点应该以⼀个
什么样的规律运动呢?
为了⽅便计算各个点的坐标,我们
可以先引⼊⼀个外接矩形,也就是你在
8
通过各个点的不同规律的运动,产⽣变化的形状
右侧 Keynote 中看到的那个被虚线框起来的⿊⾊矩形。
⾸先计算出这个外接矩形的位置。origin 根据中⼼点的 x(y) 减去宽
度(⾼度)的 1/2 获得。
//outsideRectSize
外接矩形边⻓
CGFloat origin_x = self.position.x - outsideRectSize/2 + (progress - 0.5)*(self.frame.size.width - outsideRectSize);
CGFloat origin_y = self.position.y - outsideRectSize/2;
self.outsideRect = CGRectMake(origin_x, origin_y, outsideRectSize, outsideRectSize);
其次我们还需要判断当前是向左滑还是向右滑,左滑的时候 B 动 D
不动;右滑的时候 D 动 B 不动。
//只要外接矩形在左侧,则改变B点;在右边,改变D点
if (progress <= 0.5) {
self.movePoint = POINT_B;
NSLog(@"B点动");
}else{
self.movePoint = POINT_D;
NSLog(@"D点动");
}
9
现在有了矩形的位置,接下来我们计算关键点的坐标就可以完全基于
这个矩形做为参考了。为了便于对照代码,我⽤ Keynote
画出了⽰意
图,⽅便你进⾏对照。
-(void)drawInContext:(CGContextRef)ctx{
CGFloat offset = self.outsideRect.size.width / 3.6;
CGFloat movedDistance = (self.outsideRect.size.width * 1 /
6) * fabs(self.progress-0.5)*2;
CGPoint rectCenter = CGPointMake(self.outsideRect.origin.x
+ self.outsideRect.size.width/2 , self.outsideRect.origin.y +
self.outsideRect.size.height/2);
CGPoint pointA = CGPointMake(rectCenter.x
,self.outsideRect.origin.y + movedDistance);
10
CGPoint pointB = CGPointMake(self.movePoint == POINT_D ?
rectCenter.x + self.outsideRect.size.width/2 : rectCenter.x +
self.outsideRect.size.width/2 + movedDistance*2 ,rectCenter.y);
CGPoint pointC = CGPointMake(rectCenter.x ,rectCenter.y +
self.outsideRect.size.height/2 - movedDistance);
CGPoint pointD = CGPointMake(self.movePoint == POINT_D ?
self.outsideRect.origin.x - movedDistance*2 :
self.outsideRect.origin.x, rectCenter.y);
CGPoint c1 = CGPointMake(pointA.x + offset, pointA.y);
CGPoint c2 = CGPointMake(pointB.x, self.movePoint ==
POINT_D ? pointB.y - offset : pointB.y - offset + movedDistance);
CGPoint c3 = CGPointMake(pointB.x, self.movePoint ==
POINT_D ? pointB.y + offset : pointB.y + offset - movedDistance);
CGPoint c4 = CGPointMake(pointC.x + offset, pointC.y);
CGPoint c5 = CGPointMake(pointC.x - offset, pointC.y);
CGPoint c6 = CGPointMake(pointD.x, self.movePoint ==
POINT_D ? pointD.y + offset - movedDistance : pointD.y + offset);
CGPoint c7 = CGPointMake(pointD.x, self.movePoint ==
POINT_D ? pointD.y - offset + movedDistance : pointD.y - offset);
CGPoint c8 = CGPointMake(pointA.x - offset, pointA.y);
//圆的边界
UIBezierPath* ovalPath = [UIBezierPath bezierPath];
[ovalPath moveToPoint: pointA];
[ovalPath addCurveToPoint:pointB controlPoint1:c1
controlPoint2:c2];
[ovalPath addCurveToPoint:pointC controlPoint1:c3
controlPoint2:c4];
[ovalPath addCurveToPoint:pointD controlPoint1:c5
controlPoint2:c6];
[ovalPath addCurveToPoint:pointA controlPoint1:c7
controlPoint2:c8];
[ovalPath closePath];
11
//外接虚线矩形
UIBezierPath *rectPath = [UIBezierPath
bezierPathWithRect:self.outsideRect];
CGContextAddPath(ctx, rectPath.CGPath);
CGContextSetStrokeColorWithColor(ctx, [UIColor
blackColor].CGColor);
CGContextSetLineWidth(ctx, 1.0);
CGFloat dash[] = {5.0, 5.0};
CGContextSetLineDash(ctx, 0.0, dash, 2); //1
CGContextStrokePath(ctx); //给线条填充颜⾊
CGContextAddPath(ctx, ovalPath.CGPath);
CGContextSetStrokeColorWithColor(ctx, [UIColor
blackColor].CGColor);
CGContextSetFillColorWithColor(ctx, [UIColor
redColor].CGColor);
CGContextSetLineDash(ctx, 0, NULL, 0); //2
CGContextDrawPath(ctx, kCGPathFillStroke); //同时给线条和线条
包围的内部区域填充颜⾊
//-----
下所有代码全是为了辅助观察
-----
//标记出每个点并连线,⽅便观察,给所有关键点染⾊ -- ⽩⾊,辅助线颜⾊
-- ⽩⾊
CGContextSetFillColorWithColor(ctx, [UIColor
yellowColor].CGColor);
CGContextSetStrokeColorWithColor(ctx, [UIColor
blackColor].CGColor);
NSArray *points = @[[NSValue
valueWithCGPoint:pointA],[NSValue
valueWithCGPoint:pointB],[NSValue
valueWithCGPoint:pointC],[NSValue
valueWithCGPoint:pointD],[NSValue valueWithCGPoint:c1],[NSValue
valueWithCGPoint:c2],[NSValue valueWithCGPoint:c3],[NSValue
valueWithCGPoint:c4],[NSValue valueWithCGPoint:c5],[NSValue
valueWithCGPoint:c6],[NSValue valueWithCGPoint:c7],[NSValue
valueWithCGPoint:c8]];
12
[self drawPoint:points withContext:ctx];
//连接辅助线
UIBezierPath *helperline = [UIBezierPath bezierPath];
[helperline moveToPoint:pointA];
[helperline addLineToPoint:c1];
[helperline addLineToPoint:c2];
[helperline addLineToPoint:pointB];
[helperline addLineToPoint:c3];
[helperline addLineToPoint:c4];
[helperline addLineToPoint:pointC];
[helperline addLineToPoint:c5];
[helperline addLineToPoint:c6];
[helperline addLineToPoint:pointD];
[helperline addLineToPoint:c7];
[helperline addLineToPoint:c8];
[helperline closePath];
CGContextAddPath(ctx, helperline.CGPath);
CGFloat dash2[] = {2.0, 2.0};
CGContextSetLineDash(ctx, 0.0, dash2, 2);
CGContextStrokePath(ctx); //给辅助线条填充颜⾊
}
//
在 point 位置画⼀个点,⽅便观察运动情况
-(void)drawPoint:(NSArray *)points
withContext:(CGContextRef)ctx{
for (NSValue *pointValue in points) {
CGPoint point = [pointValue CGPointValue];
CGContextFillRect(ctx, CGRectMake(point.x - 2,point.y 2,4,4));
}
}
代 码 中 的 (CGContextRef)ctx 字 ⾯ 意 思 是 指 上 下 ⽂ , 你 可 以 理 解 为
⼀块全局的画布。也就是说,⼀旦在某个地⽅改了画布的⼀些属性,之
13
后任何地⽅使⽤该画布属性都是改了之后的。⽐如上⾯在 //1 中把线条
样式改成了虚线,那么在下⽂ //2 中如果不恢复成连续的直线,那么画
出来的依然是虚线样式。
第⼀个 demo 到这⾥就告⼀个段落了。你可以去我的 Github 主页找
到 A - G U I D E - TO - i O S - A N I M AT I O N 这 个 r e p o , ⾥ ⾯ 有 本 册 电 ⼦ 书 所 有 d e m o
的源码可供学习,并且提供了 Swift 版本。本节的源码参见
AnimatedCircleDemo。当然我还是强烈建议你亲⾃动⼿写⼀遍。以我局限
的⾃学经历来说,很多东西看的时候以为都会了,但只有真的上⼿才会
暴露好多知识漏洞。如果你现在想接着看下⼀个 demo 的话,你可以继
续。但我还是建议你别急。
这 ⼀ 回 我 们 来 拆 解 G o o e y S l i d e M e n u 。 效 果 如 M OVIE 2.2
。授⼈与鱼不如授⼈与渔。在介绍这个动画之前,我觉得
我有必要向你分享⼀下我做动画的⼀般性原则。因为如果
你不知道这个通⽤的⽅法论,很可能你学会了我教的这
个,但下次你看到其它动画的时侯就不会分析了,那这就是我对你的不
负责。所以,我必须告诉你我做动画时的⼼得。其中⼀条就是:「善于
14
M OVIE 2.2 分镜头2 —— 实现蓝⾊视图边界的反
弹动画
拆解」。即把⼀个复杂的动画分解为
⼏个分动画,然后再把这些分动画逐
⼀解决。
这是我在看过⼤量动画并亲⼿实践
之后发现的⼀条规律。然⽽有⼀天,
我突然意识到,这条规律不仅仅适⽤
于做动画,这其实是⼀条普世价值观。任何宏观上复杂的事情,都可以
通过⽡解的⽅式细分成⼀个个⼩的 Task ,然后各个击破。咦,这不就
是当年⽼⽑打江⼭的战术吗?
题外话,其实我练习指弹吉他的过程也深刻悟到了这⼀点。指弹吉他
的难度是⽐普通弹唱⼤很多的,因为仅仅凭借两只⼿要同时担任不同乐
器的⼯作。往往乍⼀看⼀⾸指弹吉他谱⼦就直接想放弃了。但通过分解
的⽅式就可以很好地解决这个问题。分解乐谱之后,我不再需要考虑整
篇还有多长才结束,只要就把属于今天的 Task 完成就⾏了,⽽每天的
Task 又不是很难,所以每天都会很有成就感,⾃然就能顺⽔推⾈地进
⾏下去,直到完成既定⽬标。这又让我联想到了番茄⼯作法,把⼀个任
务分成不同的⼩番茄,每个番茄各个击破,直到完成整个任务。这么看
15
来,很多 solution 本质上的理念都是相同的嘛,莫⾮这就是传说中的真
理?
抱歉,我跑题了。下⾯回到主题。
M OVIE 2.3 GooeySlideMenu
我们先把问题拆解成⼀个淡蓝⾊的
View 从屏幕左侧移⼊,这⼀步成功了,
我们再去考虑实现边界的弯曲。为了保
障不被其他视图遮挡,我决定把它加在
U I W i n d o w 上 。 就 像 M OVIE 2.3 这 样 :
self.frame =
CGRectMake(-keyWindow.frame.size.width/2-EXTRAAREA, 0,
keyWindow.frame.size.width/2+EXTRAAREA,
keyWindow.frame.size.height);
self.backgroundColor = [UIColor clearColor];
[keyWindow insertSubview:self belowSubview:helperSideView];
你 可 能 会 疑 问 这 ⾥ 的 EXTRAAREA 是 什 么 ? 你 可 以 看 ⼀ 下 I MAGE 2.1。
注意到,淡蓝⾊的 SlideMenu 其实并没有全部被蓝⾊填充满,右侧还留
出 了 3 0 p x ( 即 代 码 中 的 EXTRAAREA ) 的 透 明 区 域 。 理 由 很 简 单 , 因 为 如 果
不这么做,发⽣弹性时向右突出的边界就看不到了。⾄于动画就⾮常简
单了,由于 origin 的末状态为 (0,0),所以可以直接⽤ bounds 。
16
[UIView animateWithDuration:0.3 animations:^{
self.frame = self.bounds;
}];
I MAGE 2.1 解释为什么需要留出间隙
拆解的第⼀步我们已经完成。下⾯我
们继续完成拆解的第⼆步 —— 实现边界的
反 弹 。 效 果 参 见 M OVIE 2.4。
这⾥就涉及到了两个技术点 —— CADisplayLink 以及贝塞尔曲线。贝塞尔曲线
已经在前⾯有所介绍,这⾥介绍⼀下 CADisplayLink。
简单地理解, CADisplayLink 就是
⼀个定时器,每隔 1/60 秒(16.66667ms)
刷新⼀次屏幕。使⽤的时候,我们要把
它添加到⼀个 runloop 中,并给它绑定
⼀个 target 和 selector ,才能在屏幕
以 1/60 秒刷新的时候实时调⽤绑定的
⽅法。
17
M OVIE 2.4 分镜头1 —— 蓝⾊视图从左侧移⼊
例如:
self.displayLink = [CADisplayLink displayLinkWithTarget:self
selector:@selector(displayLinkAction:)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop]
forMode:NSDefaultRunLoopMode];
另外,CADisplayLink 中还有⼀些其它属性。
@property(readonly, nonatomic) CFTimeInterval duration;
每帧之间的时间
@property(nonatomic) NSInteger frameInterval;
间隔多少帧调⽤⼀次 selector ⽅法,默认值是 1 ,即每帧都调⽤⼀次。
如果每帧都调⽤⼀次的话,对于 iOS 设备来说那刷新频率就是 60HZ 也就
是每秒 60 次,如果将 frameInterval 设为 2 那么就会两帧调⽤⼀次,也
就是变成了每秒刷新 30 次。
@property(getter=isPaused, nonatomic) BOOL paused;
是否暂停当前的定时器,控制 CADisplayLink 的运⾏。
当我们想结束⼀个 CADisplayLink 的时候,应该调⽤:
- (void)invalidate;
从⽽从 runloop 中彻底删除之前绑定的 target 跟 selector。
由于 CADisplayLink 绑定的⽅法会在每次屏幕刷新时被调⽤,精确度
相当之⾼。正是基于这个特点,CADisplayLink ⾮常适合 UI 的重绘。
18
下⾯又到了算坐标点画贝塞尔曲线的时间
了。我们的⽬标是在 CADisplayLink 绑定的⽅法
-(void)displayLinkAction:(CADisplayLink
*)dis 中 实 现 重 绘 。 这 次 的 计 算 难 度 ⽐ 之 前 粘
性⼩球 的要⼩很多,因为这⾥我们只需要⼀条
贝塞尔曲线,⽽且只需要⼀个控制点。
⼀图胜千⾔。左图中红⾊的点表⽰控制
点,⿊⾊的两个点表⽰唯⼀⼀条贝塞尔曲线的两个端点,其余线段均⽤
直线连接。
接下来的问题就是,如何控制
红 点 的 运 动 ? G ALLERY 2.1 很 好 地 说 明
了红点⾊运动轨迹。如下:当 Menu 弹
出时,那么红点从先 O 点运动到 P
点,再从 P 点运动到 Q 点,最后从 Q
点运动到 O 点;反之,当 Menu 隐藏
时,O -> Q -> P -> O.
那么问题就化归到了数学问题:
19
G ALLERY 2.1 控制点的运动轨迹演⽰
“ 如何产⽣⼀组变化的数值,O 增加到某个正数,再从这个正数(也就是最⼤值
P 点)递减到⼀个负数,最后从这个负数(也就是最⼩值 Q 点)递增到 O ?”
这⾥介绍两个思路:
✤
Layer ⾃定义 Property 的动画。
✤
辅助视图。
因为我们的⽬标很明确,就是需要产⽣⼀组变化的数值,这个数值满
⾜上⾯所述的先递增再递减的规律,⾄于这个实现的过程就可以是上⾯
提到的两种⽅案。考虑到⽅案⼀我会在之后单独拿出⼀章来介绍,所以
M OVIE 2.5 利⽤两个辅助视图
创建弹性动画
这⾥我先介绍辅助视图这个技巧。说
起这个思路,还得多亏我看到了这篇
博 客 : Recreating Skype's Action Sheet Animation。
效 果 请 看 M OVIE 2.5 。
没错,我们创建了两个辅助视图,设
置起点和终点都⼀样,利⽤弹性动画
天⽣的回弹特性,我们只要赋予两个
20
辅助视图以不同的动画参数,并且实时计算出两个辅助视图的横坐标 X
之差,就可以间接地得到⼀组从 0 增⾄⼀个正数后,递减⾄⼀个负数,
最后再回到 0 的数据。
K EYNOTE 2.3 慢动作解释两个辅助视图的
作⽤
你 也 可 以 点 击 右 侧 的 K EYNOTE
2.3 明 ⽩ 我 的 意 思 , 即 :
『创建两个辅助视图,设置起点
和终点都⼀样,利⽤弹性动画⾃⾝的
震荡特点,赋予两个辅助视图以不同
的动画参数,实时计算两个辅助视图
的横坐标 X 之差,就可以间接地得
到⼀组从 0 增加到⼀个正数后,递减到⼀个负数,最后再回到 0 的数
据。』
好了,是时候写代码了。
//
-(void)trigger{
if (!triggered) {
[keyWindow insertSubview:blurView belowSubview:self];
[UIView animateWithDuration:0.3 animations:^{
self.frame = self.bounds;
21
}];
[self beforeAnimation];
[UIView animateWithDuration:0.7 delay:0.0f
usingSpringWithDamping:0.5f initialSpringVelocity:0.9f
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction animations:^{
helperSideView.center =
CGPointMake(keyWindow.center.x,
helperSideView.frame.size.height/2);
} completion:^(BOOL finished) {
[self finishAnimation];
}];
[UIView animateWithDuration:0.3 animations:^{
blurView.alpha = 1.0f;
}];
[self beforeAnimation];
[UIView animateWithDuration:0.7 delay:0.0f
usingSpringWithDamping:0.8f initialSpringVelocity:2.0f
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction animations:^{
helperCenterView.center = keyWindow.center;
} completion:^(BOOL finished) {
if (finished) {
UITapGestureRecognizer *tapGes = [[UITapGestureRecognizer alloc]initWithTarget:self
action:@selector(tapToUntrigger:)];
[blurView addGestureRecognizer:tapGes];
[self finishAnimation];
}
}];
[self animateButtons];
22
triggered = YES;
}else{
[self tapToUntrigger:nil];
}
}
[self beforeAnimation] 和 [self finishAnimation] ⽤ 于 控 制 C A DisplayLink 什么时候应该移除。⽤到了动画的累加计数:每开始⼀个动
画时计数器加 1,每停⽌⼀个动画时计数器减 1,当两个动画都完成时,
计数器为 0,此时移除 CADisplayLink。
//
-(void)beforeAnimation{
if (self.displayLink == nil) {
self.displayLink = [CADisplayLink
displayLinkWithTarget:self
selector:@selector(displayLinkAction:)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop]
forMode:NSDefaultRunLoopMode];
}
self.animationCount ++;
}
//
-(void)finishAnimation{
self.animationCount --;
if (self.animationCount == 0) {
[self.displayLink invalidate];
self.displayLink = nil;
23
}
}
以上⼏处代码只是实现了辅助视图的运动以及 Menu 侧滑的运动,接
下来的代码才是重头戏,因为我们要尝试让边界发⽣回弹。
//CADisplayLink
-(void)displayLinkAction:(CADisplayLink *)dis{
CALayer *sideHelperPresentationLayer
= (CALayer
*)[helperSideView.layer presentationLayer];
CALayer *centerHelperPresentationLayer = (CALayer
*)[helperCenterView.layer presentationLayer];
CGRect centerRect = [[centerHelperPresentationLayer
valueForKeyPath:@"frame"]CGRectValue];
CGRect sideRect = [[sideHelperPresentationLayer
valueForKeyPath:@"frame"]CGRectValue];
diff = sideRect.origin.x - centerRect.origin.x;
[self setNeedsDisplay];
}
//
[self setNeedsDisplay]
- (void)drawRect:(CGRect)rect {
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0, 0)];
[path
addLineToPoint:CGPointMake(self.frame.size.width-EXTRAAREA,
0)];
[path
addQuadCurveToPoint:CGPointMake(self.frame.size.width-EXTRAAREA
, self.frame.size.height)
24
controlPoint:CGPointMake(keyWindow.frame.size.width/2+diff,
keyWindow.frame.size.height/2)];
[path addLineToPoint:CGPointMake(0,
self.frame.size.height)];
[path closePath];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextAddPath(context, path.CGPath);
[_menuColor set];
CGContextFillPath(context);
}
我 在 后 ⾯ 提 到 了 Pr esentati on Layer 的 作 ⽤ — — 即 可 以 实 时 获 取 L aye r
属 性 的 当 前 值 。 ⽽ 我 们 的 这 个 demo 中 要 获 取 的 正 是 这 两 个 辅 助 视 图 实 时
的 X 坐 标 , 从 ⽽ 才 能 计 算 出 di ff。 有 了 这 个 di ff, 我 们 再 调 ⽤ [self setNeedsDisplay]; ( 这 个 ⽅ 法 会 触 发 UIVi ew 的 drawRe c t 或 C A L aye r 的 drawR e ct In C o nt e xt ) , 同 时 在 -(void)drawRect:(CGRect)rect 中 绘 制 边 界 。 曲
线 ⽤ 贝 塞 尔 曲 线 , 其 余 都 是 直 线 , ⽤ addLi neToPoi nt: 就 可 以 解 决 。 唯 ⼀ 需
要 注 意 的 是 控 制 点 的 X 坐 标 需 要 加 上 di ff:
CGPointMake(keyWindow.frame.size.width/2+diff,
keyWindow.frame.size.height/2)
⾄此, 这个 demo 到这⾥也结束了,剩下的就需要靠你动⼿实践
了。亲⾝体验和看⼀遍的效果是完全不同的。源码我已经上传到了
25
Github,你可以去 GooeySlideMenuDemo 看看。当然,再次提醒,Swift 也
⼀并提供了。
这⼀节我们学习了使⽤ CADisplayLink 以及复习了 Presentation Layer
的功能,还复习了绘制贝塞尔曲线知识。
第三⼩节,我们来尝试复刻⼀下⼿机 QQ ⾥那个起泡拖拽
的 交 互 。 见 M OVIE 2.6 。
M OVIE 2.6 ⼿机 QQ 起泡拖拽交互
思路如下:
★
★
UIPanGestureStateChanged
这个交互中,最核⼼的要数计算关键点的通⽤坐标了。⼀图胜千⾔。
下⾯我绘制了⼀幅分析图,⽅便你计算坐标位置,其中 OA ⊥ AB, PB
26
⊥ AB, 且 OA=PB=d/2。 这样⼀来,问题就转化成了⼀个⾼中数学求
点坐标的题⽬了。
我已经在图中计算出六个关键点 A,B,C,D,E,F,G 的坐标位置了。
接下来就是要把数学表达式转化成代码了。⾸先我们创建⼀些需要的变
量:
CGFloat
CGFloat
CGFloat
CGFloat
CGFloat
CGFloat
CGFloat
CGFloat
CGFloat
CGPoint
CGPoint
CGPoint
CGPoint
CGPoint
CGPoint
r1;
r2;
x1;
y1;
x2;
y2;
centerDistance;
cosDigree;
sinDigree;
pointA; //A
pointB; //B
pointD; //D
pointC; //C
pointO; //O
pointP; //P
27
根据上图中的计算公式,我们不难⽤代码表⽰出这⼏个变量:
x1
y1
x2
y2
=
=
=
=
backView.center.x;
backView.center.y;
self.frontView.center.x;
self.frontView.center.y;
centerDistance = sqrtf((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1));
if (centerDistance == 0) {
cosDigree = 1;
sinDigree = 0;
}else{
cosDigree = (y2-y1)/centerDistance;
sinDigree = (x2-x1)/centerDistance;
}
r1 = oldBackViewFrame.size.width / 2 - centerDistance/self.viscosity;
pointA = CGPointMake(x1-r1*cosDigree, y1+r1*sinDigree); // A
pointB = CGPointMake(x1+r1*cosDigree, y1-r1*sinDigree); // B
pointD = CGPointMake(x2-r2*cosDigree, y2+r2*sinDigree); // D
pointC = CGPointMake(x2+r2*cosDigree, y2-r2*sinDigree);// C
pointO = CGPointMake(pointA.x + (centerDistance / 2)*sinDigree, pointA.y
+ (centerDistance / 2)*cosDigree);
pointP = CGPointMake(pointB.x + (centerDistance / 2)*sinDigree, pointB.y
+ (centerDistance / 2)*cosDigree);
然后,⽤贝塞尔曲线把这些点连起来,就有了粘性⼩球完整绘制。最
后,我们要做的就是在⼩球绑定的⼿势的 Change ⽅法⾥⾯调⽤这个绘
制函数,已达到⼩球的形状随着⼿指的移动⽽变化,就有了粘性的交互
了。
if (ges.state == UIGestureRecognizerStateChanged){
...
//
[self drawRect];
}
28
原理其实⼗分简单,代码已经上传⾄ Github,⽀持 Objective-C 和
Swift 。
这⼀⼩节,让我们来膜拜⼀下 yoavlt 的
项⺫。这个
项⺫是⼀个「圆球」效果的 loading 视图, 我也提
交了 pr 。同样原理另⼀个项⺫是
LiquidFloatingActionButton,是⼀个「圆球」效果的
菜单。我们来看⼀下这两个效果。
M OVIE 2.7 LiquidLoader
M OVIE 2.8 LiquidFloatingActionButton
29
下 ⾯ 我 们 来 看 ⼀ 下 LiquidLoader 是 怎 么 实 现 的 。
⾸先我们来看⼀看架构。
稍 微 解 释 ⼀ 下 。 对 外 使 ⽤ 的 类 就 是 LiquidLoader , 其 中 ⽤ 到 了 LiquidLoadEffect 这 个 类 , 这 个 类 是 个 基 类 , 有 两 个 ⼦ 类 继 承 于 它 — — LiquidLineEffect 和 LiquidCircleEffect , ⽤ 来 实 现 两 种 样 式 。 LiquidLoadEffect 这 个 类 中
⽤ 到 了 LiquittableCircle , 这 个 类 ⽤ 来 绘 制 ⼩ 球 ; 除 此 之 外 , LiquidLoadEffect
中 还 ⽤ 到 了 ⼀ 个 「 引 擎 」 — — SimpleCircleLiquidEngine,这 个 引 擎 的 作 ⽤
就是判断当前静⽌的圆球与运动的圆球之间的距离,从⽽决定是否显⽰
粘连的部分。
具 体 到 代 码 中 。 ⾸ 先 , 国 际 惯 例 , 创 建 ⼀ 个 定 时 器 CADisplayLink
, 绑 定 到 ⼀ 个 ⽅ 法 : update() 。 这 个 ⽅ 法 中 我 们 去 更 新 ⼀ 个 动 态 变 量 ,
取 名 为 key , 根 据 key 我 们 来 移 动 ⼩ 球 的 位 置 , 同 时 ⽤ 「 引 擎 」 来 判 断
30
移动的⼩球和静⽌的⼩球之间的距离。以上就是主干线。下⾯我挖⼏处
细节和你讲讲。
⾸ 先 , 既 然 ⼩ 球 的 位 置 是 通 过 实 时 改 写 center 实 现 的 , 那 么 我 们 怎
么 指 定 动 画 的 曲 线 函 数 呢 ? ⽐ 如 M OVIE 2.7 中 , 仔 细 看 你 会 发 现 ⼩ 球 在
两端有个减速的过程,中间段加速。要实现这样的曲线函数,我们就要
依赖三⾓函数了。
func sineTransform(key: CGFloat) -> CGFloat {
return sin(key * CGFloat(M_PI)) * 0.5 + 0.5
}
通过这个⺴站,我们可以得到如下函数图像:
根 据 ⾼ 中 物 理 知 识 我 们 知 道 , 加 速 度 看 斜 率 , 所 以 图 中 key 取 值 1
和 2 的时候加速度最⼤,此时⼩球正在经过在中点位置。
我们再来看另⼀个细节:
31
circles.each { circle in
if self.moveCircle != nil {
self.engine?.push(self.moveCircle!, other: circle)
}
}
这 段 代 码 也 是 在 update() ⽅ 法 ⾥ , 通 过 遍 历 所 有 静 ⽌ ⼩ 球 , 让 每 个
静⽌的⼩球与运动的⼩球进⾏⽐较,⽐较两球圆⼼之间的距离是否超过
某个事先规定好的阈值。
func isConnected(circle: LiquittableCircle, other: LiquittableCircle) -> Bool {
let distance = circle.center.minus(other.center).length()
return distance - circle.radius - other.radius < radiusThresh
}
原 先 的 项 ⺫ 中 并 没 有 设 置 duration 的 接 ⼝ , 我 在 提 交 的 p r 中 增 加
了这个功能。核⼼代码如下:
override func update() {
switch key {
case 0.0...2.0:
key += 2.0/(duration*60)
default:
key = 0.0
}
}
只 要 让 动 态 变 量 key 每 次 增 加 的 值 等 于 2 . 0 / 总 帧 数 即 可 。 之 所 以 是
2.0 是由三⾓函数决定的,因为上⾯的三⾓函数⼀个周期为 2 。
32
除了上⾯这些之外,更令我⼤开眼界的是作者写的⼀⼤批⼯具类。⾝
体⼒⾏地诠释了什么叫「⼯欲善其事必先利其器」。相信你看过源码也
会有和我⼀样的感触。
好啦,这个项⺫的分析就到这⾥就结束了,你可以在这⾥翻看源码。
33
3
CORE ANIMATION
34
这⼀章节,我将会把 Core Animation 技巧做个总结。当然,你不⽤担
⼼我会像学校的⽼师⼀样很枯燥地向你灌输知识点,这是别⼈的做法。
事实上从我局限的切⾝感受来说,我⼀向认为结果导向的学习⽅法才是
最有效率的:「你要去什么地⽅,就直接去你想去的那个地⽅」。所以
贯穿我整册电⼦书的教学模式都是:「我精⼼挑选⼀些涵盖尽可能多知
识点的例⼦,让你直接接触这些⽣动的(半)成品,期间涉及到了知识
点,我再发散出去详细介绍」。通过这些活⽣⽣的案例让你在实战中了
解动画的相关知识。或许你会想,但是这样不够系统性啊,我担⼼⾃⼰
只是学到了冰⼭⼀⾓。我想说的是,朋友,我还是那句话,「等你都准
备好了,或许你就没有动⼒了」。不⽤那么在意这些形式上的东西,你
想想你多少次⼼⾎来潮地准备好了你认为完备的⼀切,结果没坚持⼏天
就半途⽽废了。正所谓「星星之⽕,可以燎原」。知识从来不
是靠看来的,都是靠找来的。我⾃⾝就是这种学习模式的
受益者。通过这种结果导向的⽅式学到的每⼀个知识点
就像⼀颗颗散落的珍珠,随着你越捡越多,当你有⼀
天捡得差不多了,这时你再回过头去看那些当时看⼏
页就犯困的理论书,你会发现⼀切看起来都是那么顺利,
顺便也从全局范畴上对之前零散的知识点进⾏了⼀次结构性的巩固。然
35
后你就会惊喜地发现,所有知识点就像珍珠被串上了线⼀样,⼀切都联
系起来了。
本章第⼀个例⼦,我们来
M OVIE 3.1 模拟 Twitter 的启动动画
看 看 如 何 实 现 类 似 Tw i t t e r
的启动动画。效果请参见
M OVIE 3.1。说 实 话 , 当 时
我 第 ⼀ 次 打 开 Tw i t t e r 看 到 这 个 效 果 的
时候,I was totally blown away
在开始之前,我们先了解⼀下 Layer 的 mask 属性。
@property(strong) CALayer *mask;
I MAGE 3.1 ⼤⼩为 100*100 的 photoImage
可以发现 mask 也是⼀个 CALayer。所以当我们使⽤时,就需要单独创建⼀
个 CALayer 作为 mask。⽐如我提前准备好了
需 要 遮 罩 的 图 ⽚ 视 图 p h o t o I m a g e — — I MAGE
3.1。 ⼤ ⼩ 为 1 0 0 * 1 0 0 的 U I I m a g e V i e w , 连 好
IBOutlet 。 在 V i e w C o n t r o l l e r . m 的
36
-(void)viewDidLoad ⽅ 法 中 创 建 m a s k , 赋 值 给 p h o t o I m a g e 的 m a s k 属
性。
CALayer *maskLayer = [CALayer layer];
maskLayer.contents = (id)[UIImage imageNamed:@"logo"]
.CGImage; //必须是CGImageRef
maskLayer.frame = CGRectMake(0, 0,
_photoImage.frame.size.width, _photoImage.frame.size.height);
_photoImage.layer.mask = maskLayer;
运⾏之后,可以看到 photoImage 已经被遮罩了。
通过 View Hierarchy 可以看到视图的层
级关系。为了实现开头那个 zoom out 的欢
迎 动 画 , 我 把 我 的 的 思 路 ⽤ G ALLERY 3.1
做了展⽰
。
解释⼀下,⾸先毫⽆疑问⼀开始显⽰
LaunchScreen 。接下来 LaunchScreen 消失了,即将进⼊ NavigationCont r o l l e r 时 , 我 们 在 -(void)viewDidLoad 中 为 其 设 置 遮 罩 , 并 且 把 遮 罩
的位置约束在和 LaunchScreen 中星星同⼀个位置,这样看起来就好像星
星⼀直停在原地。不过虽然星星的位置不动了,但是 LaunchScreen
⼀
旦结束之后就会露出⼀个⿊底(UIWindow), ⽴刻就露馅。解决⽅法也是
37
简单粗暴,只要把 UIWindow 的背景⾊
G ALLERY 3.1 解释启动动画的视图
层级
改成和 LaunchScreen ⼀样的颜⾊就⾏
了。
下⾯展⽰代码。⾸先在
func ap-
plication didFinishLaunchingWithOp-
顺便说⼀句:这些图都是⽤ Keynote
做的。
tions 中 设 置 w i n d o w 的 颜 ⾊ :
self.window!.backgroundColor = UIColor(red: 128/255, green: 0/
255, blue: 0/255, alpha: 1)
然 后 在 -(void)viewDidLoad 中 写 上 如 下 代 码 , 让 m a s k 和
n a v i g a t i o n C o n t r o l l e r. v i e w 在 不 同 时 间 做 放 ⼤ 的 动 画 :
// logo mask
self.navigationController!.view.layer.mask = CALayer()
self.navigationController!.view.layer.mask.contents =
UIImage(named: "logo.png")!.CGImage
self.navigationController!.view.layer.mask.position =
self.view.center
self.navigationController!.view.layer.mask.bounds =
CGRect(x: 0, y: 0, width: 60, height: 60)
//
mask
let transformAnimation = CAKeyframeAnimation(keyPath:
"bounds")
transformAnimation.delegate = self
transformAnimation.duration = 1
transformAnimation.beginTime = CACurrentMediaTime() + 1 //
38
let initalBounds = NSValue(CGRect:
self.navigationController!.view.layer.mask.bounds)
let secondBounds = NSValue(CGRect: CGRect(x: 0, y: 0, width:
50, height: 50))
let finalBounds = NSValue(CGRect: CGRect(x: 0, y: 0, width:
2000, height: 2000))
transformAnimation.values = [initalBounds, secondBounds,
finalBounds]
transformAnimation.keyTimes = [0, 0.5, 1]
transformAnimation.timingFunctions = [CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut), CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)]
transformAnimation.removedOnCompletion = false
transformAnimation.fillMode = kCAFillModeForwards
self.navigationController!.view.layer.mask.addAnimation(transfo
rmAnimation, forKey: "maskAnimation")
//
navigationController.view
UIView.animateWithDuration(0.25,
delay: 1.3,
options: UIViewAnimationOptions.TransitionNone,
animations: {
self.navigationController!.view.transform =
CGAffineTransformMakeScale(1.05, 1.05)
},
completion: { finished in
UIView.animateWithDuration(0.3,
delay: 0.0,
options: UIViewAnimationOptions.CurveEaseInOut,
animations: {
self.navigationController!.view.transform =
CGAffineTransformIdentity
},
completion: nil
)
})
39
然⽽,如果你现在运⾏以上的代码,你会发现仍有不⾜。因为 mask
⼀开始就存在,所以你可以在程序启动时就能透过 mask 看到后⾯的视
图。⽽我们希望的是 mask 后⾯的内容是在 mask 扩⼤的过程中逐渐显⽰
出来的,正如⼀开始效果视频中展⽰的那样。这⾥的 solution 就要善于
「作弊」,这也是我在做动画的过程中深谙的⼀个技巧。我们要做的就
是 在 N a v i g a t i o n C o n t r o l l e r. v i e w 和 m a s k 之 间 再 加 ⼀ 层 背 景 ⾊ 为 ⽩ ⾊ 的 图
层,让这个图层挡住背后的内容,同时这个图层在 mask 动画的同时做
透明度渐变到 0 的动画。
//
NavigationController.view
mask
var maskBgView = UIView(frame:
self.navigationController!.view.layer.frame)
maskBgView.backgroundColor = UIColor.whiteColor()
self.navigationController!.view.addSubview(maskBgView)
self.navigationController!.view.bringSubviewToFront(maskBgView)
//
UIView.animateWithDuration(0.1,
delay: 1.35,
options: UIViewAnimationOptions.CurveEaseIn,
animations: {
maskBgView.alpha = 0.0
},
completion: { finished in
maskBgView.removeFromSuperview()
})
40
下⾯延伸相关知识。⾸先,我们就来谈谈
CAKeyframeAnimation 。
顾 名 思 义 , CAKeyframeAnimation 就 相 当 于 F l a s h ⾥ 的 关 键 帧 动 画 , 如
果你⽤过 Flash 制作动画的话你就知道,如果我们要实现⼀个简单的位
置平移、⼤⼩缩放、形状变换,我们只需要使⽤补间动画就可以实现。
具体操作就是给出动画的起始状态和结束状态两个关键帧,中间的动画
过程只需要设置⼀个补间即可,剩下的事情软件会⾃动完成。⽽这⾥的
起 始 状 态 和 结 束 状 态 的 概 念 , 也 被 沿 ⽤ 到 了 CAKeyframeAnimation ⾥ 所
说的关键帧 。
CAKeyframeAnimation 中 我 们 通 过 k e y P a t h 就 可 以 指 定 动 画 的 类 型 。
⽐ 如 let transformAnimation = CAKeyframeAnimation(keyPath:
"bounds") 中 的 bounds 就 是 指 定 了 动 画 类 型 : 让 layer 的 size 发 ⽣
动 画 。 关 于 k e y P a t h 的 可 选 值 , 你 可 以 查 看 CALayer 的 A P I ⽂ 档 :
/* The bounds of the layer. Defaults to CGRectZero. Animatable.
*/
/** Geometry and layer hierarchy properties. **/
var bounds: CGRect
/* The position in the superlayer that the anchor point of
the layer's
* bounds rect is aligned to. Defaults to the zero point.
Animatable. */
var position: CGPoint
41
/* Defines the anchor point of the layer's bounds rect, as
a point in
* normalized layer coordinates - '(0, 0)' is the bottom
left corner of
* the bounds rect, '(1, 1)' is the top right corner. Defaults to
* '(0.5, 0.5)', i.e. the center of the bounds rect. Animatable. */
var anchorPoint: CGPoint
/* A transform applied to the layer relative to the anchor
point of its
* bounds rect. Defaults to the identity transform. Animatable. */
var transform: CATransform3D
......
类似这⼀些
Animatable 的 属 性 , 都 可 以 作 为 CAAnimation 的 k e y -
P a t h 。 然 后 , 我 们 把 每 个 关 键 帧 的 对 应 参 数 赋 值 给 CAKeyframeAnimation 的 values 属 性 。 代 码 中 , 我 设 置 了 3 个 关 键 帧 ,
transformAnimation.values = [initalBounds, secondBounds, finalBounds]
并 且 设 置 对 应 的 时 间 点 transformAnimation.keyTimes =
[0, 0.5, 1] , 也 就 是 动 画 ⼀ 开 始 时 bounds 处 于
initalBounds 状
态 ; duration ⼀ 半 时 间 时 处 于 secondBounds 状 态 ; 动 画 结 束 时 处 于
finalBounds 状 态 。
42
但 是 我 们 还 要 注 意 。 当 你 给 ⼀ 个 CALayer 添 加 动 画 的 时 候 , 动 画 其
实并没有改变这个 layer 的实际属性。取⽽代之的,系统会创建⼀个原
始 layer 的拷贝。在⽂档中,苹果称这个原始 layer 为 Model Layer ,⽽
这个复制的 layer 则被称为 Presentation Layer 。 Presentation Layer 的
属性会随着动画的进度实时改变,⽽ Model Layer 中对应的属性则并不
会改变。所以如果你想要获取动画中每个时刻的状态,请使⽤ layer 的
func presentationLayer() -> AnyObject!
Hold on a second. 此时如果你只是做了上⾯的步骤,你会发现效果并
不是我们所想的那样。你会发现动画在结束之后突然回到了初始状态。
这 ⾥ 就 可 以 引 出 removedOnCompletion 和 fillMode 了 。
removedOnCompletion 的 官 ⽅ 解 释 是 :
/* When true, the animation is removed from the render tree
once its
* active duration has passed. Defaults to YES. */
也 就 是 默 认 情 况 下 系 统 会 在 duration 时 间 后 ⾃ 动 移 除 这 个 CAKeyframeAnimation
当 remove 了某个动画,那么系统就会⾃动销毁这个
layer 的 Presentation Layer ,只留下 Model Layer 。 ⽽前⾯提到 Model
Layer 的属性其实并没有变化,所以也就有了你前⾯看到的结果,视图在
43
⼀ 瞬 间 回 到 了 动 画 的 初 始 状 态 。 要 解 决 这 种 情 况 , 你 需 要 先 把 removedOnCompletion 设 置 为 false , 然 后 设 置 fillMode 为 kCAFillModeForwards
。 关 于 fillMode , 它 有 四 个 值 :
• kCAFillModeRemoved 这个是默认值 也就是说当动画开始前和动画结束后
动画对layer都没有影响 动画结束后 layer会恢复到之前的状态。
• kCAFillModeForwards 当动画结束后 layer会⼀直保持着动画最后的状态
• kCAFillModeBackwards 这个和 kCAFillModeForwards 是相对的 就是在动画开
始前 你只要将动画加⼊了⼀个layer layer便⽴即进⼊动画的初始状态并等待动
画开始 你可以这样设定测试代码 将⼀个动画加⼊⼀个layer的时候延迟5秒执
⾏ 然后就会发现在动画没有开始的时候 只要动画被加⼊了 layer , layer 便处于
动画初始状态, 动画结束后 layer 会恢复到之前的状态。
• kCAFillModeBoth 理解了上⾯两个 这个就很好理解了 这个其实就是上⾯两个
的合成 动画加⼊后⽴即开始,layer便处于动画初始状态 动画结束后layer保持
动画最后的状态
你 除 了 可 以 设 置 removedOnCompletion 为 false
kCAFillModeForwards
fillMode 为
外 , 这 ⾥ 还 有 个 t r i c k , 就 是 你 可 以 在 addAni-
mation 之 前 显 式 地 把 M o d e l L a y e r 的 对 应 属 性 设 置 为 结 束 时 的 状 态 , 这
样同样也能避免之前动画结束后复位的问题。
但 设 置 removedOnCompletion 和 fillMode 不 是 正 确 的 ⽅ 式 。 正 确
的做法可以参考 WWDC 2011 中的 session 421 - Core Animation Essentials. 为了保证教程的连贯性,我把视频放在了这章的结尾,你可以在这
章结束之后再看这个 session。
44
除此之外,这个 demo 还有⼀个⽐较关键的地⽅在于需要不断地调
delay
才⾏,确保每个动画都能衔接上。所以⼀个好动画离不开⼀堆好
参数。 这⼀节到这⾥也就结束了。源码请见
。OC/Swift are both sup-
ported.
下⼀个案例也是关于 mask 的应⽤,只不过这回套了⼀个
外⾐ —— 转场动画,顺便也可以介绍⼀下它的使⽤。
先 看 效 果 M OVIE
M OVIE 3.2 圆圈遮罩的转场动画
3.2。
尽管核⼼⽤的依然是 CoreAnimation ,但是⾸先我们先来认识下实现
转场动画的基本步骤。
iOS7 开始苹果推出了⾃定义转场
的 API 。从此,任何可以⽤ CoreAnimation 实现的动画,都可以出现在两
个 ViewController 的切换之间。并且实现⽅式⾼度解耦,这也意味着在
保证代码⼲净的同时想要替换其他动画⽅案时只需简单改⼀个类名就可
以了,真正体会了⼀把⾼颜值代码带来的愉悦感。同时,iOS7 还推出了
45
⼿势驱动的转场动画,同样⾼度解耦。想必随着⼤屏 iPhone 的普及,软
件层⾯的交互优化将显得格外具有意义。如今的 App 要是还不⽀持⼿势
滑动返回,就真的太不骄傲了。
苹 果 在 UINavigationControllerDelegate 和 UIViewControllerTransitioningDelegate 中 给 出 了 ⼏ 个 协 议 ⽅ 法 , 通 过 返 回 类 型 就 可 以
很清楚地知
道 各 ⾃ 的 具 体 作 ⽤ 。 你 只 需 要 重 载 它 们 , 然 后 return ⼀ 个 动 画 的 实 例
对 象 , ⼀ 切 都 搞 定 了 。 使 ⽤ 准 则 就 是 : UINavigationController pushViewController 时 重 载
UINavigationControllerDelegate 的 ⽅ 法 ;
UIViewController presentViewController 时 重 载 UIViewControllerTransitioningDelegate 的 ⽅ 法 。
UINavigationControllerDelegate:
- (nullable id )
navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id
) animationController;
- (nullable id )
navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperati
on)operation fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC;
46
UIViewControllerTransitioningDelegate:
- (nullable id ) animationControllerForPresentedController: (UIViewController *)presented presentingController:(UIViewController *)presenting
sourceController: (UIViewController *)source;
- (nullable id ) animationControllerForDismissedController: (UIViewController *)dismissed;
- (nullable id )
interactionControllerForPresentation:(id
)animator;
- (nullable id
)interactionControll
erForDismissal:(id )
animator;
那么接下来就体现了解耦带来的好处了。具体步骤:
1 、 创 建 继 承 ⾃ NSObject 并 且 声 明 UIViewControllerAnimatedTransitioning 的 的 动 画 类 。
2、重载
UIViewControllerAnimatedTransitioning 中 的 协 议 ⽅
法。
UIViewControllerAnimatedTransitioning
@protocol UIViewControllerAnimatedTransitioning
47
// This is used for percent driven interactive transitions, as
well as for container controllers that have companion animations that might need to
// synchronize with the main animation.
- (NSTimeInterval)transitionDuration:(nullable id
)transitionContext;
// This method can only be a nop if the transition is interactive and not a percentDriven interactive transition.
- (void)animateTransition:(id
)transitionContext;
@optional
// This is a convenience and if implemented will be invoked by
the system when the transition context's completeTransition:
method is invoked.
- (void)animationEnded:(BOOL) transitionCompleted;
@end
3、没有了。
K EYNOTE 3.1 演⽰遮罩动画的思路
在准备动⼿实践之前,我依旧先打算
让你过⽬⼀下动画的实现思路。请看 Keyn o t e 演 ⽰ K EYNOTE 3.1。
- (NSTimeInterval)transitionDuration:(id
)transitionContext{
return self.duration;
}
48
- (void)animateTransition:(id
)transitionContext{
self.transitionContext = transitionContext;
ViewController * fromVC = (ViewController *)[transitionContext
viewControllerForKey:UITransitionContextFromViewControllerKey];
SecondViewController *toVC = (SecondViewController *)[transitionContext
viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *contView = [transitionContext containerView];
UIButton *button = fromVC.button;
UIBezierPath *maskStartBP = [UIBezierPath
bezierPathWithOvalInRect:button.frame];
[contView addSubview:fromVC.view];
[contView addSubview:toVC.view];
CGPoint finalPoint;
//判断触发点在那个象限,从⽽计算出覆盖的最⼤半径
if(button.frame.origin.x > (toVC.view.bounds.size.width /
2)){
if (button.frame.origin.y <
(toVC.view.bounds.size.height / 2)) {
//第⼀象限
finalPoint = CGPointMake(button.center.x - 0,
button.center.y - CGRectGetMaxY(toVC.view.bounds)+30);
}else{
//第四象限
finalPoint = CGPointMake(button.center.x - 0,
button.center.y - 0);
}
}else{
if (button.frame.origin.y <
(toVC.view.bounds.size.height / 2)) {
//第⼆象限
49
finalPoint = CGPointMake(button.center.x CGRectGetMaxX(toVC.view.bounds), button.center.y CGRectGetMaxY(toVC.view.bounds)+30);
}else{
//第三象限
finalPoint = CGPointMake(button.center.x CGRectGetMaxX(toVC.view.bounds), button.center.y - 0);
}
}
CGFloat radius = sqrt((finalPoint.x * finalPoint.x) +
(finalPoint.y * finalPoint.y));
UIBezierPath *maskFinalBP = [UIBezierPath
bezierPathWithOvalInRect:CGRectInset(button.frame, -radius, -radius)];
//创建⼀个 CAShapeLayer
toView
path
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.path = maskFinalBP.CGPath; //将它的 path 指定为最终
的 path 来避免在动画完成后会回弹
toVC.view.layer.mask = maskLayer;
CABasicAnimation *maskLayerAnimation = [CABasicAnimation
animationWithKeyPath:@"path"];
maskLayerAnimation.fromValue = (__bridge
id)(maskStartBP.CGPath);
maskLayerAnimation.toValue = (__bridge
id)((maskFinalBP.CGPath));
maskLayerAnimation.duration = [self
transitionDuration:transitionContext];
maskLayerAnimation.timingFunction = [CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
maskLayerAnimation.delegate = self;
[maskLayer addAnimation:maskLayerAnimation forKey:@"path"];
}
#pragma mark - CABasicAnimation的Delegate
50
- (void)animationDidStop:(CAAnimation *)anim
finished:(BOOL)flag{
//告诉 iOS 这个 transition 完成
[self.transitionContext completeTransition:![self. transitionContext transitionWasCancelled]];
//清除 fromVC 的 mask
[self.transitionContext
viewControllerForKey:UITransitionContextFromViewControllerKey].
view.layer.mask = nil;
[self.transitionContext
viewControllerForKey:UITransitionContextToViewControllerKey].vi
ew.layer.mask = nil;
}
到 这 ⾥ 这 个 简 单 的 动 画 就 全 部 结 束 了 。 源 码 请 见 K Y P i n g Tr a n s i t i o n D e mo。 事实上,我们看到的绝⼤多数动画基本都是这些标准动画的叠加。
⽐如平移、缩放、旋转等,只不过这些效果串在⼀起看起来就感觉⾮常
复杂了,更何况动画的时间往往⽐较短
。常⾔道,唯快不破。
顺便,我开源了⼀个类似的转场动画
M OVIE 3.3, 只 需 给 定 ⼀ 个 任 意 p o i n t , 就
可以通过圆圈放⼤的动画进⾏转场。地址在
A - G U I D E - TO - i O S - A N I M AT I O N 下 的 K Y B u b b l e Tr a n s i t i o n 。 如 果 你 对 其 中 的 类 似
⽓泡晃
动效果感兴趣,可以来 KYFloatingBubble 看
51
M OVIE 3.3 ⽓泡放⼤的转场动画
看,没有任何特殊技巧,完全⽤的 Core Animation 基本功。
如果让我站在⽬前局限的⾓度评价⼀下 CoreAnimation 的话,我觉得最⼤感悟就是 ——「⼤道⾄简」。我
看到过的所有真正⽜逼的国外动画师到最后⽤的并不是
什么⿊科技或者深奥的算法来做动画,当然也有,但更
多的还是⽤最基础的 CoreAnimation ,通过各种基本动画的组合
(duration,delay,timeFunction,damping,velocity...),以及合理的参数调
节,让⼀款优秀的动画跃然于屏幕之上。所以下⾯我将尝试给你介绍⼏
个动画,完完全全⽤的就是 CoreAnimation 中的「基本单位」—— translation、rotation、scale... 打造出乍看起来很「复杂」的动画效果。相信看
完这部分,你就可以应付实际应⽤中绝⼤多数动画需求了,毕竟⽇常还
是 C o r e A n i m a t i o n ⽤ 到 的 最 多 。 l e t ’s g e t s t a r t e d !
52
这个例⼦是⼀个图⽚弹跳切换的效
M OVIE 3.4 图⽚弹跳+翻转动画
果,最早出现在锤⼦⽇历中。当你对某⼀
条内容加星之后,就会出现这个活泼的切
换 动 画 。 效 果 参 见 M OVIE 3.4 。
动画分为两个阶段:⼀个弹上去的阶
段,⼀个落下来的阶段。弹上去的过程让视图绕 y 轴旋转 90 °,此时第
⼀ 阶 段 的 动 画 结 束 。 在 代 理 ⽅ 法 animationDidStop 中 开 始 第 ⼆ 个 动 画
—— 下落。在这个阶段⼀开始⽴刻替换图⽚, 随后在落下的同时让视图
继续旋转 90°。然后你可能有疑问了,这怎么才转了 180° ?那不是动画
结 束 之 后 图 ⽚ 是 反 过 来 的 吗 ? 对 , 所 以 我 们 要 在 下 落 动 画 结 束 之 后 removeAllAnimations
代码也⾮常短:
//上弹动画
-(void)animate{
if (animating == YES) {
return;
}
animating = YES;
53
CABasicAnimation *transformAnima = [CABasicAnimation
animationWithKeyPath:@"transform.rotation.y"];
transformAnima.fromValue = @(0);
transformAnima.toValue = @(M_PI_2);
transformAnima.timingFunction = [CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
CABasicAnimation *positionAnima = [CABasicAnimation
animationWithKeyPath:@"position.y"];
positionAnima.fromValue = @(self.starView.center.y);
positionAnima.toValue = @(self.starView.center.y - 14);
positionAnima.timingFunction = [CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionEaseOut];
CAAnimationGroup *animGroup = [CAAnimationGroup animation];
animGroup.duration = jumpDuration;
animGroup.fillMode = kCAFillModeForwards;
animGroup.removedOnCompletion = NO;
animGroup.delegate = self;
animGroup.animations = @[transformAnima,positionAnima];
[self.starView.layer addAnimation:animGroup
forKey:@"jumpUp"];
}
//下落动画
- (void)animationDidStop:(CAAnimation *)anim
finished:(BOOL)flag{
if ([anim isEqual:[self.starView.layer
animationForKey:@"jumpUp"]]) {
self.state = self.state==Mark?non_Mark:Mark;
NSLog(@"state:%ld",_state);
CABasicAnimation *transformAnima = [CABasicAnimation
animationWithKeyPath:@"transform.rotation.y"];
transformAnima.fromValue = @(M_PI_2);
transformAnima.toValue = @(M_PI);
54
transformAnima.timingFunction = [CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
CABasicAnimation *positionAnima = [CABasicAnimation
animationWithKeyPath:@"position.y"];
positionAnima.fromValue = @(self.starView.center.y 14);
positionAnima.toValue = @(self.starView.center.y);
positionAnima.timingFunction = [CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionEaseIn];
CAAnimationGroup *animGroup = [CAAnimationGroup animation];
animGroup.duration = downDuration;
animGroup.fillMode = kCAFillModeForwards;
animGroup.removedOnCompletion = NO;
animGroup.delegate = self;
animGroup.animations = @[transformAnima,positionAnima];
[self.starView.layer addAnimation:animGroup
forKey:@"jumpDown"];
}else if([anim isEqual:[self.starView.layer
animationForKey:@"jumpDown"]]){
[self.starView.layer removeAllAnimations];
animating = NO;
}
}
关于两个阶段动画时间上的思考。理论上,在忽略空⽓阻⼒的情况
下,两个阶段的动画⼀定是相等的,⽆论物体的初速度为多少。但是现
实世界中,我们看到的现象是:如果以⼀个初速度往上抛⼀个物体,你
的感觉是物体上抛过程花费的时间短,下落花费的时间长。因为上抛是
55
个减速运动,⽽下落是个加速运动。这个感觉很重要,虽然违背物理规
律,但是我们要做的就是更真实地模拟⼈对现实世界的感知。所以我们
在做动画中需要做这些违背物理规律的事情:即把下落的时间设置得⽐
上弹的长。以期符合真实感知。更何况, CoreAnimation 中是不能设置初
速度的,默认都是从零速度开始运动,所以我们必须通过缩短上抛时间
以此来模拟出这个初速度,让动画看起来更真实。
#define jumpDuration 0.125
#define downDuration 0.215
⼀个优秀动画的特点除了复杂之外,也就是短时间内输出⾜够多的信
息量;另⼀个特点,就是细节决定质量。在这个 demo 中,我在星星弹
跳的底部加了⼀个阴影,随着视图的上弹下落阴影的 bounds 也会有动
画。千万别认为这些属于情怀的东西「然并卵」,⽤户不是傻⼦,你⽤
⼼设置的细节都会被⽤户察觉到。然后别宣传,要等⽤户⾃⼰去发现。
这和提前就告诉⽤户带去的⼼理感受是完全不⼀样的。这些东西也是我
在锤⼦科技⽿濡⽬染学到的,在以后的动画教程中,我除了在介绍技术
层⾯的东西之外,也会额外穿插⼀些⼈性化、感性⽅⾯的⼩⼼得,⼩体
会,拿出来权当供各位参考。好了,⾄于这个阴影动画的时机,我设置
在 了 两 个 动 画 的 ⼀ 开 始 , 也 就 是 animationDidStart 中 :
56
- (void)animationDidStart:(CAAnimation *)anim{
if ([anim isEqual:[self.starView.layer
animationForKey:@"jumpUp"]]) {
[UIView animateWithDuration:jumpDuration delay:0.0f
options:UIViewAnimationOptionCurveEaseOut animations:^{
_shadowView.alpha = 0.2;
_shadowView.bounds = CGRectMake(0, 0,
_shadowView.bounds.size.width*1.6,
_shadowView.bounds.size.height);
} completion:NULL];
}else if ([anim isEqual:[self.starView.layer
animationForKey:@"jumpDown"]]){
[UIView animateWithDuration:jumpDuration delay:0.0f
options:UIViewAnimationOptionCurveEaseOut animations:^{
_shadowView.alpha = 0.4;
_shadowView.bounds = CGRectMake(0, 0,
_shadowView.bounds.size.width/1.6,
_shadowView.bounds.size.height);
} completion:NULL];
}
}
第三节到这⾥也就结束了。源码请见 JumpStarDemo。
57
第 4 节 要 呈 现 的 效 果 如 M OVIE 3.5 所 ⽰ :
这 是 ⼀ 个 下 载 按 钮 的 动 画 , 涉 及 到 了 cornerRadius, bounds, strokeEnd 等
M OVIE 3.5 下载按钮动画
相关属性的动画。
⾸先我们讲讲这个
cornerRadius 。 顾 名 思 义 , 这 个 属 性 是 ⽤ 来
绘制矩形的圆⾓,具体这个值表⽰的意义是
这样的
。
正如图中展⽰的那样,如果你让想让⼀个正⽅形变成圆形,那么你所
要 做 的 就 是 把 cornerRadius 这 个 值 变 成 边 长 的 1 / 2 。
同 理 , 如 果 是 ⼀ 个 矩 形 , 想 让 两 头 变 为 圆 ⾓ , 只 需 要 把 cornerRadius 设 置 成 矩 形 ⾼ 的 1 / 2 即 可
。
设 置 了 cornerRadius 之 后 别 忘 了 , 记 得 开 启
self.clipsToBounds
= YES; 或 者 self.layer.masksToBounds = YES; 把 圆 ⾓ 之 外 的 部 分
「切除」。
这个 demo 并没有⾮常复杂的算法,纯粹就是不同动画在不同时间上
的叠加,只要条理清晰,想清楚⼀个动画结束后该接什么动画就⾏了。
下⾯我们着重看看代码。
58
-(void)tapped:(UITapGestureRecognizer *)tapped{
originframe = self.frame;
if (animating == YES) {
return;
}
for (CALayer *subLayer in self.layer.sublayers) {
[subLayer removeFromSuperlayer];
}
self.backgroundColor = [UIColor colorWithRed:0.0
green:122/255.0 blue:255/255.0 alpha:1.0];
animating = YES;
self.layer.cornerRadius = self.progressBarHeight/2;
CABasicAnimation *radiusAnimation = [CABasicAnimation
animationWithKeyPath:@"cornerRadius"];
radiusAnimation.duration = 0.2f;
radiusAnimation.timingFunction = [CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionEaseOut];
radiusAnimation.fromValue = @(originframe.size.height/2);
radiusAnimation.delegate = self;
[self.layer addAnimation:radiusAnimation
forKey:@"cornerRadiusShrinkAnim"];
}
-(void)animationDidStart:(CAAnimation *)anim{
if ([anim isEqual:[self.layer
animationForKey:@"cornerRadiusShrinkAnim"]]) {
[UIView animateWithDuration:0.6f delay:0.0f
usingSpringWithDamping:0.6 initialSpringVelocity:0.0
options:UIViewAnimationOptionCurveEaseOut animations:^{
59
self.bounds = CGRectMake(0, 0, _progressBarWidth,
_progressBarHeight);
} completion:^(BOOL finished) {
[self.layer removeAllAnimations];
[self progressBarAnimation];
}];
}
}
圆变成进度条了,接下来就是进度条动画了。正如我上⾯代码中写的
那 样 , 在 上 ⼀ 个 动 画 结 束 之 后 调 ⽤ 了 [self progressBarAnimation];
-(void)progressBarAnimation{
CAShapeLayer *progressLayer = [CAShapeLayer layer];
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(_progressBarHeight/2,
self.bounds.size.height/2)];
[path
addLineToPoint:CGPointMake(self.bounds.size.width-_progressBarH
eight/2, self.bounds.size.height/2)];
progressLayer.path = path.CGPath;
progressLayer.strokeColor = [UIColor whiteColor].CGColor;
progressLayer.lineWidth = _progressBarHeight-6;
progressLayer.lineCap = kCALineCapRound;
[self.layer addSublayer:progressLayer];
CABasicAnimation *pathAnimation = [CABasicAnimation
animationWithKeyPath:@"strokeEnd"];
pathAnimation.duration = 2.0f;
pathAnimation.fromValue = @(0.0f);
pathAnimation.toValue = @(1.0f);
pathAnimation.delegate = self;
[pathAnimation setValue:@"progressBarAnimation"
forKey:@"animationName"];
[progressLayer addAnimation:pathAnimation forKey:nil];
}
60
关于进度条动画,我不打算使⽤ popup 的⽅式介绍。我将单独拿出
篇幅来讲讲。
⾸ 先 你 要 知 道 这 类 进 度 的 动 画 , 都 是 ⽤ 的 strokeEnd 属 性 。 ⽽ strokeEnd 不 是 CALayer 的 属 性 , ⽽ 是 其 ⼦ 类 CAShapeLayer 的 ⼀ 个 特 有
的 属 性 。 所 以 我 们 必 须 创 建 ⼀ 个 CAShapeLayer.
参 数 就 是 path.
其次,⼀个必须赋值的
D e m o 中 , 我 们 绘 制 了 ⼀ 条 直 线 作 为 CAShapeLayer 的
path.
如何设置直线的起始点才能让⽩⾊进度条距离四周的间距相等呢?结
论 是 x = _progressBarHeight/2. 证 明 如 下 :
因 为 我 们 设 置 了 progressLayer.lineCap = kCALineCapRound; lineCap 指 的 是 线 段 的 线 帽 , 也 就 是 决 定 ⼀ 条 线 段 两 段 的 封 ⼜ 样 式 , 有 三 种
样式可以选择:
61
kCALineCapButt: 默认格式,不附加任何形状;
kCALineCapRound: 在线段头尾添加半径为线段 lineWidth ⼀半的半圆;
kCALineCapSquare: 在线段头尾添加半径为线段 lineWidth ⼀半的矩形
由 于 我 们 之 前 设 置 了 kCALineCapRound,
lineWidth/2,
⽽半圆的半径就是
所 以 起 始 点 的 x 坐 标 应 该 满 ⾜ 公 式 x = space +
lineWidth/2, 又 ∵ lineWidth = _progressBarHeight - space*2
∴
x = _progressBarHeight/2, 也 就 是 说 起 始 点 的 x 坐 标 与 lineWidth
的值并没有关系。
所以,只要保证了 path 的起点 x 坐标等于外围进度条(demo 中的
蓝 ⾊ 进 度 条 ) ⾼ 度 的 1 / 2 , 那 么 ⽆ 论 设 置 p a t h 的 lineWidth 为 多 少 都
可以让⽩⾊进度条距离四周的间距相等。
关 于 @property CGFloat strokeStart; 和 @property CGFloat
strokeEnd; 这 两 个 属 性 , 正 如 它 的 名 字 ⼀ 样 , 定 义 了 线 段 的 开 始 和 结
62
束 , 并 且 取 值 都 在 [ 0 , 1 ] 之 间 。 默 认 strokeStart 为 0 , strokeEnd 为
1。通过设置不同的值,可以控制线条的展⽰状态。
注 意 [pathAnimation setValue:@"progressBarAnimation"
forKey:@"animationName"]; 这 ⼀ 句 , 就 是 我 们 之 前 说 的 判 断 不 同 anim
的第⼆种⽅法:KVO.
当进度条动画⾛完后,我们先让进度条做⼀个透明度到 0 的动画,
之后⽴马同时开始⼀个 cornerRadius 动画和⼀个 bounds 动画,让进度
条恢复到圆形状态。
-(void)animationDidStop:(CAAnimation *)anim
finished:(BOOL)flag{
if ([[anim
valueForKey:@"animationName"]isEqualToString:@"progressBarAnima
tion"]){
[UIView animateWithDuration:0.3 animations:^{
for (CALayer *subLayer in self.layer.sublayers) {
subLayer.opacity = 0.0f;
}
} completion:^(BOOL finished) {
if (finished) {
for (CALayer *subLayer in self.layer.sublayers)
{
[subLayer removeFromSuperlayer];
}
self.layer.cornerRadius =
originframe.size.height/2;
63
CABasicAnimation *radiusAnimation = [CABasicAnimation animationWithKeyPath:@"cornerRadius"];
radiusAnimation.duration = 0.2f;
radiusAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
radiusAnimation.fromValue =
@(_progressBarHeight/2);
radiusAnimation.delegate = self;
[self.layer addAnimation:radiusAnimation
forKey:@"cornerRadiusExpandAnim"];
}
}];
}
}
-(void)animationDidStart:(CAAnimation *)anim{
if ([anim isEqual:[self.layer
animationForKey:@"cornerRadiusExpandAnim"]]){
[UIView animateWithDuration:0.6f delay:0.0f
usingSpringWithDamping:0.6 initialSpringVelocity:0.0
options:UIViewAnimationOptionCurveEaseOut animations:^{
self.bounds = CGRectMake(0, 0,
originframe.size.width, originframe.size.height);
self.backgroundColor = [UIColor
colorWithRed:0.1803921568627451 green:0.8
blue:0.44313725490196076 alpha:1.0];
} completion:^(BOOL finished) {
[self.layer removeAllAnimations];
[self checkAnimation];
//----animating = NO;
}];
}
}
64
进度条恢复到圆形状态之后,我们就该进度打勾的动画了。正如你在
上 ⾯ 看 到 的 那 样 , 我 在 代 码 最 后 也 就 是 b o u n d s 动 画 结 束 时 removeAllAnimations , 并 调 ⽤ 了 [self checkAnimation];
打 勾 动 画 的 思 路 依 然 是 给 ⼀ 个 CAShapeLayer 指 定 ⼀ 个 勾 形 的 path,
然 后 进 ⾏ strokeEnd 的 动 画 。
-(void)checkAnimation{
CAShapeLayer *checkLayer = [CAShapeLayer layer];
UIBezierPath *path = [UIBezierPath bezierPath];
CGRect rectInCircle = CGRectInset(self.bounds,
self.bounds.size.width*(1-1/sqrt(2.0))/2,
self.bounds.size.width*(1-1/sqrt(2.0))/2);
[path moveToPoint:CGPointMake(rectInCircle.origin.x +
rectInCircle.size.width/9, rectInCircle.origin.y +
rectInCircle.size.height*2/3)];
[path addLineToPoint:CGPointMake(rectInCircle.origin.x +
rectInCircle.size.width/3,rectInCircle.origin.y +
rectInCircle.size.height*9/10)];
[path addLineToPoint:CGPointMake(rectInCircle.origin.x +
rectInCircle.size.width*8/10, rectInCircle.origin.y +
rectInCircle.size.height*2/10)];
checkLayer.path = path.CGPath;
checkLayer.fillColor = [UIColor clearColor].CGColor;
checkLayer.strokeColor = [UIColor whiteColor].CGColor;
checkLayer.lineWidth = 10.0;
checkLayer.lineCap = kCALineCapRound;
checkLayer.lineJoin = kCALineJoinRound;
[self.layer addSublayer:checkLayer];
65
CABasicAnimation *checkAnimation = [CABasicAnimation
animationWithKeyPath:@"strokeEnd"];
checkAnimation.duration = 0.3f;
checkAnimation.fromValue = @(0.0f);
checkAnimation.toValue = @(1.0f);
checkAnimation.delegate = self;
[checkAnimation setValue:@"checkAnimation"
forKey:@"animationName"];
[checkLayer addAnimation:checkAnimation forKey:nil];
}
现在你已经学完了这个 demo 。理论上,所有描线的动画你都可以⽤
这 种 ⽅ 式 先 指 定 ⼀ 个 path 然 后 改 变 strokeEnd, strokeStart 来 实 现 。
⽐如我在
上找到的两则动画:
M OVIE 3.6 线条动画原型 001
M OVIE 3.7 线条动画原型 002
Paperclip Loader -by Jokūbas
Submit -by Lars Lundberg
最 后 , 我 还 想 补 充 的 ⼀ 点 是 关 于 CAKeyframeAnimation 中 @property CGPathRef path; 这 ⼀ 属 性 。 这 是 经 典 的 路 径 动 画 。 你 只 需 指 定 ⼀
个 指 针 类 型 的 CGPathRef , 剩 下 的 事 情 就 交 给 CAKeyframeAnimation 吧
— — 视 图 的 anchorPoint 就 会 沿 着 你 给 出 的 这 条 path 运 动 。 ⽐ 如 右 侧
66
这 个 M OVIE 3.8 中 L o a d i n g 的 例 ⼦ , 我 也
放 在 了 A - G U I D E - TO - i O S - A N I M AT I O N 下 的 K Y LoadingHUD 下,代码中⽆⾮就是多了个
path 属 性 , 相 信 已 经 不 ⽤ 我 解 释 了 , 你 ⼀
眼就能看懂。
67
M OVIE 3.8 path 属性的效果
最后,附上 WWDC 2011 Session 421 —— Core Animation Essentials
68
4
动画中的数学
69
如果要我说我最喜欢哪种类型的动画,我想应该就是这⼀节的主题
——「结合数学知识的动画」。尽管我在前⾯的章节中后多或少都涉及到
了数学知识的使⽤,但毕竟不是主⾓。⽽这⼀章,我将把数学的⾓⾊晋
升到第⼀位置,让数学的魅⼒延伸到程序中来。除此之外,你还必须努
⼒习惯动⼿在纸上画草图,习惯找到数学和程序之间的衔接点。这将会
⾮常有趣,也会很有成就感。
这⼀章的第⼀个知识点,我们来聊聊动画曲线(timingFunction)。
众说周知,我们常⽤的 CoreAnimation 动画曲线只有默认提供的四个
枚举值:
CA_EXTERN NSString * const kCAMediaTimingFunctionLinear
__OSX_AVAILABLE_STARTING (__MAC_10_5, __IPHONE_2_0);
CA_EXTERN NSString * const kCAMediaTimingFunctionEaseIn
__OSX_AVAILABLE_STARTING (__MAC_10_5, __IPHONE_2_0);
CA_EXTERN NSString * const kCAMediaTimingFunctionEaseOut
__OSX_AVAILABLE_STARTING (__MAC_10_5, __IPHONE_2_0);
CA_EXTERN NSString * const kCAMediaTimingFunctionEaseInEaseOut
__OSX_AVAILABLE_STARTING (__MAC_10_5, __IPHONE_2_0);
70
当 然 , 你 完 全 可 以 ⾃ ⼰ 创 建 时 间 曲 线 。 使 ⽤ CAMediaTimingFunction
中的
+ (instancetype)functionWithControlPoints:(float)c1x
:(float)c1y :(float)c2x :(float)c2y;
⽅ 法 就 可 以 创 建 ⼀ 个 timingFunction
。请看下⽅这幅图,就是对上⾯这个
⽅法的解释。有没有觉得很熟悉,没
错,又是贝塞尔曲线。正如我 前⾯
说的那样,这绝对是对计算机图形学
领域具有⾥程碑意义的学术成果。
具 体 讲 讲 ⾃ 定 义 timingFunction 的 ⽅ 法
其 中 的 c 1x , c 1y 代 表 第 ⼀
个 关 键 点 , 也 就 是 图 中 p 1 点 , c 2x , c 2y 代 表 第 ⼆ 个 关 键 点 , 对 应 图 中
p 2 点 。 从 图 中 可 以 看 出 , c 1 x , c 1 y , c 2 x , c 2 y 的 范 围 都 是 [ 0 , 1 ] 。 这 和 CALayer 的 anchorPoint 很 类 似 。 然 ⽽ 头 疼 的 是 , 每 次 ⾃ 定 义 都 要 计 算 两
个关键点的坐标的确是⼀件让⼈望⽽却步的事情。
不过,历史的经验告诉我们,绝⼤多数问题你都不会是第⼀个想到
的,我们都是站在巨⼈的肩上看更远的风景。尤其对于这种被众多领域
71
普遍使⽤的东西,各种⽅便⽣成贝塞尔曲线关键点的⼯具可谓是层出不
穷。前⼈早就已经开发⼀⼤堆了,步骤简单到只需通过拖拽控制点描绘
出你想要的曲线,⼯具就⽴刻⾃动⽣成了关键点坐标。这⾥推荐我常⽤
的⼀个⼯具,就是这个⽹站 —— Rob La Placa 。或者有款 Xcode插件
— — C ATw e a k e r 。
然后,我们可以更进⼀步。抛开使
⽤ CAMediaTimingFunction
I MAGE 4.1 震荡曲线
毕竟这限
制了动画只能从 0 变化到 1,不能做到
完全⾃定义,⽐如先从 0 ease-out 变
化到 1 ,再 ease-in 变化回到 0;或者
更 复 杂 , ⽐ 如 想 要 以 震 荡 曲 线 I MAGE
4.1 的 规 律 运 动 。 所 以 , 我 们 就 会 很 ⾃ 然 地 想 到 , 如 果 能 把 任 意 ⼀ 条 数
学中的函数图像转化成⾃定义的动画曲线,那该有多好。
72
这次的案例,我们将介绍实现⼀个平滑的⼿势驱动动画。还是
先 请 预 览 最 终 效 果 M OVIE
M OVIE 4.1 ⼿势驱动动画最终效果
4.1 。
整体思路是这样的:
设定最⼤滑动距离为 120。
随着滑动距离绝对值(离开初始位置的距离,竖直向上或竖直向下)
的增加,逐渐接近最⼤滑动距离。这个过程中,视图同时做三个变换:
• 第⼀个是 translation .让视图的 center 的位移等于⼿指的位移;
• 其次是 scale . 从 1.0 到 0.8;
• 另⼀个变换是 Rotate(绕 x 轴且带透视效果) ,从0增长到1,之后
⽴即从1减⼩到0。
以 上 三 个 分 运 动 叠 加 在 ⼀ 起 , 就 是 M OVIE 4.1 的 效 果 了 。
既然是三个分运动,我们还是把他们分解开来,各个击破。 循序渐
进的成就感才是最有持久的。⾸先是位移,这个⾮常容易。
73
-(void)panGestureRecognized:(UIPanGestureRecognizer *)pan{
static CGPoint initialPoint = currentPhoto.center;
CGFloat factorOfAngle = 0.0f;
CGFloat factorOfScale = 0.0f;
CGPoint transition = [pan translationInView:self.view];
if (pan.state == UIGestureRecognizerStateBegan) {
initialPoint = self.center;
}else if{
if(pan.state == UIGestureRecognizerStateChanged){
self.center = CGPointMake(initialPoint.x,initialPoint.y
+ transition.y);
}
}
}
然后是实现 scale 变换。先确定以什么样的动画曲线进⾏动画:我们
让视图以平滑的⼆次函数曲线从 1.0 缩⼩到 0.8,所以这个差值 0.2 的函
数曲线就可以遵循如下的⼆次函数:
滑 动 距 离 到 达 最 ⼤ 规 定 距 I MAGE 4.2 scale 的函数图像
离时,动画也就到达了末状
态,我们使⽤⼀个系数 factorOfScale 。下⾯就可以建模
出⼀道⾼中数学题了:
74
(SCROLLDISTANCE,
1),
(0,0)
(2*SCROLLDISTANCE,0)
[0,SCROLLDISTANCE]
如果你还记得初中数学知识的话,我们可以很快地求解得到
。转换成代码就是:
factorOfScale =
MAX(0,-1/(SCROLLDISTANCE*SCROLLDISTANCE)*Y*(Y-2*SCROLLDISTANCE)
);
接 下 来 创 建 ⼀ 个 CATransform3D , 并 把 这 个 CATransform3D 赋 值 给
l a y e r 的 transform 属 性 。 把 这 段 代 码 放 在 ⼿ 势 绑 定 的 ⽅ 法 中
if(pan.state == UIGestureRecognizerStateChanged){
...
CGFloat Y =MIN(SCROLLDISTANCE,MAX(0,ABS(transition.y)));
//⼀个开⼝向下,顶点(SCROLLDISTANCE,1),过(0,0),
(2*SCROLLDISTANCE,0)的⼆次函数
factorOfScale =
MAX(0,-1/(SCROLLDISTANCE*SCROLLDISTANCE)*Y*(Y-2*SCROLLDISTANCE)
);
CATransform3D t = CATransform3DIdentity;
t = CATransform3DScale(t, 1-factorOfScale*0.2,
1-factorOfScale*0.2, 0);
75
self.layer.transform = t;
...
}
最后就是 Rotate 变换。根据我们设想的那样,我们需要让图⽚先往
⾥转到最⼤值,⽐如
I MAGE 4.3 rotate 的函数图像
36°;随后向外旋转回
到 0°。结合平滑的运动
曲线,因此很容易想到
右⾯这条函数曲线。
有没有觉得曲线很熟悉
是的,同样也可以近似地看成⼀条⼀元⼆次
曲 线 , 变 量 为 transition.y ( 准 确 的 说 , 应 该 是 ABS(transition.y))
, 且 0 < = ABS(transition.y) < = SCROLLDISTANCE ) 。 建 模 完 成 , 又 到
了复习⾼中数学知识的时间了:
(SCROLLDISTANCE/2,1),
(0,0)
(SCROLLDISTANCE,0)
[0,SCROLLDISTANCE]
76
factorOfAngle = MAX(0,-4/(SCROLLDISTANCE*SCROLLDISTANCE)*Y*(YSCROLLDISTANCE));
在 之 前 的 代 码 中 为 CATransform3D t 追 加 变 换 , 最 终 得 到 的 是 :
-(void)panGestureRecognized:(UIPanGestureRecognizer *)pan{
static CGPoint initialPoint;
CGFloat factorOfAngle = 0.0f;
CGFloat factorOfScale = 0.0f;
CGPoint transition = [pan
translationInView:self.superview];
if (pan.state == UIGestureRecognizerStateBegan) {
initialPoint = self.center;
}else if(pan.state == UIGestureRecognizerStateChanged){
self.center = CGPointMake(initialPoint.x,initialPoint.y
+ transition.y);
CGFloat Y
=MIN(SCROLLDISTANCE,MAX(0,ABS(transition.y)));
//⼀个开⼝向下,顶点(SCROLLDISTANCE/
2,1),过(0,0),(SCROLLDISTANCE,0)的⼆次函数
factorOfAngle = MAX(0,-4/
(SCROLLDISTANCE*SCROLLDISTANCE)*Y*(Y-SCROLLDISTANCE));
//⼀个开⼝向下,顶点(SCROLLDISTANCE,1),过(0,0),
(2*SCROLLDISTANCE,0)的⼆次函数
factorOfScale =
MAX(0,-1/(SCROLLDISTANCE*SCROLLDISTANCE)*Y*(Y-2*SCROLLDISTANCE)
);
CATransform3D t = CATransform3DIdentity;
77
t.m34 = 1.0/-1000;
t = CATransform3DRotate(t,factorOfAngle*(M_PI/5),
transition.y>0?-1:1, 0, 0);
t = CATransform3DScale(t, 1-factorOfScale*0.2,
1-factorOfScale*0.2, 0);
self.layer.transform = t;
}
}
本节内容到这⾥也就结束了。你可以在
下载到源码。
下⾯这个案例,我把线条动画和数学知识结合在了⼀起。
通过这个案例,可以很好地向你展⽰如何⾃⼰归纳出⼀
个数学公式,并把它⽤到⼀个⾃定义动画中。
⾸ 先 , 我 们 还 是 先 看 最 终 效 果 M OVIE 4.2 :
OK,可以看到随着⼿指在屏幕上滑
M OVIE 4.2 ⾃定义的下拉线条动画
动距离的改变,线条⼀开始逐渐靠拢,
到达⼀定位置后开始弯曲,最终合并成
了⼀个圆。你可能也已经注意到,我已
经把这个动画封装到了⼀个上拉、下拉
刷新的控件中,并且⽤在了⼤象公会这
78
款独⽴开发的 App 中。
下⾯让我讲讲我思考这个动画的整个过程。⾸先,最终控制这个动画
进 度 的 是 ⼀ 个 CALayer 内 部 的 ⾃ 定 义 属 性 :
@property(nonatomic,assign)CGFloat progress;
⽆论你是通过⼿指滑动产⽣偏移量,还是滑动 UISlider 改变⼀个数
值,最终都将转化到这个属性的改变。然后,在这个属性的 setter ⽅法
⾥,我们让 layer 去实时重绘,就像这样:
-(void)setProgress:(CGFloat)progress{
self.curveLayer.progress = progress;
[self.curveLayer setNeedsDisplay];
}
⾄于重绘的算法,这属于细节上要考虑的事了。我们做⼀个动画的步
骤是先把宏观上的思路理清,再去考虑细节上的实现。就像开发⼀个
App ⼀样,⼀开始肯定是先考虑架构,再去往这个框架⾥添砖加⽡,修
修补补。现在,我们对这个动画的整体思路已经清楚了,下⾯开始深⼊
到细节去思考具体算法的实现。我把这个动画分成了两部分:0~0.5
和
0 . 5 ~ 1 . 0 . 什 么 意 思 呢 ? 我 给 你 做 了 两 个 K e y n o t e : K EYNOTE 4.1,
K EYNOTE 4.2。
79
还是那句话 ——「善于分
K EYNOTE 4.1 线条弯曲动画前半程分析
解」。我们先看前半程,也就是
progress 从⼀开始的 0 运动到中
间状态 0.5 的这⼀个阶段。这⼀个
阶段两条线段分别从上⽅和下⽅两
个⽅向向中间运动,直到接触到中
线为⽌。这⼀阶段的画线算法⾮常简单,只要能实时获得 A,B 两点的坐
标 , 剩 下 ⽤ UIBezierPath 的 moveToPoint,addLineToPoint 就 完 事
了。所以,问题转换成了求 A,B 两点运动的公式(其实只要求出⼀点,
另⼀点⽆⾮就相差了⼀个线段长度 h)。这⾥我纠结了好久,该⽤什么
⽅式像你介绍计算出这两个公式的过程,最后我能想到的只有通过做
K EYNOTE 4.1 , K EYNOTE 4.2 这两 个 演 ⽰ ⽂ 稿 的 ⽅ 式 , 剩 下 的 就 只 能 意
会不能⾔传了。其实你只要愿意动笔在纸上尝试推演⼀番,并不难求得
这两个点的运动公式:
yA = H/2 + h + (1-2*progress) * (H/2 - h)
yB = H/2 + (1-2*progress) * (H/2 - h)
接下来是动画的第⼆阶段 0.5~1.0。这个阶段有些许复杂:「B 点
保持不动,A 点继续运动到 B 的位置,同时,在顶部根据当前的进度再
80
画出圆弧」。视觉上给⼈的感觉就
K EYNOTE 4.2 线条弯曲动画前半程和后半程的分析
好像尾巴在逐渐缩短,头部在慢慢
弯曲。
在这个过程中,我们不难先求
得 A 点的坐标是:
yA = H/2 + h - h*(progress - 0.5) *2
⽐ 较 ⿇ 烦 的 是 这 个 圆 弧 该 怎 么 画 ? 答 案 是 可 以 ⽤ UIBezierPath 中
提供的
- (void)addArcWithCenter:(CGPoint)center radius:(CGFloat)radius
startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle
clockwise:(BOOL)clockwise NS_AVAILABLE_IOS(4_0);
这个⽅法绘制出圆弧。具体算法是:以
CGPointMake(self.frame.size.width/2
为圆⼼,10
到
2*M_PI
self.frame.size.height/2)
为半径,按顺时针⽅向,从 M_PI(90°) 的起始⾓度,画
的结束⾓度。
到这⾥,我们只完成了⼀条线段的整个过程。同理,也能获得另⼀条
线段的绘制算法。最后,别忘了线段顶端还有个箭头。绘制箭头的算法
81
G ALLERY 4.1: 我 们 以 B 点 作 为 箭 头 的 起 始 起 点 , 斜 向 左 下 ⽅ 3 0 ° ⾓ 延
长 3 个单位。弯曲之后也同理,只需要额外加上线段转过的⾓度即可。
G ALLERY 4.1 掩饰线段顶部箭头的绘制思路
相应的代码就是:
[arrowPath moveToPoint:pointB];
[arrowPath addLineToPoint:CGPointMake(pointB.x - 3*(cosf(Degree)), pointB.y + 3*(sinf(Degree)))];
[curvePath1 appendPath:arrowPath];
最终,整个动画完整的绘制算法如下:
-(void)drawInContext:(CGContextRef)ctx{
[super drawInContext:ctx];
UIGraphicsPushContext(ctx);
CGContextRef context = UIGraphicsGetCurrentContext();
//--------- Draw ---------//Path 1
82
UIBezierPath *curvePath1 = [UIBezierPath bezierPath];
curvePath1.lineCapStyle = kCGLineCapRound;
curvePath1.lineJoinStyle = kCGLineJoinRound;
curvePath1.lineWidth = 2.0f;
//arrowPath
UIBezierPath *arrowPath = [UIBezierPath bezierPath];
if (self.progress <= 0.5) {
CGPoint pointA =
CGPointMake(self.frame.size.width/2-Radius, CenterY - Space +
LineLength + (1-2*self.progress)*(CenterY-LineLength));
CGPoint pointB =
CGPointMake(self.frame.size.width/2-Radius, CenterY - Space +
(1-2*self.progress)*(CenterY-LineLength));
[curvePath1 moveToPoint:pointA];
[curvePath1 addLineToPoint:pointB];
//arrow
[arrowPath moveToPoint:pointB];
[arrowPath addLineToPoint:CGPointMake(pointB.x - 3*(cosf(Degree)), pointB.y + 3*(sinf(Degree)))];
[curvePath1 appendPath:arrowPath];
}else if (self.progress > 0.5) {
CGPoint pointA =
CGPointMake(self.frame.size.width/2-Radius, CenterY - Space +
LineLength - LineLength*(self.progress-0.5)*2);
CGPoint pointB =
CGPointMake(self.frame.size.width/2-Radius, CenterY - Space);
[curvePath1 moveToPoint:pointA];
[curvePath1 addLineToPoint:pointB];
[curvePath1
addArcWithCenter:CGPointMake(self.frame.size.width/2, CenterYSpace) radius:Radius startAngle:M_PI endAngle:M_PI + ((M_PI*9/
10) * (self.progress-0.5)*2) clockwise:YES];
//arrow
[arrowPath moveToPoint:curvePath1.currentPoint];
83
[arrowPath
addLineToPoint:CGPointMake(curvePath1.currentPoint.x - 3*(cosf(Degree - ((M_PI*9/10) * (self.progress-0.5)*2))),
curvePath1.currentPoint.y + 3*(sinf(Degree - ((M_PI*9/10) *
(self.progress-0.5)*2))))];
[curvePath1 appendPath:arrowPath];
}
//Path 2
UIBezierPath *curvePath2 = [UIBezierPath bezierPath];
curvePath2.lineCapStyle = kCGLineCapRound;
curvePath2.lineJoinStyle = kCGLineJoinRound;
curvePath2.lineWidth = 2.0f;
if (self.progress <= 0.5) {
CGPoint pointA =
CGPointMake(self.frame.size.width/2+Radius, 2*self.progress *
(CenterY + Space - LineLength));
CGPoint pointB =
CGPointMake(self.frame.size.width/2+Radius,LineLength +
2*self.progress*(CenterY + Space - LineLength));
[curvePath2 moveToPoint:pointA];
[curvePath2 addLineToPoint:pointB];
//arrow
[arrowPath moveToPoint:pointB];
[arrowPath addLineToPoint:CGPointMake(pointB.x + 3*(cosf(Degree)), pointB.y - 3*(sinf(Degree)))];
[curvePath2 appendPath:arrowPath];
}else if (self.progress > 0.5) {
[curvePath2
moveToPoint:CGPointMake(self.frame.size.width/2+Radius, CenterY
+ Space - LineLength + LineLength*(self.progress-0.5)*2)];
[curvePath2
addLineToPoint:CGPointMake(self.frame.size.width/2+Radius, CenterY + Space)];
[curvePath2
addArcWithCenter:CGPointMake(self.frame.size.width/2, (CenterY+Space)) radius:Radius startAngle:0
endAngle:(M_PI*9/10)*(self.progress-0.5)*2 clockwise:YES];
84
//arrow
[arrowPath moveToPoint:curvePath2.currentPoint];
[arrowPath
addLineToPoint:CGPointMake(curvePath2.currentPoint.x + 3*(cosf(Degree - ((M_PI*9/10) * (self.progress-0.5)*2))),
curvePath2.currentPoint.y - 3*(sinf(Degree - ((M_PI*9/10) *
(self.progress-0.5)*2))))];
[curvePath2 appendPath:arrowPath];
}
CGContextSaveGState(context);
CGContextRestoreGState(context);
[[UIColor blackColor] setStroke];
[arrowPath stroke];
[curvePath1 stroke];
[curvePath2 stroke];
UIGraphicsPopContext();
}
你 仍 然 可 以 在 我 的 G i t h u b r e p o — — A - G U I D E - TO - i O S - A N I M AT I O N 下 的
AnimatedCurveDemo 找到对应的源码。
85
第三个例⼦是我在发布本册电
M OVIE 4.3 模拟 tvOS 中的3D浮动效果
⼦书之前临时加的⼀个例⼦,
也是在看了 9⽉9⽇苹果新品
发布会之后才有的灵感。
效 果 请 看 M OVIE 4.3 。
下⾯我就带你来实现⼀下这个效果。⾸先
我要告诉你的是,这个 demo 的代码⾮常短,
也 就 1 0 0 ⾏ 左 右 。 其 中 ⽤ 到 的 核 ⼼ ⽅ 法 就 是 CATransform3DRotate
除此之外,没有使⽤到任何其它关于动画的 API。我会毫⽆保留地
向你展⽰我的整个思考过程,相信在看完之后,以后你再遇到类似的问
题也能举⼀反三。
⾸先,我要向你介绍的是整体视图
的层级。
tvOSCardView 是我们对外展⽰的
类,其上⽅有两个 subView —— 分别
是 cardImageView 和 cardParallaxView。 初 始 化 代 码 如 下 :
86
-(void)setUpSomething{
self.layer.shadowColor = [UIColor blackColor].CGColor;
self.layer.shadowOffset = CGSizeMake(0, 10);
self.layer.shadowRadius = 10.0f;
self.layer.shadowOpacity = 0.3f;
cardImageView = [[UIImageView
alloc]initWithFrame:self.bounds];
cardImageView.image = [UIImage imageNamed:@"poster"];
cardImageView.layer.cornerRadius = 5.0f;
cardImageView.clipsToBounds = YES;
[self addSubview:cardImageView];
UIPanGestureRecognizer *panGes = [[UIPanGestureRecognizer
alloc]initWithTarget:self action:@selector(panInCard:)];
[self addGestureRecognizer:panGes];
cardParallaxView = [[UIImageView
alloc]initWithFrame:cardImageView.frame];
cardParallaxView.image = [UIImage imageNamed:@"5"];
cardParallaxView.layer.transform =
CATransform3DTranslate(cardParallaxView.layer.transform, 0, 0,
200);
[self insertSubview:cardParallaxView
aboveSubview:cardImageView];
}
为什么需要这样设计这样的视图层级是有原因的。注意到了吗?我们
的这个视图带有阴影,但是细⼼的你又会发现,视图同时还带有圆⾓,
⽽ 圆 ⾓ 就 必 须 在 设 置 了 cornerRadius 的 同 时 开 启
layer.masksToBounds
clipsToBounds 或
如果此时阴影也是加在这个圆⾓的视图上,那么
87
阴 影 也 就 会 被 裁 掉 。 所 以 这 就 是 为 什 么 我 们 要 把 阴 影 设 置 在 self.layer
上 , 然 后 把 圆 ⾓ 设 置 在 cardImageView 上 。
再说说是⼿势控制了。我先不急给你看代码,我先聊聊⼀开始我是怎
么想的。我想要的效果是⼿指移到什么地⽅,什么地⽅就会「突」起
来,并且是绕着视图中点旋转的,⽽默认 iOS 中的坐标系是以左上⾓为
原点,⽔平向左为 x 轴正⽅向,竖直向下为 y 轴正⽅向。所以可以预感
到我们必须以视图中⼼为原点创建⼀个虚拟坐标系,以⽅便后续动画数
值的计算。下⾯给出的代码的作⽤就是 —— 把系统默认的坐标系统转换
成我们虚构的⼀个坐标系统,这个假设的坐标系以视图中⼼为坐标系原
点,就像下图所展⽰的那样:
-(void)panInCard:(UIPanGestureRecognizer *)panGes{
CGPoint touchPoint = [panGes locationInView:self];
if (panGes.state == UIGestureRecognizerStateChanged) {
CGFloat xFactor = MIN(1, MAX(-1,(touchPoint.x (self.bounds.size.width/2)) / (self.bounds.size.width/2)));
CGFloat yFactor = MIN(1, MAX(-1,(touchPoint.y (self.bounds.size.height/2)) / (self.bounds.size.height/2)));
cardImageView.layer.transform = [self
transformWithM34:1.0/-500 xf:xFactor yf:yFactor];
cardParallaxView.layer.transform = [self
transformWithM34:1.0/-250 xf:xFactor yf:yFactor];
88
}else if (panGes.state == UIGestureRecognizerStateEnded){
[UIView animateWithDuration:0.3 animations:^{
cardImageView.layer.transform = CATransform3DIdentity;
cardParallaxView.layer.transform = CATransform3DIdentity;
} completion:NULL];
}
}
-(CATransform3D )transformWithM34:(CGFloat)m34 xf:(CGFloat)xf
yf:(CGFloat)yf{
CATransform3D t = CATransform3DIdentity;
t.m34 = m34;
t = CATransform3DRotate(t, M_PI/9 * xf, 0, 1, 0);
t = CATransform3DRotate(t, M_PI/9 * yf, -1, 0, 0);
return t;
}
代码中,我通过两个转换公式获得了
两 个 数 值 xFactor 和 yFactor, 并 且 取 值 范 围 控 制 在 了 [ - 1 , 1 ] , 作
为我们新建的虚拟坐标系下的坐标点。
这样⼀来视图的左上⾓、右上⾓、左下
⾓、右下⾓就分别被转换成了 (-1,-1) ,
(1,-1) , (-1,1) , (1,1) 。然⽽为什么是 1,1 呢?
89
我 们 知 道 CATransform3D CATransform3DRotate (CATransform3D t,
CGFloat angle,CGFloat x, CGFloat y, CGFloat z) 这 个 ⽅ 法 在 给 定 ⼀
个 弧 度 制 的 ⾓ 度 之 后 , 后 ⾯ 的 x , y, z 只 需 要 指 定 ⼀ 个 任 意 正 数 或 零 或 任
意负数即可。通常我们使⽤ -1,0,1。关于苹果规定的 rotate 正⽅向参见
G ALLERY 4.2 。
G ALLERY 4.2 rotation.x/y/z 的正⽅向
接下来我就开始尝试把这个
坐 标 系 统 运 ⽤ 到 CATransform3DRotate 中 。 先 规 定 最 ⼤
偏移⾓度为 20°,也就是
M_PI/9
然后我处理的⽅法
绕 X 轴旋转的正⽅向
依然是分解,把⼀个多纬度动画
拆开为两个分运动逐个分析。我
先 分 析 了 绕 x 轴 旋 转 的 分 运 动 , 也 就 是 随 着 yFactor 值 从 - 1 到 1 , 视
图从 -20° 转到 20°。由于绕 x 轴旋转的正⽅向是上半部分向外,下部分
向 内 , 因 此 ⼀ 开 始 的 ⽅ 向 为 正 , 与 yFactory ⼀ 开 始 的 符 号 刚 好 相 反 , 所
以把 x
设 为 - 1 , 即 t = CATransform3DRotate(t, M_PI/9 * yf, -1,
0, 0)
90
同理,由于绕 y 轴旋转的正⽅向是左半部分向外,右半部分向内,
⽽ xFactor 的 值 是 从 - 1 到 1 符 合 绕 y 轴 的 正 ⽅ 向 , 所 以 把 y 设 为 1
即 可 : t = CATransform3DRotate(t, M_PI/9 * xf, 0, 1, 0)
最 后 我 们 加 了 ⼀ 个 回 弹 动 画 , 让 视 图 在 ⼿ 势 结 束 之 后 恢 复 成 CATransform3DIdentity
到这⾥,这⼀节又该结束了,你可以在 tvOSCardAnimationDemo
源码。
91
查看
5
自定义属性动画
92
这⼀节我们将进⼊⼀个全新的动画世界。这绝对是⼀些你未尝涉及过
的动画技巧。掌握了这些技巧,你可以实现很多之前望⽽却步的动效。
它就是 —— ⾃定义属性动画(Custom Property Animation)
M OVIE 5.1 ⾃定义动画 —— GooeyEffect
⽐ 如 像 M OVIE 5.1 这
种:
事不宜迟,我们赶紧来
看看这个效果的实现。
⾸先,我们创建两个类:Menu
和
MenuLayer 。把
MenuLayer
添加到 Menu 的 layer 上。
Menu 只负责点击事件、以及作为添加 item 的容器,动画的具体实现我
们放在 MenuLayer 中,这也符合 UIView 的 CALayer 的先天使命。
在编写这本电⼦书之前,我就希望我能给带给各位读者的不仅仅只是
问题的答案,更应该是解决问题的过程已经我的思考。这才是能帮助你
脱离这⼏个案例依然能⾃⼰完成动画的根本⽅法论。所以,我不想只给
你说下⼀步应该怎么做,我尽量把我⾃⼰从第⼀眼看到这个动画时的⼼
93
理活动到最终实现的整个过程呈现给你,希望能对你以后⾃⼰实现相应
的动画有所借鉴。
我在第⼀眼看到这个动效时,坦率地讲,我真的觉得很难实现。但我
知道,「天下武功唯快不破」。凡是让⼈⽆从下⼿的动效,很⼤程度上
是因为看不清,也就是动画的播放速度快到让你根本不知道它是怎么动
的。那么接下来就很⾃然地会想到,有什么办法可以让动画慢下来呢?
最好是可以⼿动控制关键帧地进⾏逐帧播放。如果你有视频剪辑的经验
那么你就知道最直接的⽅法就是拖⼊ Final Cut Pro 或者 PR 这类剪辑软
件中,在时间线进⾏逐帧浏览。但归功于我是⼀位重度 Keynote 使⽤
者,我在⼤学中的每次演讲都是在 Keynote 的帮助下完成的,以⾄于熟
悉到我⼀直拿它当 PS ⽤。你之前看到的所有插图都是我在 Keynote 中完
成的。然⽽现在我还要告诉你的是,Keynote 还可以让你逐帧查看视频或
GIF 。
⾸先你需要将⼀个视频或 GIF 拖⼊ Keynote ,然后选中右侧的「影
⽚」板块,通过滑动「标记帧」滑块可以实现逐帧预览。
94
I NTERACTIVE 5.1 演⽰如何在 Keynote 中逐帧查看视频或 GIF
2、选中「影⽚」
1、导⼊⼀则 GIF 或者
视频
3、滑动「标记帧」查
看逐帧动画
通过对逐帧图⽚的慢动作分析,我很快就有了思路:「依然⽤贝塞尔
曲线拼出⼀个圆,依然通过控制点的运动实现⼩球的形变」。只是这回
关 键 点 的 移 动 规 律 需 要 推 敲 ⼀ 下 。 M OVIE 5.2 是 我 完 成 的 第 ⼀ 版 初 稿 。
打开控制点可视化后,你会发现其中
的秘密:⼩球的形变其实就是由于顶上三
个点的运动产⽣的。所以核⼼问题就转化
换成了:如何确定控制点的运动轨迹?到
这⾥我们再理⼀理⽬前的思路:
95
M OVIE 5.2 Gooey Effect 第⼀版
“
CAKeyframeAnimation
layer
layer
”
下⾯我会⼀边给出代码⼀边通过警⽰符号向你做出解释。
+(BOOL)needsDisplayForKey:(NSString *)key{
if ([key isEqualToString:@"xAxisPercent"]) {
return YES;
}
return [super needsDisplayForKey:key];
}
-(void)drawInContext:(CGContextRef)ctx{
CGRect real_rect = CGRectInset(self.frame, OFF,OFF);
CGFloat offset = real_rect.size.width/ 3.6;
CGPoint center = CGPointMake(CGRectGetMidX(self.frame),
CGRectGetMidY(self.frame));
CGFloat moveDistance = _xAxisPercent*(offset);
CGPoint top_left
=
CGPointMake(center.x-offset-moveDistance, OFF);
CGPoint top_center = CGPointMake(center.x-moveDistance,
OFF);
CGPoint top_right =
CGPointMake(center.x+offset-moveDistance, OFF);
CGPoint right_top
= CGPointMake(CGRectGetMaxX(real_rect), center.y-offset);
CGPoint right_center = CGPointMake(CGRectGetMaxX(real_rect), center.y);
CGPoint right_bottom = CGPointMake(CGRectGetMaxX(real_rect), center.y+offset);
96
CGPoint bottom_left
=
CGRectGetMaxY(real_rect));
CGPoint bottom_center =
xY(real_rect));
CGPoint bottom_right =
CGRectGetMaxY(real_rect));
CGPoint left_top
=
CGPoint left_center =
CGPoint left_bottom =
CGPointMake(center.x-offset,
CGPointMake(center.x, CGRectGetMaCGPointMake(center.x+offset,
CGPointMake(OFF, center.y-offset);
CGPointMake(OFF, center.y);
CGPointMake(OFF, center.y+offset);
UIBezierPath *circlePath = [UIBezierPath bezierPath];
[circlePath moveToPoint:top_center];
[circlePath addCurveToPoint:right_center
controlPoint1:top_right controlPoint2:right_top];
[circlePath addCurveToPoint:bottom_center
controlPoint1:right_bottom controlPoint2:bottom_right];
[circlePath addCurveToPoint:left_center
controlPoint1:bottom_left controlPoint2:left_bottom];
[circlePath addCurveToPoint:top_center
controlPoint1:left_top controlPoint2:top_left];
[circlePath closePath];
CGContextAddPath(ctx, circlePath.CGPath);
CGContextSetFillColorWithColor(ctx, [UIColor
colorWithRed:29.0/255.0 green:163.0/255.0 blue:1
alpha:1].CGColor);
CGContextFillPath(ctx);
}
理 论 上 , 我 们 的 MenuLayer 只 需 要 这 两 段 代 码 就 可 以 了 。 但 我 们 还
需要重载
-(id)initWithLayer:(MenuLayer *)layer ⽅ 法 拷 贝 前 ⼀ 个
layer 的相应属性,否则当我们每次调⽤
-(void)drawInContext:(CGContextRef)ctx 当 前 l a y e r 的 属 性 都 将 是 缺
省值。
97
-(id)initWithLayer:(MenuLayer *)layer{
self = [super initWithLayer:layer];
if (self) {
//...在这⾥拷⻉layer的所有property
self.showDebug = layer.showDebug;
self.xAxisPercent = layer.xAxisPercent;
}
return self;
}
现 在 你 已 经 完 成 了 MenuLayer 中 的 所 有 代 码 。 我 们 回 到 Menu 类
中 , 看 看 应 该 如 何 触 发 MenuLayer 的 重 绘 以 及 如 何 确 定 控 制 点 的 运 动 轨
迹。
关 于 运 动 轨 迹 , 思 路 是 分 解 为 3 步 。 结 合 G ALLERY 5.1 体 会 。
G ALLERY 5.1 控制点3个状态下的不同位置
✤ 第⼀步:从初始位置开始运动到左
侧最远位置,运动距离为⼩球宽度的1/3.6
倍。
✤ 第⼆步:从左侧最远位置运动到右
侧最远位置。
✤ 第三步:从右侧最远位置以弹性动
画恢复到初始位置。
下 ⾯ 的 代 码 是 关 于 MenuLayer 的 重 绘 。
98
-(void)openAnimation{
CAKeyframeAnimation *openAnimation_1 = [[KYSpringLayerAnimation sharedAnimManager]createBasicAnima:@"xAxisPercent"
duration:0.3 fromValue:@(0) toValue:@(1)];
openAnimation_1.delegate = self;
[self.menuLayer addAnimation:openAnimation_1
forKey:@"openAnimation_1"];
}
我 创 建 了 ⼀ 个 CAKeyframeAnimation ⽤ 来 刷 新
menuLayer 的
@"xAxisPercent" 属 性 , 又 因 为 menuLayer 对 该 属 性 进 ⾏ 了 监 听 , 所 以
menuLayer 会 进 ⾏ 重 绘 。 值 得 注 意 的 是 , 我 使 ⽤ 的 是 ⾃ 定 义 的 动 画 ⽣ 成
器 — — KYSpringLayerAnimation
这⾥⾯的考量在于,通过这个⾃定
义动画⽣成器,你可以实现各种想要的效果,⽽所需要的仅仅是⼀个曲
线⽅程。
下⾯我们来看看这个动画⽣成器的源码:
-(CAKeyframeAnimation *)createBasicAnima:(NSString *)keypath
duration:(CFTimeInterval)duration fromValue:(id)fromValue
toValue:(id)toValue{
CAKeyframeAnimation *anim = [CAKeyframeAnimation
animationWithKeyPath:keypath];
anim.values = [self basicAnimationValues:fromValue
toValue:toValue duration:duration];
anim.duration = duration;
anim.fillMode = kCAFillModeForwards;
anim.removedOnCompletion = NO;
return anim;
}
99
-(NSMutableArray *) basicAnimationValues:(id)fromValue
toValue:(id)toValue duration:(CGFloat)duration{
NSInteger numOfFrames = duration * 60;
NSMutableArray *values = [NSMutableArray
arrayWithCapacity:numOfFrames];
for (NSInteger i = 0; i < numOfFrames; i++) {
[values addObject:@(0.0)];
}
CGFloat diff = [toValue floatValue] - [fromValue floatValue];
for (NSInteger frame = 0; frame= 0.2) {
hightFactor = 1-_xAxisPercent;
}else{
hightFactor = _xAxisPercent;
}
moveDistance_1 = (real_rect.size.width/2 - offset)/2;
moveDistance_2 =
_xAxisPercent*(real_rect.size.width/3);
top_left
=
CGPointMake(center.x-offset-moveDistance_1*2 + moveDistance_2,
OFF);
106
top_center = CGPointMake(center.x-moveDistance_1 +
moveDistance_2, OFF);
top_right =
CGPointMake(center.x+offset+moveDistance_2, OFF);
}else if(_animState == STATE3){
moveDistance_1 = (real_rect.size.width/2 - offset)/2;
moveDistance_2 = (real_rect.size.width/3);
CGFloat gooeyDis_1 =
_xAxisPercent*(center.x-offset-moveDistance_1*2 +
moveDistance_2-(center.x-offset));
CGFloat gooeyDis_2 =
_xAxisPercent*(center.x-moveDistance_1 +
moveDistance_2-(center.x));
CGFloat gooeyDis_3 =
_xAxisPercent*(center.x+offset+moveDistance_2-(center.x+offset)
);
top_left
=
CGPointMake(center.x-offset-moveDistance_1*2 + moveDistance_2 gooeyDis_1, OFF);
top_center = CGPointMake(center.x-moveDistance_1 +
moveDistance_2 - gooeyDis_2, OFF);
top_right =
CGPointMake(center.x+offset+moveDistance_2 - gooeyDis_3, OFF);
}
//和
.....
⼀样
}
⾃定义动画的这⼀章就先到这⾥了。你可以在 KYGooeyMenuDemo 中
找到本节的源码。
107
6
其他效果
108
这⼀章节的内容,属于⼀些我暂时⽆法归类但
都⾮常有趣的玩意⼉。⽐如我会在这⼀章中介绍
⼀些动画的⼩技巧,或者是⼀些使⽤得⽐较少的
iOS SDK 中的动画类。
本章第⼀节,我打算先介绍的是 ——
UIKitDynamics 。可以说 UIKitDynamics 这个 iOS7 中引⼊的类平常⽤的并
不算多,但是它所展现的视觉效果⼀定是让你印象深刻的。
下⾯我通过⼀个模拟 Smartisan OS 中锁屏界⾯的 demo
给你介绍⼀下 UIKitDynamics 的使⽤⽅法。具体效果请见
M OVIE 6.1 。
M OVIE 6.1 模拟 Smartisan OS 锁屏界⾯
在这个效果中,我⼀共
使⽤了五个 Behavior:
UIGravityBehavior
UIPushBehavior
109
UIAttachmentBehavior
UIDynamicItemBehavior
UICollisionBehavior
以及⼀个物理引擎的容器 UIDynamicAnimator,所有 Behavior 都要加进
UIDynamicAnimator 中才能奏效。
⾸先我们先来实现点击⼀次发⽣弹跳的效果。准备⼯作:设置好封⾯
图,绑定⼀个单击⼿势,准备好所需的 Behavior 以及初始化物理引擎容
器 UIDynamicAnimator:
- (void)viewDidLoad {
[super viewDidLoad];
self.lockScreenView = [[UIImageView
alloc]initWithFrame:self.view.bounds];
self.lockScreenView.image = [UIImage
imageNamed:@"lockScreen"];
self.lockScreenView.contentMode = UIViewContentModeScaleToFill;
self.lockScreenView.userInteractionEnabled = YES;
[self.view addSubview:_lockScreenView];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer
alloc]initWithTarget:self action:@selector(tapOnIt:)];
[self.lockScreenView addGestureRecognizer:tap];
}
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
110
self.animator = [[UIDynamicAnimator
alloc]initWithReferenceView:self.view];
UICollisionBehavior *collisionBehaviour = [[UICollisionBehavior alloc]initWithItems:@[self.lockScreenView]];
[collisionBehaviour
setTranslatesReferenceBoundsIntoBoundaryWithInsets:UIEdgeInsets
Make(-_lockScreenView.frame.size.height, 0, 0, 0)];
[self.animator addBehavior:collisionBehaviour];
self.gravityBehaviour = [[UIGravityBehavior alloc]
initWithItems:@[self.lockScreenView]];
self.gravityBehaviour.gravityDirection = CGVectorMake(0.0,
1.0);
self.gravityBehaviour.magnitude = 2.6f;
[self.animator addBehavior:self.gravityBehaviour];
self.pushBehavior = [[UIPushBehavior alloc]
initWithItems:@[self.lockScreenView]
mode:UIPushBehaviorModeInstantaneous];
self.pushBehavior.magnitude = 2.0f;
self.pushBehavior.angle = M_PI;
[self.animator addBehavior:self.pushBehavior];
self.itemBehaviour = [[UIDynamicItemBehavior alloc]
initWithItems:@[self.lockScreenView]];
self.itemBehaviour.elasticity = 0.35f;
[self.animator addBehavior:_itemBehaviour];
}
111
现 在 , 我 们 处 理 点 击 的 ⼿ 势 , 点 击 ⼀ 次 触 发 ⼀ 个 pushBehavior, 并 设 置 它 的 pushDirection 为 竖 直 向 上 的 CGVectorMake(0.0f,
-80.0f)。
-(void)tapOnIt:(UITapGestureRecognizer *)tapGes{
self.pushBehavior.pushDirection = CGVectorMake(0.0f,
-80.0f);
self.pushBehavior.active = YES;
}
接 着 我 们 来 看 看 如 何 实 现 ⼿ 势 拖 拽 。 ⾸ 先 处 理 UIPanGestureRecognizer。
-(void)panOnIt:(UIPanGestureRecognizer *)panGes{
CGPoint location =
CGPointMake(CGRectGetMidX(_lockScreenView.frame), [panGes
locationInView:self.view].y);
if (panGes.state == UIGestureRecognizerStateBegan) {
[self.animator removeBehavior:self.gravityBehaviour];
self.attachmentBehaviour = [[UIAttachmentBehavior
alloc]initWithItem:self.lockScreenView
attachedToAnchor:location];
[self.animator addBehavior:_attachmentBehaviour];
}else if (panGes.state == UIGestureRecognizerStateChanged){
self.attachmentBehaviour.anchorPoint = location;
}else if (panGes.state == UIGestureRecognizerStateEnded){
112
CGPoint velocity = [panGes
velocityInView:_lockScreenView];
NSLog(@"v:%@",NSStringFromCGPoint(velocity));
[self.animator removeBehavior:_attachmentBehaviour];
_attachmentBehaviour = nil;
if (velocity.y < -1300.0f) {
[self.animator removeBehavior:_gravityBehaviour];
[self.animator removeBehavior:_itemBehaviour];
_gravityBehaviour = nil;
_itemBehaviour = nil;
//gravity
self.gravityBehaviour = [[UIGravityBehavior alloc]
initWithItems:@[self.lockScreenView]];
self.gravityBehaviour.gravityDirection =
CGVectorMake(0.0, -1.0);
self.gravityBehaviour.magnitude = 2.6f;
[self.animator addBehavior:self.gravityBehaviour];
//item
self.itemBehaviour = [[UIDynamicItemBehavior alloc]
initWithItems:@[self.lockScreenView]];
self.itemBehaviour.elasticity = 0.0f;//1.0 完全弹性碰
撞,需要⾮常久才能恢复;
[self.animator addBehavior:_itemBehaviour];
self.pushBehavior.pushDirection =
CGVectorMake(0.0f, -200.0f);
self.pushBehavior.active = YES;
}else{
[self restore];
}
}
}
UIAttachmentBehavior , 从 这 个 名 字 就 可 以 看 出 这 个 动 作 是 有 附
着 、 附 加 的 效 果 。 其 实 就 是 视 图 的 锚 点 ( anchorPoint) 始 终 和 某 个 指 定
113
的 点 保 持 ⼀ 段 length 距 离 的 效 果 。 这 种 连 接 可 以 是 两 个 i t e m 之 间 的 连
接 ( 两 个 i t e m 的 anchorPoint 之 间 的 距 离 ) , 也 可 以 是 ⼀ 个 i t e m 和 ⼀ 个
点之间的连接。其他⼀些属性:
两个点之间的距离:
@property (readwrite, nonatomic) CGFloat length;
阻尼系数 (0~1 之间;默认为 0):
@property (readwrite, nonatomic) CGFloat damping; // 1: critical damping
弹性系数 (值越⼤ item 的弹性效果就越明显):
@property (readwrite, nonatomic) CGFloat frequency; // in Hertz
通过这些属性,你可以想象成两个点之间是通过⼀条「⽊棍」刚性连
接着,或者是⼀根「橡⽪筋」弹性连接着。对于这个 demo,⾸先我们在⼿势开始时先确定这个⽬标点:
CGPoint location =
CGPointMake(CGRectGetMidX(_lockScreenView.frame), [panGes
locationInView:self.view].y);
然后创建动作。因为这个 demo 中 item 需要跟⼿,所以我们不需要
弹 性 也 不 需 要 阻 尼 , 都 使 ⽤ 默 认 值 。 然 后 让 i t e m 的 anchorPoint 跟 着
这个⽬标点,想象成两者之间连着⼀根「⽊棍」。
114
最后在⼿势结束时根据移动的速度判断是否成功。
CGPoint velocity = [panGes velocityInView:_lockScreenView];
好了,这个简单的 UIDynamicKit 的
demo 就到这⾥了。源码参见 UIDynami c s D e m o 。 除 了 UISnapBehavior 之
外,这个 demo 基本涉及了所有的动
作。类似的弹跳效果还出现在了 Mac OS X 的 dock 中。
⾄ 于 UISnapBehavior ( 吸 附 动 作 ) 其 实 是 所 有 动 作 中 最 简 单 的 。 因
为它只有这⼀个⽅法和⼀个属性:
- (instancetype)initWithItem:(id )item
snapToPoint:(CGPoint)point;
@property (nonatomic, assign) CGFloat damping; // damping value
from 0.0 to 1.0. 0.0 is the least oscillation.
绑定⼀个 item ,指定⼀个吸附
M OVIE 6.2
UISnapBehaior 吸附效果的的简单演⽰
的⽬标点,设置⼀个弹性系数,最
后 别 忘 了 加 到 UIDynamicAnimator
中,⼀切⼯作就结束了。然后就只
需 要 静 静 地 看 效 果 ( M OVIE 6.2 ) 。
115
接 下去我们来玩点有意思的。我特意抽出这⼀节向你
介绍 UIKitDynamics 中⼀个神奇的特性 ——
Action
Block.
// When running, the dynamic animator calls the action block on
every animation step.
@property (nonatomic,copy) void (^action)(void);
任 何 ⼀ 个 UIDynamicBehavior 都 能 够 实 时 调 ⽤ 这 个 action 。 举 个
例⼦,⽐如你可以实现下⾯这些效果:
M OVIE 6.3 UIDynamicBehavior 中 action
的效果⽰范01
M OVIE 6.4 UIDynamicBehavior 中 action
的效果⽰范02
下⾯我就拿第⼆个例⼦作为本章的第⼆个 demo 。⾸先⼀起来看下原
理视频:
116
这个动画的思路其实很简单:给
M OVIE 6.5 UIDynamicBehavior 中 action 的效果
⽰范02 -- 原理解析
最 上 ⽅ 的 矩 形 添 加 UIGravityBehavior 和
ior
UICollisionBehav-
在其下⽅添加三个
UIAttachmentBehavior
分别连接着
下⽅最近的对象。同时,我们把第⼀
个灰块、第三个灰块以及⼩球这三个
视 图 绑 定 到 同 ⼀ 个 UIGravityBehavior 上 ⾯ , 叫 做 viewsGravity, 在
viewsGravity 的 action block 中 , 把 第 ⼀ 个 灰 块 、 第 三 个 灰 块 作 为 ⼀
条贝塞尔曲线的两个控制点,实时地绘制出这条曲线,也就是你看到的
「绳⼦」的样⼦了。
然后详细看⼀下代码细节:
- (void)viewDidLoad {
[super viewDidLoad];
_panView=[[UIView alloc] initWithFrame:CGRectMake(0, 0,
[[UIScreen mainScreen] bounds].size.width, [[UIScreen mainScreen] bounds].size.height/2)];
[_panView setAlpha:0.5];
[self.view addSubview:_panView];
[_panView.layer setShadowOffset:CGSizeMake(-1, 2)];
[_panView.layer setShadowOpacity:0.5];
[_panView.layer setShadowRadius:5.0];
[_panView.layer setShadowColor:[UIColor
blackColor].CGColor];
117
[_panView.layer setMasksToBounds:NO];
[_panView.layer setShadowPath:[UIBezierPath
bezierPathWithRect:_panView.bounds].CGPath];
UIPanGestureRecognizer *pan=[[UIPanGestureRecognizer alloc]
initWithTarget:self action:@selector(PanTheView:)];
[_panView addGestureRecognizer:pan];
CAGradientLayer *grd=[[CAGradientLayer alloc] init];
[grd setFrame:_panView.frame];
grd.colors = [[NSArray alloc] initWithObjects:(__bridge
id)([UIColor colorWithRed:0.0 green:191.0/255.0
blue:255.0/255.0 alpha:1].CGColor),(__bridge id)([UIColor
whiteColor].CGColor), nil];
[_panView.layer addSublayer:grd];
_ballImageView=[[UIImageView alloc]
initWithFrame:CGRectMake(([[UIScreen mainScreen]
bounds].size.width/2)-30, [[UIScreen mainScreen]
bounds].size.height/1.5, 60, 60)];
[_ballImageView setImage:[UIImage imageNamed:@"ball"]];
[self.view addSubview:_ballImageView];
[_ballImageView.layer setShadowOffset:CGSizeMake(-4, 4)];
[_ballImageView.layer setShadowOpacity:0.5];
[_ballImageView.layer setShadowRadius:5.0];
[_ballImageView.layer setShadowColor:[UIColor
blackColor].CGColor];
[_ballImageView.layer setMasksToBounds:NO];
//1
_middleView = [[UIView alloc]
initWithFrame:CGRectMake(_ballImageView.center.x-15, 200, 30,
30)];
[_middleView setBackgroundColor:[UIColor grayColor]];
[self.view addSubview:_middleView];
[_middleView setCenter:CGPointMake(_middleView.center.x,
(_ballImageView.center.y-_panView.center.y)+15)];
//2
118
_topView = [[UIView alloc]
initWithFrame:CGRectMake(_ballImageView.center.x-15, 200, 30,
30)];
[_topView setBackgroundColor:[UIColor grayColor]];
[self.view addSubview:_topView];
[_topView setCenter:CGPointMake(_topView.center.x,
(_middleView.center.y-_panView.center.y)+_panView.center.y/2)];
//3
_bottomView = [[UIView alloc]
initWithFrame:CGRectMake(_ballImageView.center.x-15, 200, 30,
30)];
[_bottomView setBackgroundColor:[UIColor grayColor]];
[self.view addSubview:_bottomView];
[_bottomView setCenter:CGPointMake(_bottomView.center.x,
(_middleView.center.y-_panView.center.y)+_panView.center.y*1.5)
];
[self setUpBehaviors];
}
代 码 虽 然 多 但 就 是 简 单 的 布 局 ⽽ 已 。 唯 ⼀ 需 要 ⼀ 提 的 就 是 CAGradientLayer 。 这 是 中 的 ⼀ 个 类 , 估 计 平 时 接 触
的机会也⽐较少。其实这个类实现的渐变⾊效果⾮常有⽤,有了它你不
再需要设计师提供的渐变⾊的切图了,⼀个常⽤的场景就是配图的⽂字
背景。
@property(copy) NSArray *colors;
119
//
@property(copy) NSArray *locations; //
@[@(0.0),@(0.4),@(1.0)],
nil
0~1
@property CGPoint startPoint;
@property CGPoint endPoint; //
[1,1]
[.5,0]
[.5,1]
[0,0]
下⾯来看⼀下如何初始化 UIKitDynamics。
-(void)setUpBehaviors{
_animator=[[UIDynamicAnimator alloc]
initWithReferenceView:self.view];
_panGravity=[[UIGravityBehavior alloc]
initWithItems:@[_panView]];
[_animator addBehavior:_panGravity];
_viewsGravity=[[UIGravityBehavior alloc]
initWithItems:@[_ballImageView,_topView,_bottomView]];
[_animator addBehavior:_viewsGravity];
__weak ViewController *weakSelf = self;
_viewsGravity.action=^{
NSLog(@"acting");
UIBezierPath *path=[[UIBezierPath alloc] init];
[path moveToPoint:weakSelf.panView.center];
[path addCurveToPoint:weakSelf.ballImageView.center
controlPoint1:weakSelf.topView.center
controlPoint2:weakSelf.bottomView.center];
if (!weakSelf.layer) {
weakSelf.layer=[[CAShapeLayer alloc] init];
weakSelf.layer.fillColor = [UIColor
clearColor].CGColor;
weakSelf.layer.strokeColor = [UIColor
colorWithRed:224.0/255.0 green:0.0/255.0 blue:35.0/255.0
alpha:1.0].CGColor;
120
weakSelf.layer.lineWidth = 5.0;
//Shadow
[weakSelf.layer
[weakSelf.layer
[weakSelf.layer
[weakSelf.layer
blackColor].CGColor];
[weakSelf.layer
setShadowOffset:CGSizeMake(-1, 2)];
setShadowOpacity:0.5];
setShadowRadius:5.0];
setShadowColor:[UIColor
setMasksToBounds:NO];
[weakSelf.view.layer insertSublayer:weakSelf.layer
below:weakSelf.ballImageView.layer];
}
weakSelf.layer.path=path.CGPath;
};
//UICollisionBehavior
UICollisionBehavior *Collision=[[UICollisionBehavior alloc]
initWithItems:@[_panView]];
[Collision addBoundaryWithIdentifier:@"Left"
fromPoint:CGPointMake(-1, 0) toPoint:CGPointMake(-1, [[UIScreen
mainScreen] bounds].size.height)];
[Collision addBoundaryWithIdentifier:@"Right"
fromPoint:CGPointMake([[UIScreen mainScreen]
bounds].size.width+1,0) toPoint:CGPointMake([[UIScreen mainScreen] bounds].size.width+1, [[UIScreen mainScreen]
bounds].size.height)];
[Collision addBoundaryWithIdentifier:@"Middle"
fromPoint:CGPointMake(0, [[UIScreen mainScreen]
bounds].size.height/2) toPoint:CGPointMake([[UIScreen mainScreen] bounds].size.width, [[UIScreen mainScreen]
bounds].size.height/2)];
[_animator addBehavior:Collision];
//3 UIAttachmentBehaviors
UIAttachmentBehavior *attach1=[[UIAttachmentBehavior
alloc]initWithItem:_panView attachedToItem:_topView];
[_animator addBehavior:attach1];
UIAttachmentBehavior *attach2=[[UIAttachmentBehavior alloc]
initWithItem:_topView attachedToItem:_bottomView];
121
[_animator addBehavior:attach2];
UIAttachmentBehavior *attach3=[[UIAttachmentBehavior alloc]
initWithItem:_bottomView offsetFromCenter:UIOffsetMake(0, 0)
attachedToItem:_ballImageView offsetFromCenter:UIOffsetMake(0,
-_ballImageView.bounds.size.height/2)];
[_animator addBehavior:attach3];
//UIDynamicItemBehavior
UIDynamicItemBehavior *PanItem=[[UIDynamicItemBehavior alloc]
initWithItems:@[_panView,_topView,_bottomView,_ballImageView]];
PanItem.elasticity=0.5;
[_animator addBehavior:PanItem];
}
我 们 把 _panView 单 独 绑 定 ⼀ 个
UIGravityBehavior 把 @[_bal-
lImageView,_topView,_bottomView] 也 绑 定 到 ⼀ 个 UIGravityBehavior
上 。 然 后 设 置 ⼀ 个 作 ⽤ 于 _panView 的 UICollisionBehavior
下 ⾯ 是 关 键 , 我 们 创 建 三 个 UIAttachmentBehavior 分 别 让 上 ⼀ 个
视图 attach 下⽅相邻的视图,想象成「⼀环扣⼀环」的样⼦。
有 个 细 节 需 要 提 ⼀ 下 , 就 是 这 ⾥ 初 始 化 第 三 个 UIAttachmentBehavior 的 时 候 ⽤ 到 了
- (instancetype)initWithItem:(id )item1
offsetFromCenter:(UIOffset)offset1 attachedToItem:(id )item2 offsetFromCenter:(UIOffset)offset2;
122
这个⽅法。因为我们希望⼩球可以绕顶点转起来,所以把⼩球上的锚点
设置在了⼩球的的顶端,⽽不是⼩球默认的正中⼼,否则你将看不到⼩
球的转动。
接 下 来 是 必 不 可 少 的 ⼀ ⾏ 代 码 , 在 _panView 的 ⼿ 势 绑 定 ⽅ 法 中 , 我
们需要实时调⽤这句:
[_animator updateItemUsingCurrentState:pan.view];
我们来看⼀下官⽅⽂档是怎么说的:
// Update the item state in the animator if an external change
was made to this item
(void)updateItemUsingCurrentState:(id )item;
Asks a dynamic animator to read the current state of a dynamic item, replacing the
animator ’s internal representation of the item’s state.A dynamic animator automatically reads the initial state (position and rotation) of each dynamic item you add to
it, and then takes responsibility for updating the item’s state. If you actively change
the state of a dynamic item after you’ve added it to a dynamic animator, call this
method to ask the animator to read and incorporate the new state.
⽂ 档 中 说 , 当 外 界 变 化 ( ⽐ 如 _panGravity 开 始 作 ⽤ 了 ) 作 ⽤ 于
pan.View 上 时 , 去 刷 新 当 前 _animator 中 所 有 i t e m 的 p o s i t i o n 和 r o t a t i o n 。 ⽽ 这 些 i t e m 包 括 着 _ballImageView,_topView,_bottomView。 p o -
123
s i t i o n 和 r o t a t i o n 的 变 化 又 会 触 发 _ballImageView,_topView,_bottomView 这 三 者 绑 定 的 _viewsGravity.action. 在 这 个 action 中 实 时 绘 制
⼀ 条 贝 塞 尔 曲 线 , 就 有 了 你 看 到 的 ⼀ 条 弹 性 的 绳 ⼦ 。 由 于 这 个 action 会
在 动 画 的 每 ⼀ 步 都 会 调 ⽤ ( on every animation step) , 所 以 动 画 会 显
得相当流畅。
这 个 d e m o 主 要 介 绍 了 action 的 作 ⽤ 以 及 updateItemUsingCurrentState ⽅ 法 的 使 ⽤ 。 掌 握 了 这 个 技 巧 , 你 可 以 创 造 出 更 多 神 奇 的 物 理 特
效。源码详见 DynamicActionBlockDemo。
第三⼩节,我们来聊聊另⼀个⾮常具有视觉冲击⼒的效
果 — — 粒 ⼦ 效 果 ( CAEmitterLayer) 。 在 i O S 5 中 , 苹
果 引 ⼊ 了 ⼀ 个 新 的 C A L a y e r ⼦ 类 — — CAEmitterLayer。 CAEmitterLayer 是 ⼀ 个 ⾼ 性 能 的 粒 ⼦ 引 擎 , 被 ⽤
来创建复杂的粒⼦动画如:烟雾,⽕,⾬等效果,并且很好
地控制了性能。
124
关于
CAEmitterLayer, iOS Core Animation: Advanced Tech-
niques 中 给 出 了 解 释 :
“
CAEmitterLayer
CAEmitterCell
CAEmitterCell
CAEmitterCell
CAEmitterLayer
Cell
CALayer
CAEmittercontents
CGImage
”
下 ⾯ 我 们 深 ⼊ 到 头 ⽂ 件 中 , 并 ⽤ ⼀ 个 d e m o 介 绍 CAEmitterLayer 的
具体⽤法。
M OVIE 6.6 CAEmitterLayer 展⽰下雪效果
⽐如这个很经典的下雪效果:
代码如下。由于⽐较简单,我直接
⽤尖帽符做了解释。:
CAEmitterLayer *snowEmitter = [CAEmitterLayer layer];
snowEmitter.emitterPosition =
CGPointMake(self.view.bounds.size.width / 2.0, -30);
snowEmitter.emitterSize =
CGSizeMake(self.view.bounds.size.width * 2.0, 0.0);;
125
snowEmitter.emitterShape = kCAEmitterLayerLine;
snowEmitter.emitterMode = kCAEmitterLayerOutline;
CAEmitterCell *snowflake = [CAEmitterCell emitterCell];
snowflake.birthRate = 1.0;
snowflake.lifetime
= 120.0;
snowflake.velocity
= -10;
snowflake.velocityRange = 10;
snowflake.yAcceleration = 2;
snowflake.emissionRange = 0.5 * M_PI;
snowflake.spinRange = 0.25 * M_PI;
snowflake.contents = (id) [[UIImage imageNamed:@"snow"]
CGImage];
snowflake.color
= [[UIColor colorWithRed:0.600 green:0.658
blue:0.743 alpha:1.000] CGColor];
snowEmitter.shadowOpacity
snowEmitter.shadowRadius
snowEmitter.shadowOffset
snowEmitter.shadowColor
=
=
=
=
1.0;
0.0;
CGSizeMake(0.0, 1.0);
[[UIColor whiteColor] CGColor];
snowEmitter.emitterCells = [NSArray
arrayWithObject:snowflake];
[self.view.layer insertSublayer:snowEmitter atIndex:0];
这个简单的 demo 就到这⾥,代码参见 SnowEffectDemo。
126
下⾯我再追加⼀个例⼦,这个例⼦是 Github 上的⼀
个开源库 —— MCFireworksButton。我觉得⾮常具有代
表 性 。 不 仅 效 果 出 ⾊ , 还 涉 及 到 了 如 何 ⼿ 动 控 制 CAEmitterLayer 动 画
M OVIE 6.7 溅出⽔花的点赞效果
的开始与结束。这也正是我想额外介
绍的⼀点。
效 果 参 见 M OVIE 6.8。
先看它的初始化代码:
- (void)setup {
CAEmitterCell *explosionCell = [CAEmitterCell emitterCell];
explosionCell.name = @"explosion";
explosionCell.alphaRange = 0.20;
explosionCell.alphaSpeed = -1.0;
explosionCell.lifetime = 0.7;
explosionCell.lifetimeRange = 0.3;
explosionCell.birthRate = 0;
explosionCell.velocity = 40.00;
explosionCell.velocityRange = 10.00;
explosionCell.contents = (id)[[UIImage
imageNamed:@"Like-Blue"] CGImage];
explosionCell.scale = 0.05;
explosionCell.scaleRange = 0.02; //
scale
contents
_explosionLayer = [CAEmitterLayer layer];
_explosionLayer.name = @"emitterLayer";
_explosionLayer.emitterShape = kCAEmitterLayerCircle;
127
_explosionLayer.emitterMode = kCAEmitterLayerOutline;
_explosionLayer.emitterPosition =
CGPointMake(CGRectGetMidX(self.bounds),
CGRectGetMidY(self.bounds));
_explosionLayer.emitterSize = CGSizeMake(25, 0);
_explosionLayer.emitterCells = @[explosionCell];
_explosionLayer.renderMode = kCAEmitterLayerOldestFirst;
_explosionLayer.masksToBounds = NO;
[self.layer addSublayer:_explosionLayer];
self.emitterCells = @[explosionCell];
}
接下来我们需要⼿动控制动画的开始和结束。还记得前⾯提到的
@property(copy) NSString *name; 吗 ? 想 要 ⼿ 动 控 制 动 画 的 开 始 和 结
束,我们必须通过 KVC 的⽅式设置 cell 的值才⾏。
动画开始:
- (void)explode {
self.explosionLayer.beginTime = CACurrentMediaTime();
[self.explosionLayer setValue:@500
forKeyPath:@"emitterCells.explosion.birthRate"];
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW,
0.1 * NSEC_PER_SEC);
dispatch_after(delay, dispatch_get_main_queue(), ^{
[self stop];
});
}
对
[self.explosionLayer setValue:@500
forKeyPath:@"emitterCells.explosion.birthRate"];
128
这句话的解读
是 : CAEmitterLayer 根 据 ⾃ ⼰ 的 emitterCells 属 性 找 到 名 叫
sion 的 c e l l , 并 设 置 它 的
explo-
birthRate 为 5 0 0 。 从 ⽽ 间 接 地 控 制 了 动 画 的
开始。
同理,停⽌动画的思路也⼀样。通过设置
birthRate 为 0
间接地
控制了动画的结束。
动画停⽌:
- (void)stop {
[self.explosionLayer setValue:@0
forKeyPath:@"emitterCells.explosion.birthRate"];
}
!
到这⾥,这个 demo 基本就讲完了,你可以在 MCFireworksButton-
Demo 这⾥找到源码。
以 上 两 个 d e m o 涉 及 了 CAEmitterLayer 会 ⽤ 到 的 绝 ⼤ 多 数 知 识 点 ,
到最后可能真正费时的就是调参数了。没错,参数调节⼀直是⼀个优秀
的动画的重要组成部分。
129
这 ⼀ ⼩ 节 , 我 们 讲 介 绍 ⼀ 个 很 ⼩ 众 的 类 — — CAReplicatorLayer 。
我们先来看⼀下它能实现什么样的效果。
从 M OVIE 6.8 中 可 以 看 到 , 每 ⼀ 个 动
M OVIE 6.8 CAReplicatorLayer Demo
画中都存在好⼏个重复的元素。没错,
这 就 是 CAReplicatorLayer 的 最 ⼤ 特
点,可以重复任意个数的元素按你期望
的布局排列,甚⾄,可以让每个元素拥
有动画,制作出这种类似 loading 效果
的视觉效果。
⾸先我们来看⼀下官⽅的解释:
The CAReplicatorLayer class creates a specified number of copies of its sublayers
(the source layer), each copy potentially having geometric, temporal and color transformations applied to it.
130
简单来说,它⾃⼰能够重建包括⾃⼰在内的 n 个 copies,这些 copies 是原 layer 中的所有 sublayers,并且任何对原 layer 的 sublayers 设
置的变化是可以积累的 (accumulative) 。我们以视频中的脉冲动画为
例 ⼦ 详 细 介 绍 ⼀ 下 CAReplicatorLayer 的 ⽤ 法 。
⾸ 先 画 ⼀ 个 圆 形 的 CAShapeLayer , 作 为 CAReplicatorLayer 将 要 复
制的原型,也可以理解为是第⼀个元素,下⾯的代码我设置了第⼀个元
素的起始位置位于 (0,0),在下⾯的介绍中你会认识到这很重要,因为
这决定了接下去的元素该怎么排列。
let pulseLayer = CAShapeLayer()
pulseLayer.frame = bounds
pulseLayer.path = UIBezierPath(ovalInRect:
pulseLayer.bounds).CGPath
pulseLayer.fillColor = color.CGColor
接 下 来 , 创 建 CAReplicatorLayer , 并 且 做 ⼀ 些 必 要 的 初 始 化 。 其
中 instanceCount 指 定 了 ⼀ 共 显 ⽰ 元 素 的 数 ⽬ , instanceDelay 指 定 了
重 复 元 素 之 间 的 时 间 间 隔 , 单 位 为 秒 。 最 后 别 忘 了 把 之 前 创 建 的 pulseLayer 作 为 ⼦ 图 层 添 加 到 CAReplicatorLayer 上 。
let replicatorLayer = CAReplicatorLayer()
replicatorLayer.frame = bounds
replicatorLayer.instanceCount = 8
replicatorLayer.instanceDelay = 0.5
replicatorLayer.addSublayer(pulseLayer)
131
pulseLayer.opacity = 0.0
layer.addSublayer(replicatorLayer)
此时,你如果运⾏程序你会看到⼀个居中的圆。不是说有三个吗?没
错,的确有三个,只是重叠在⼀起了(如果你想让每个元素的位置不⼀
样 , 请 继 续 往 下 看 ) 。 现 在 我 们 让 每 个 圆 圈 动 起 来 , 因 为 CAReplicatorLayer 会 ⾃ 动 复 制 其 上 的 所 有 图 层 , 所 以 我 们 只 要 给 第 ⼀ 个 元 素 加 上 动
画 , 之 后 复 制 出 来 的 元 素 都 会 继 承 这 个 动 画 , 又 因 为 前 ⾯ 我 们 设 置 了 instanceDelay , 所 以 每 个 动 画 都 会 以 相 同 间 隔 错 开 , 从 ⽽ 连 接 成 ⼀ 个 完
整的动画。
下 ⾯ 我 们 来 实 现 具 体 的 动 画 , 这 是 ⼀ 个 CAAnimationGroup 动 画 , 分
别是透明度 1~0 的动画和缩放 0~1 的动画。
func startToPluse() {
let groupAnimation = CAAnimationGroup()
groupAnimation.animations = [alphaAnimation(), scaleAnimation()]
groupAnimation.duration = 4.0
groupAnimation.autoreverses = false
groupAnimation.repeatCount = HUGE
pulseLayer.addAnimation(groupAnimation, forKey: "groupAnimation")
}
private func alphaAnimation() -> CABasicAnimation{
let alphaAnim = CABasicAnimation(keyPath: "opacity")
alphaAnim.fromValue = NSNumber(float: 1.0)
alphaAnim.toValue = NSNumber(float: 0.0)
return
}
132
private func scaleAnimation() -> CABasicAnimation{
let scaleAnim = CABasicAnimation(keyPath: "transform")
let t = CATransform3DIdentity
let t2 = CATransform3DScale(t, 0.0, 0.0, 0.0)
scaleAnim.fromValue = NSValue.init(CATransform3D: t2)
let t3 = CATransform3DScale(t, 1.0, 1.0, 0.0)
scaleAnim.toValue = NSValue.init(CATransform3D: t3)
return scaleAnim
}
这⾥我还要特别指出⼀点。你能发现下⾯三个数值之间的关系吗?
replicatorLayer.instanceDelay = 0.5
replicatorLayer.instanceCount = 8
groupAnimation.duration = 4.0
如果你注意到了 0.5*8 = 4.0 的话,请不要怀疑这是个巧合。这是
动画连贯的必要条件。请看下⾯张图。
虚线处是⼀个元素完整动画所需要的时
间。如果此时想让第⼀个元素第⼆遍动画
⽴刻衔接上,那么就必须满⾜:
duration
=
instanceCount
instanceDelay
必须满从图中也可以看出。
133
*
到这⾥,我们已经完成了这个动画的全部⼯作。这个动画是最简单的
CAReplicatorLayer 动 画 , 我 们 只 是 对 CAReplicatorLayer 有 了 ⼀ 个 简
单的认识,下⾯我们来看看如何实现元素的⾃定义布局。
⽐如上⾯视频⾥的第⼀个动画,三个⽔平排列的翻转⽅块。
这 ⾥ 就 要 介 绍 instanceTransform 这 个 属 性 了 。 这 是 ⾃ 定 义 布 局
的关键。还记得第⼀个元素的坐标
吗 ? 当 我 们 设 定 了 第 ⼀ 个 元 素 的 坐 标 , 之 后 的 元 素 的 坐 标 都 会 根 据 instanceTransform 进 ⾏ 累 加 。 ⽐ 如 这 个 并 排 的 翻 转 ⽅ 块 , 第 ⼀ 个 元 素 的
坐 标 是 ( 0 , h e i g h t * 1 / 2 ) , 设 置 instanceTransform 为
replicatorLayer.instanceTransform = CATransform3DMakeTranslation(translationX, 0, 0) , 所 以 第 ⼆ 个 元 素 的 位 置 就 是 第 ⼀ 个 元 素 的
X 轴 坐 标 平 移 translationX 的 距 离 , 第 三 个 元 素 的 坐 标 就 是 第 ⼆ 个 元
素 的 X 轴 坐 标 平 移 translationX 的 距 离 , 依 此 类 推 。
下⾯我们再来点旋转。⽐如像这个布局该怎么实现?
我们可以设置第⼀个元素的位置位于(0,0),
后⼀个元素始终是前⼀个元素沿 X 轴坐标平移⼀段距
离,同时绕⾃⾝中点旋转 120 度(圆周的三等分)。
134
var transform = CATransform3DIdentity
transform = CATransform3DTranslate(transform, dotSize, 0, 0.0)
transform = CATransform3DRotate(transform,
(CGFloat(360/replicatorLayer.instanceCount)*CGFloat(M_PI))/CGFl
oat(180.0), 0.0, 0.0, 1.0)
replicatorLayer.instanceTransform = transform
考虑到圆形⽆论怎么旋转都⼀样,不利于我们的分析,所以我⽤
了⼀个异形来直观地体现元素之间到底做了怎样的变换。请看下图。
左上⾓的⽅块是第⼀个元素,正朝向为圆⾓向上。
随后,元素中点平移规定距离,再顺时针旋转 120
度,这就是第⼆个元素的位置了。注意!你知道此时第
⼆个元素的坐标系统是怎么样的吗?因为这直接关系到
下⼀个元素的坐标。
右图中我标注了第⼆个元素的坐标⽰意图。
是 的 , 也 就 是 说 , 当 我 们 设 置 了 instanceTransform , 发 ⽣ 了 旋 转 之 后 , 其 坐 标 轴 也 发 ⽣ 了 旋 转 。
这就很好解释了为什么第三个元素会出现在前两个
元素的下⽅,因为我们规定了后⼀个元素是前⼀个元素
沿着 X 轴正⽅向平移⼀段距离同时绕⾃⾝中点旋转 120 度获得的。
135
相 信 现 在 你 对 instanceTransform 有 了 更 深 刻 的 理 解 。 下 ⾯ 我 们 来
看⼀看视频中第三个 3*3 的矩阵排列的实现思路。
这⾥我使⽤的⼀个技巧就是
⽤ 两 个 CAReplicatorLayer
。 replicatorLayerX 先 ⽔
平 复 制 出 三 个 圆 圈 , replicatorLayerY 再 竖 直 复 制 出
三 个 replicatorLayerX
就可以创建出⼀个 3*3 排列的矩阵了。
以上三个 demo 我封装到了⼀个库⾥,名字叫
ReplicatorLoaderDemo-Swift
关 于 CAReplicatorLayer 其 实 还 有 ⼀ 个 「 隐 蔽 」 技 能 , 就 是 ⽤ 来 做
倒 影 效 果 。 就 像 下 ⾯ 这 种 ( 图 ⽚ 来 ⾃ iOS Core Animation: Advanced Techniques) 。
136
你只需要设定元素个数为 2 个,然后做⼀个 y ⽅向 -1 的缩放变换
就⾏了。
replicatorLayerX.instanceCount = nbColumn
transform = CATransform3DScale(transform, 1, -1, 0)
137
当你翻到这一页时,说明本书的内容已经全部结束了。非
常感谢你能坚持到最后。我会一直更新下去,但恕我不能保证
更新频率。如果你平时看到了那些有意思的交互设计却疑惑如
何实现的,请来我的微博 @KITTEN-YANG 找我,我们一起
琢磨琢磨。如果你发现书中有什么错误或者你有什么疑问的地
方 , 请 联 系 我 kittenyang@icloud.com 。
更多请访问 Kitten的时间胶囊
http://kittenyang.com
138
Source Exif Data:
File Type : PDF File Type Extension : pdf MIME Type : application/pdf Linearized : No Page Count : 139 PDF Version : 1.4 Title : A GUIDE TO IOS ANIMATION 2.0.pdf Producer : Mac OS X 10.11.2 Quartz PDFContext Creator : iBooks Author Create Date : 2016:02:04 20:20:11Z Modify Date : 2016:02:04 20:20:11ZEXIF Metadata provided by EXIF.tools