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