來(lái)源:Airfei
鏈接:http://www.itdecent.cn/p/1e2b8ff3519e
最近在技術(shù)群里,有人發(fā)了一張帶有動(dòng)畫(huà)效果的圖片。覺(jué)得很有意思,便動(dòng)手實(shí)現(xiàn)了一下。在這篇文章中你將會(huì)學(xué)到Core Animation顯式動(dòng)畫(huà)中的關(guān)鍵幀動(dòng)畫(huà)、組合動(dòng)畫(huà)、CABasicAnimation動(dòng)畫(huà)。先上一張?jiān)瓐D的動(dòng)畫(huà)效果。
本文要實(shí)現(xiàn)的效果圖如下:
把原動(dòng)畫(huà)gif動(dòng)畫(huà)在mac上使用圖片瀏覽模式打開(kāi),我們可以看到動(dòng)畫(huà)每一幀的顯示。從每一幀上的展示過(guò)程,可以把整體的動(dòng)畫(huà)進(jìn)行拆分成兩大部分。
第一部分(Part1)從初始狀態(tài)變成取消狀態(tài)(圖片上是由橫實(shí)線變成上線橫線交叉的圓)。
第二部分(Part2)從取消狀態(tài)變回初始狀態(tài)。
下面我們先詳細(xì)分析Part1是怎么實(shí)現(xiàn)的。根據(jù)動(dòng)畫(huà)圖,把Part1再細(xì)分成三步。
Step1 : 中間橫實(shí)線的由右向左的運(yùn)動(dòng)效果。這其實(shí)是一個(gè)組合動(dòng)畫(huà)。是先向左偏移的同時(shí)橫線變短。先看一下實(shí)現(xiàn)的動(dòng)態(tài)效果。
向左偏移—使用基本動(dòng)畫(huà)中animationWithKeyPath鍵值對(duì)的方式來(lái)改變動(dòng)畫(huà)的值。我們這里使用position.x,同樣可以使用transform.translation.x來(lái)平移。
改變橫線的大小—使用經(jīng)典的strokeStart和strokeEnd。其實(shí)上橫線長(zhǎng)度的變化的由strokeStart到strokeEnd之間的值來(lái)共同來(lái)決定。改變strokeEnd的值由1.0到0.4,不改變strokeStart的值。橫線的長(zhǎng)度會(huì)從右側(cè)方向由1.0倍長(zhǎng)度減少到0.4倍長(zhǎng)度。參見(jiàn)示意圖的紅色區(qū)域。
-(void) animationStep1{
//最終changedLayer的狀態(tài)
_changedLayer.strokeEnd = 0.4;
//基本動(dòng)畫(huà),長(zhǎng)度有1.0減少到0.4
CABasicAnimation *strokeAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
strokeAnimation.fromValue = [NSNumber numberWithFloat:1.0f];
strokeAnimation.toValue = [NSNumber numberWithFloat:0.4f];
//基本動(dòng)畫(huà),向左偏移10個(gè)像素
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"position.x"];
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0];
pathAnimation.toValue = [NSNumber numberWithFloat:-10];
//組合動(dòng)畫(huà),平移和長(zhǎng)度減少同時(shí)進(jìn)行
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
animationGroup.animations = [NSArray arrayWithObjects:strokeAnimation,pathAnimation, nil];
animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
animationGroup.duration = kStep1Duration;
//設(shè)置代理
animationGroup.delegate = self;
animationGroup.removedOnCompletion = YES;
//監(jiān)聽(tīng)動(dòng)畫(huà)
[animationGroup setValue:@"animationStep1" forKey:@"animationName"];
//動(dòng)畫(huà)加入到changedLayer上
[_changedLayer addAnimation:animationGroup forKey:nil];
}
Step2 : 由左向右的動(dòng)畫(huà)–向右偏移同時(shí)橫線長(zhǎng)度變長(zhǎng)??匆幌耂tep2要實(shí)現(xiàn)的動(dòng)畫(huà)效果。其思路和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;
//設(shè)置代理
animationGroup.delegate = self;
animationGroup.removedOnCompletion = YES;
[animationGroup setValue:@"animationStep2" forKey:@"animationName"];
[_changedLayer addAnimation:animationGroup forKey:nil];
}
Step3: 圓弧的動(dòng)畫(huà)效果和上下兩個(gè)橫實(shí)線的動(dòng)畫(huà)效果。
畫(huà)圓弧,首先想到是使用UIBezierPath。畫(huà)個(gè)示意圖來(lái)分析動(dòng)畫(huà)路徑。示意圖如下:
整個(gè)path路徑是由三部分組成,ABC曲線、CD圓弧、DD′圓。
使用UIBezierPath的方法
- (void)appendPath:(UIBezierPath *)bezierPath;
把三部分路徑關(guān)聯(lián)起來(lái)。詳細(xì)講解思路。
? ABC曲線就是貝塞爾曲線,可以根據(jù)A、B、C三點(diǎn)的位置使用方法
//endPoint 終點(diǎn)坐標(biāo) controlPoint1 起點(diǎn)坐標(biāo)
//controlPoint2 起點(diǎn)和終點(diǎn)在曲線上的切點(diǎn)延伸相交的交點(diǎn)坐標(biāo)
- (void)addCurveToPoint:(CGPoint)endPoint
? ? ? ? ?controlPoint1:(CGPoint)controlPoint1
controlPoint2:(CGPoint)controlPoint2;
二次貝塞爾曲線示意圖如下:
其中control point 點(diǎn)是從曲線上取 start point和end point 切點(diǎn)相交匯的所得到的交點(diǎn)。如下圖:
首先C點(diǎn)取圓上的一點(diǎn),-30°。那么
CGFloat angle = Radians(30);
C點(diǎn)坐標(biāo)為:
//C點(diǎn)
? ?CGFloat endPointX = self.center.x + Raduis * cos(angle);
? ?CGFloat endPointY = kCenterY - Raduis * sin(angle);
A點(diǎn)坐標(biāo)為:
//A點(diǎn) 取橫線最右邊的點(diǎn)
? ?CGFloat startPointX = self.center.x + lineWidth/2.0 ;
? ?CGFloat startPointY = controlPointY;
control point 為E點(diǎn):
//E點(diǎn) 半徑*反余弦(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;
關(guān)于弧度問(wèn)題,UIBezierPath的官方文檔中的這張圖:
StartAngle 弧度即C點(diǎn)弧度,EndAngel弧度即D點(diǎn)弧度。
CGFloat StartAngle = 2 * M_PI - angle;
CGFloat EndAngle = M_PI + angle;
? DD′圓的路徑和上面2一樣的方法確定。
StartAngle 弧度即D點(diǎn)弧度,EndAngel弧度即D′點(diǎn)弧度。
CGFloat StartAngle = M_PI *3/2 - (M_PI_2 -angle);
CGFloat EndAngle = -M_PI_2 - (M_PI_2 -angle);
下面部分代碼是所有path路徑。
UIBezierPath *path = [UIBezierPath bezierPath];
// 畫(huà)貝塞爾曲線 圓弧
[path moveToPoint:CGPointMake(self.center.x + ?lineWidth/2.0 , kCenterY)];
CGFloat angle = Radians(30);
//C點(diǎn)
CGFloat endPointX = self.center.x + Raduis * cos(angle);
CGFloat endPointY = kCenterY - Raduis * sin(angle);
//A點(diǎn)
CGFloat startPointX = self.center.x + lineWidth/2.0;
CGFloat startPointY = kCenterY;
//E點(diǎn) 半徑*反余弦(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°) 逆時(shí)針的圓弧 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°) 逆時(shí)針的圓 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路徑有了,接著實(shí)現(xiàn)動(dòng)畫(huà)效果。
圓弧的長(zhǎng)度逐漸變長(zhǎng)。我們還是使用經(jīng)典的strokeStart和strokeEnd。但是圓弧是如何變長(zhǎng)的呢?
(1) 初始圓弧有一段長(zhǎng)度。
(2) 在原始長(zhǎng)度的基礎(chǔ)上逐漸變長(zhǎng),逐漸遠(yuǎn)離A點(diǎn),同時(shí)要在D點(diǎn)停止。
(3) 長(zhǎng)度逐漸變長(zhǎng),最終要在D與D′點(diǎn)交匯。
我們分別解決這個(gè)三個(gè)問(wèn)題。
第一個(gè)問(wèn)題,strokeEnd - strokeStart > 0這樣能保證有一段圓弧。
第二個(gè)問(wèn)題,逐漸變長(zhǎng),意味著strokeEnd值不斷變大。遠(yuǎn)離A點(diǎn)意味著strokeStart的值不斷變大。在D點(diǎn)停止,說(shuō)明了strokeStart有上限值。
第三個(gè)問(wèn)題,意味著strokeEnd值不斷變大,最終值為1.0。
這三個(gè)問(wèn)題說(shuō)明了一個(gè)問(wèn)題,strokeEnd和strokeStart是一組變化的數(shù)據(jù)。
那么core animation 中可以控制一組值的動(dòng)畫(huà)是關(guān)鍵幀動(dòng)畫(huà)(CAKeyframeAnimation)。
為了更準(zhǔn)確的給出strokeEnd和strokeStart值,我們使用長(zhǎng)度比來(lái)確定。
假設(shè)我們初始的長(zhǎng)度就是曲線ABC的長(zhǎng)度。但是貝塞爾曲線長(zhǎng)度怎么計(jì)算?使用下面方法:
//求貝塞爾曲線長(zhǎng)度
-(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;
}
計(jì)算貝塞爾曲線所在的比例為:
CGFloat orignPercent = [self calculateCurveLength]/[self calculateTotalLength];
初始的strokeStart = 0、strokeEnd = orignPercent。
最終的stokeStart = ?
//結(jié)果就是貝塞爾曲線長(zhǎng)度加上120°圓弧的長(zhǎng)度與總長(zhǎng)度相比得到的結(jié)果。
CGFloat endPercent =([self calculateCurveLength] + Radians(120) *Raduis ) / [self calculateTotalLength];
實(shí)現(xiàn)動(dòng)畫(huà)的代碼為
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];
效果圖為:
閱讀 18148 投訴
寫(xiě)留言
?