原始碼下載:原始碼
最近在技術群裡,有人發了一張帶有動畫效果的圖片。覺得很有意思,便動手實現了一下。在這篇文章中你將會學到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 + Raduis * cos(angle); CGFloat startPointY = kCenterY - Raduis * sin(angle); CGFloat controlPointX = self.center.x + Raduis *acos(angle); CGFloat controlPointY = kCenterY; CGFloat endPointX = self.center.x + lineWidth /2; CGFloat endPointY = kCenterY; //組合path 路徑 起點 -150° 順時針的圓 path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.center.x,kCenterY) radius:Raduis startAngle:-M_PI + angle endAngle:M_PI + angle clockwise:YES]; //起點為 180°-> (360°-30°) UIBezierPath *path1 = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.center.x,kCenterY) radius:Raduis startAngle:M_PI + angle endAngle:2 * M_PI - angle clockwise:YES]; [path appendPath:path1]; //三點曲線 UIBezierPath *path2 = [UIBezierPath bezierPath]; [path2 moveToPoint:CGPointMake(startPointX, startPointY)]; [path2 addCurveToPoint:CGPointMake(endPointX,endPointY) controlPoint1:CGPointMake(startPointX, startPointY) controlPoint2:CGPointMake(controlPointX, controlPointY)]; [path appendPath:path2]; //比原始狀態向左偏移5個畫素 UIBezierPath *path3 = [UIBezierPath bezierPath]; [path3 moveToPoint:CGPointMake(endPointX,endPointY)]; [path3 addLineToPoint:CGPointMake(self.center.x - lineWidth/2 -5,endPointY)]; [path appendPath:path3]; _changedLayer.path = path.CGPath; //平移量 CGFloat toValue = lineWidth *(1- cos(M_PI_4)) /2.0; //finished 最終狀態 CGAffineTransform transform1 = CGAffineTransformMakeRotation(0); CGAffineTransform transform2 = CGAffineTransformMakeTranslation(0, 0); CGAffineTransform transform3 = CGAffineTransformMakeRotation(0); CGAffineTransform transform = CGAffineTransformConcat(transform1, transform2); _topLineLayer.affineTransform = transform; transform = CGAffineTransformConcat(transform3, transform2); _bottomLineLayer.affineTransform = transform; //一個圓的長度比 CGFloat endPercent = 2* M_PI *Raduis / ([self calculateTotalLength] + lineWidth); //橫線佔總path的百分比 CGFloat percent = lineWidth / ([self calculateTotalLength] + lineWidth); _changedLayer.strokeStart = 1.0 -percent; CAKeyframeAnimation *startAnimation = [CAKeyframeAnimation animationWithKeyPath:@"strokeStart"]; startAnimation.values = @[@0.0,@0.3,@(1.0 -percent)]; //在π+ angle CAKeyframeAnimation *EndAnimation = [CAKeyframeAnimation animationWithKeyPath:@"strokeEnd"]; EndAnimation.values = @[@(endPercent),@(endPercent),@1.0]; CAAnimationGroup *animationGroup = [CAAnimationGroup animation]; animationGroup.animations = [NSArray arrayWithObjects:startAnimation,EndAnimation, nil]; animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; animationGroup.duration = kStep4Duration; animationGroup.delegate = self; animationGroup.removedOnCompletion = YES; [animationGroup setValue:@"animationStep4" forKey:@"animationName"]; [_changedLayer addAnimation:animationGroup forKey:nil]; //平移x CABasicAnimation *translationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"]; translationAnimation.fromValue = [NSNumber numberWithFloat:-toValue]; translationAnimation.toValue = [NSNumber numberWithFloat:0]; //角度關鍵幀 上橫線的關鍵幀 (-45°) -> (-55°)-> 10° -> 0 CAKeyframeAnimation *rotationAnimation1 = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation.z"]; rotationAnimation1.values = @[[NSNumber numberWithFloat:- M_PI_4 ], [NSNumber numberWithFloat:- Radians(10) - M_PI_4 ], [NSNumber numberWithFloat:Radians(10) ], [NSNumber numberWithFloat:0] ]; CAAnimationGroup *transformGroup1 = [CAAnimationGroup animation]; transformGroup1.animations = [NSArray arrayWithObjects:rotationAnimation1,translationAnimation, nil]; transformGroup1.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; transformGroup1.duration = kStep4Duration; transformGroup1.removedOnCompletion = YES; [_topLineLayer addAnimation:transformGroup1 forKey:nil]; //角度關鍵幀 下橫線的關鍵幀 (45°)-> (55°)- >(-10°)-> 0 CAKeyframeAnimation *rotationAnimation2 = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation.z"]; rotationAnimation2.values = @[[NSNumber numberWithFloat: M_PI_4 ], [NSNumber numberWithFloat:Radians(10) + M_PI_4 ], [NSNumber numberWithFloat:-Radians(10) ], [NSNumber numberWithFloat:0] ]; CAAnimationGroup *transformGroup2 = [CAAnimationGroup animation]; transformGroup2.animations = [NSArray arrayWithObjects:rotationAnimation2,translationAnimation, nil]; transformGroup2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; transformGroup2.duration = kStep4Duration; transformGroup2.delegate = self; transformGroup2.removedOnCompletion = YES; [_bottomLineLayer addAnimation:transformGroup2 forKey:nil]; } |
最終效果圖:
本篇文章講解結束!