源碼下載:源碼
最近在技術群里,有人發(fā)了一張帶有動畫效果的圖片肌蜻。覺得很有意思,便動手實現(xiàn)了一下必尼。在這篇文章中你將會學到Core Animation顯式動畫中的關鍵幀動畫蒋搜、組合動畫、CABasicAnimation動畫判莉。先上一張原圖的動畫效果豆挽。
點擊此查看[原圖動畫效果](https://github.com/WZF-Fei/ZFChangeAnimation/blob/master/origin animation.gif)。
本文要實現(xiàn)的效果圖如下:
把原動畫gif動畫在mac上使用圖片瀏覽模式打開券盅,我們可以看到動畫每一幀的顯示帮哈。從每一幀上的展示過程,可以把整體的動畫進行拆分成兩大部分锰镀。
第一部分(Part1)從初始狀態(tài)變成取消狀態(tài)(圖片上是由橫實線變成上線橫線交叉的圓)娘侍。
第二部分(Part2)從取消狀態(tài)變回初始狀態(tài)。
下面我們先詳細分析Part1是怎么實現(xiàn)的泳炉。根據(jù)動畫圖憾筏,把Part1再細分成三步。
Step1 : 中間橫實線的由右向左的運動效果花鹅。這其實是一個組合動畫氧腰。是先向左偏移的同時橫線變短。先看一下實現(xiàn)的動態(tài)效果。
■ 向左偏移---使用基本動畫中
animationWithKeyPath
鍵值對的方式來改變動畫的值古拴。我們這里使用position.x
,同樣可以使用transform.translation.x
來平移箩帚。
■ 改變橫線的大小---使用經(jīng)典的strokeStart
和strokeEnd
。其實上橫線長度的變化的由strokeStart
到strokeEnd
之間的值來共同來決定黄痪。改變strokeEnd
的值由1.0到0.4紧帕,不改變strokeStart
的值。橫線的長度會從右側(cè)方向由1.0倍長度減少到0.4倍長度桅打。參見示意圖的紅色區(qū)域是嗜。
-(void) animationStep1{
//最終changedLayer的狀態(tài)
_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;
//監(jiān)聽動畫
[animationGroup setValue:@"animationStep1" forKey:@"animationName"];
//動畫加入到changedLayer上
[_changedLayer addAnimation:animationGroup forKey:nil];
}
Step2 : 由左向右的動畫--向右偏移同時橫線長度變長刻帚×仕唬看一下Step2要實現(xiàn)的動畫效果。其思路和Step1是一樣的崇众。
-(void)animationStep2
{
CABasicAnimation *translationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
translationAnimation.fromValue = [NSNumber numberWithFloat:-10];
//strokeEnd:0.8 剩余的距離toValue = lineWidth * (1 - 0.8);
translationAnimation.toValue = [NSNumber numberWithFloat:0.2 * lineWidth ];
_changedLayer.strokeEnd = 0.8;
CABasicAnimation *strokeAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
strokeAnimation.fromValue = [NSNumber numberWithFloat:0.4f];
strokeAnimation.toValue = [NSNumber numberWithFloat:0.8f];
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
animationGroup.animations = [NSArray arrayWithObjects:strokeAnimation,translationAnimation, nil];
animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
animationGroup.duration = kStep2Duration;
//設置代理
animationGroup.delegate = self;
animationGroup.removedOnCompletion = YES;
[animationGroup setValue:@"animationStep2" forKey:@"animationName"];
[_changedLayer addAnimation:animationGroup forKey:nil];
}
Step3: 圓弧的動畫效果和上下兩個橫實線的動畫效果掂僵。
- 畫圓弧,首先想到是使用
UIBezierPath
顷歌。畫個示意圖來分析動畫路徑锰蓬。示意圖如下:
整個path路徑是由三部分組成,ABC曲線
眯漩、CD圓弧
芹扭、DD′圓
。
使用UIBezierPath
的方法
- (void)appendPath:(UIBezierPath *)bezierPath;
把三部分路徑關聯(lián)起來赦抖。詳細講解思路舱卡。
? ABC曲線
就是貝塞爾曲線,可以根據(jù)A队萤、B轮锥、C三點的位置使用方法
//endPoint 終點坐標 controlPoint1 起點坐標
//controlPoint2 起點和終點在曲線上的切點延伸相交的交點坐標
- (void)addCurveToPoint:(CGPoint)endPoint
controlPoint1:(CGPoint)controlPoint1
controlPoint2:(CGPoint)controlPoint2;
二次貝塞爾曲線示意圖如下:
其中control point 點是從曲線上取 start point和end point 切點相交匯的所得到的交點。如下圖:
首先C點取圓上的一點要尔,-30°舍杜。那么,
CGFloat angle = Radians(30);
C點坐標為:
//C點
CGFloat endPointX = self.center.x + Raduis * cos(angle);
CGFloat endPointY = kCenterY - Raduis * sin(angle);
A點坐標為:
//A點 取橫線最右邊的點
CGFloat startPointX = self.center.x + lineWidth/2.0 ;
CGFloat startPointY = controlPointY;
control point 為E點:
//E點 半徑*反余弦(30°)
CGFloat startPointX = self.center.x + Raduis *acos(angle);
CGFloat startPointY = controlPointY;
? CD圓弧
的路徑使用此方法確定
+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
關于弧度問題赵辕,UIBezierPath的官方文檔中的這張圖:
StartAngle 弧度即C點弧度既绩,EndAngel弧度即D點弧度。
CGFloat StartAngle = 2 * M_PI - angle;
CGFloat EndAngle = M_PI + angle;
? DD′圓
的路徑和上面2一樣的方法確定还惠。
StartAngle 弧度即D點弧度熬词,EndAngel弧度即D′點弧度。
CGFloat StartAngle = M_PI *3/2 - (M_PI_2 -angle);
CGFloat EndAngle = -M_PI_2 - (M_PI_2 -angle);
下面部分代碼是所有path路徑。
UIBezierPath *path = [UIBezierPath bezierPath];
// 畫貝塞爾曲線 圓弧
[path moveToPoint:CGPointMake(self.center.x + lineWidth/2.0 , kCenterY)];
CGFloat angle = Radians(30);
//C點
CGFloat endPointX = self.center.x + Raduis * cos(angle);
CGFloat endPointY = kCenterY - Raduis * sin(angle);
//A點
CGFloat startPointX = self.center.x + lineWidth/2.0;
CGFloat startPointY = kCenterY;
//E點 半徑*反余弦(30°)
CGFloat controlPointX = self.center.x + Raduis *acos(angle);
CGFloat controlPointY = kCenterY;
//貝塞爾曲線 ABC曲線
[path addCurveToPoint:CGPointMake(endPointX, endPointY)
controlPoint1:CGPointMake(startPointX , startPointY)
controlPoint2:CGPointMake(controlPointX , controlPointY)];
// (360°- 30°) ->(180°+30°) 逆時針的圓弧 CD圓弧
UIBezierPath *path1 = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.center.x,kCenterY)
radius:Raduis
startAngle:2 * M_PI - angle
endAngle:M_PI + angle
clockwise:NO];
[path appendPath:path1];
// (3/2π- 60°) ->(-1/2π -60°) 逆時針的圓 DD′圓
UIBezierPath *path2 = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.center.x,kCenterY)
radius:Raduis
startAngle:M_PI *3/2 - (M_PI_2 -angle)
endAngle:-M_PI_2 - (M_PI_2 -angle)
clockwise:NO];
[path appendPath:path2];
_changedLayer.path = path.CGPath;
Path路徑有了互拾,接著實現(xiàn)動畫效果歪今。
圓弧的長度逐漸變長。我們還是使用經(jīng)典的strokeStart
和strokeEnd
颜矿。但是圓弧是如何變長的呢寄猩?
(1) 初始圓弧有一段長度。
(2) 在原始長度的基礎上逐漸變長骑疆,逐漸遠離A點田篇,同時要在D點停止。
(3) 長度逐漸變長箍铭,最終要在D與D′點交匯泊柬。
我們分別解決這個三個問題。
第一個問題诈火,strokeEnd - strokeStart > 0
這樣能保證有一段圓弧兽赁。
第二個問題,逐漸變長,意味著strokeEnd
值不斷變大冷守。遠離A點意味著strokeStart
的值不斷變大刀崖。在D點停止,說明了strokeStart
有上限值拍摇。
第三個問題亮钦,意味著strokeEnd
值不斷變大,最終值為1.0充活。
這三個問題說明了一個問題蜂莉,strokeEnd
和strokeStart
是一組變化的數(shù)據(jù)。
那么core animation 中可以控制一組值的動畫是關鍵幀動畫(CAKeyframeAnimation
)混卵。
為了更準確的給出strokeEnd
和strokeStart
值巡语,我們使用長度比
來確定。
假設我們初始的長度就是曲線ABC的長度淮菠。但是貝塞爾曲線長度怎么計算男公?使用下面方法:
//求貝塞爾曲線長度
-(CGFloat) bezierCurveLengthFromStartPoint:(CGPoint)start toEndPoint:(CGPoint) end withControlPoint:(CGPoint) control
{
const int kSubdivisions = 50;
const float step = 1.0f/(float)kSubdivisions;
float totalLength = 0.0f;
CGPoint prevPoint = start;
// starting from i = 1, since for i = 0 calulated point is equal to start point
for (int i = 1; i <= kSubdivisions; i++)
{
float t = i*step;
float x = (1.0 - t)*(1.0 - t)*start.x + 2.0*(1.0 - t)*t*control.x + t*t*end.x;
float y = (1.0 - t)*(1.0 - t)*start.y + 2.0*(1.0 - t)*t*control.y + t*t*end.y;
CGPoint diff = CGPointMake(x - prevPoint.x, y - prevPoint.y);
totalLength += sqrtf(diff.x*diff.x + diff.y*diff.y); // Pythagorean
prevPoint = CGPointMake(x, y);
}
return totalLength;
}
計算貝塞爾曲線所在的比例為:
CGFloat orignPercent = [self calculateCurveLength]/[self calculateTotalLength];
初始的strokeStart = 0
、strokeEnd = orignPercent
合陵。
最終的stokeStart = 枢赔?
//結(jié)果就是貝塞爾曲線長度加上120°圓弧的長度與總長度相比得到的結(jié)果。
CGFloat endPercent =([self calculateCurveLength] + Radians(120) *Raduis ) / [self calculateTotalLength];
實現(xiàn)動畫的代碼為
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
轉(zhuǎn)動角度。
上橫線轉(zhuǎn)動的角度順序為 0 -> 10° -> (-55°) -> (-45°)
這是一組數(shù)據(jù)低剔,使用關鍵幀處理動畫速梗。
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 ]
];
下橫線轉(zhuǎn)動的角度順序為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 ]
];
你認為這么就結(jié)束了? 最終結(jié)束的動畫如下:
發(fā)現(xiàn)相交的直線沒有居中肮塞,而是靠左顯示。
向左平移姻锁,使用transform.translation.x
//平移量
CGFloat toValue = lineWidth *(1- cos(M_PI_4)) /2.0;
即旋轉(zhuǎn)角度又發(fā)生偏移量枕赵,使用組合動畫。
上橫線組合動畫
//平移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];
下橫線組合動畫
//角度關鍵幀 下橫線的關鍵幀 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到此結(jié)束位隶。最終效果圖
Part2的思路和Part1思路是一樣的拷窜。你可以參考代碼自己思考一下。核心代碼
-(void)cancelAnimation
{
//最關鍵是path路徑
UIBezierPath *path = [UIBezierPath bezierPath];
//30度,經(jīng)過反復測試涧黄,效果最好
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];
//比原始狀態(tài)向左偏移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 最終狀態(tài)
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];
}
最終效果圖:
本篇文章講解結(jié)束篮昧!