最近需要寫個進度條動畫,之前在github上看到JSDownloadView時就想也自己實現一個,於是就去網上找了一下好看的進度條動畫素材準備實現以下。
先把原圖放一下:
第一個進度條出處是這裡,第二個進度條是正好在CocoaChina的一篇文章中看到儲存下來的沒有找到原出處;
一共做了兩款進度條,但其實實現的都不是很完美。
這兩個動畫我完全用的是CoreAimation實現,由於對POP不是很熟悉,不知道如何用POP實現類似CAKeyFrameAnimation的關鍵幀動畫,所以沒有采用POP。
具體的實現思路和上面所說的JSDownloadView是類似的,簡而言之就是分解組合動畫。
1.利用Mac自帶的預覽可以檢視GIF中的一幀幀影像,可以具體觀察GIF是如何一步步變化的,方便你拆解組合動畫
2.取出幾個關鍵幀方便我們回想動畫實現
3.首先我們需要把初始狀態給畫出來,先建立兩個CAShapeLayer,此處還需要用UIBezierPath畫出layer的path,如果不是很熟悉的可以看一下這個文章,寫動畫的過程中需要繪製大量的貝塞爾曲線
我的想法是circleLayer經過動畫後由圓圈變為橫線進度條,箭頭所用的arrowLayer變為筆,這樣動畫都在這兩個layer上面進行,思路比較清晰
1 2 3 4 5 6 7 8 9 10 11 |
@property (nonatomic, strong) CAShapeLayer *circleLayer; // 圓圈 -> 進度條 @property (nonatomic, strong) CAShapeLayer *arrowLayer; // 箭頭 -> 筆 //繪製圓圈 - (UIBezierPath *)circlePath { CGFloat width = self.bounds.size.width; CGFloat height = self.bounds.size.height; CGRect rect = (CGRect){(width-height)/2,0,height,height}; return [UIBezierPath bezierPathWithOvalInRect:rect]; } |
4.初始狀態繪製好之後就需要開始動畫,由上面的分解可知:
- 圓圈->曲線->橫線;箭頭->鉛筆。這些都是形變只需設定layer.path屬性即可
- 由於圓圈變為曲線這段動畫需要畫貝塞爾曲線比較多,而且我想到的實現方法只有利用CAKeyFrameAnimation的values設定path來實現,所以沒有新增,顯得這段過渡比較突兀(若有好的想法歡迎聯絡我)
- 可以看到箭頭有一個先向上位移->然後再移動到起點的動畫;
使用CAAnimationGroup封裝動畫讓箭頭的向上跳動和變化為鉛筆的動畫同時進行
1234567891011121314151617181920212223242526// 箭頭跳動->變成鉛筆 動畫- (CAAnimationGroup *)arrowSpringAnimation{//利用基本動畫實現向上位移CABasicAnimation *arrowAnim = [CABasicAnimation animationWithKeyPath:@"position.y"];arrowAnim.toValue = @(self.arrowLayer.position.y-kArrowOffset);arrowAnim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];arrowAnim.removedOnCompletion = NO;arrowAnim.fillMode = kCAFillModeForwards;arrowAnim.delegate = self;//其實此處使用關鍵幀動畫 可以多寫幾個貝塞爾曲線加入values中使箭頭變為鉛筆的動畫更加平滑,我此處偷懶就只寫了最終狀態所以變化狀態不是很平滑CAKeyframeAnimation *pencilAnim = [CAKeyframeAnimation animationWithKeyPath:@"path"];UIBezierPath *pencilPath = [self pencilPath];pencilAnim.removedOnCompletion = NO;pencilAnim.fillMode = kCAFillModeForwards;pencilAnim.values = @[(__bridge id)pencilPath.CGPath];pencilAnim.keyTimes = @[@(1.0)];CAAnimationGroup *group = [CAAnimationGroup animation];group.animations = @[arrowAnim,pencilAnim];group.removedOnCompletion = NO;group.fillMode = kCAFillModeForwards;return group;} - 而圓圈則是變為曲線有一個波浪狀的動畫
12345678910111213141516171819202122232425262728293031323334353637383940414243444546// 進度條波動動畫//實際上就是播放values中的那幾幀畫面造成一種波動的動畫效果- (CAKeyframeAnimation *)waveAnimation{//使用關鍵幀動畫變化circleLayer的path實現波動動畫CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"path"];CGFloat offset = 15.f;//此處的方法是繪製凹凸曲線UIBezierPath *firConcavePath = [self concaveLinePathWithOffset:offset*2];UIBezierPath *secConcavePath = [self concaveLinePathWithOffset:offset];UIBezierPath *firConvexPath = [self convexLinePathWithOffset:offset];UIBezierPath *secConvexPath = [self convexLinePathWithOffset:offset*2];UIBezierPath *thrConvexPath = [self convexLinePathWithOffset:offset];UIBezierPath *horizonPath = [self horizontalLinePath];// 將圓進行凹凸動畫最終變為直線animation.values = @[(__bridge id)firConcavePath.CGPath,(__bridge id)secConcavePath.CGPath,(__bridge id)firConvexPath.CGPath,(__bridge id)secConvexPath.CGPath,(__bridge id)thrConvexPath.CGPath,(__bridge id)horizonPath.CGPath];// animation.keyTimes = @[@(0),@(0.2),@(0.4),@(0.6),@(0.8),@(1.0)];animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];animation.fillMode = kCAFillModeForwards;animation.removedOnCompletion = NO;return animation;}//此處使用貝塞爾曲線中的方法繪製波浪曲線//繪製出上下凹凸的曲線賦值給circleLayer來模擬波動效果- (UIBezierPath *)convexLinePathWithOffset:(CGFloat)offset{UIBezierPath *path = [UIBezierPath bezierPath];CGPoint startPoint = CGPointMake(0,_progressY-offset/2);CGPoint ctrlPoint = CGPointMake(CGRectGetWidth(self.frame)/2,startPoint.y-offset);CGPoint endPoint = CGPointMake(CGRectGetWidth(self.frame),startPoint.y);[path moveToPoint:startPoint];[path addQuadCurveToPoint:endPoint controlPoint:ctrlPoint];return path;}
關於此處用到的UIBezierPath還是提一下把(不熟悉的可以看一下我上面提到的那篇文章):
呼叫下面這個方法繪製二次貝塞爾曲線,看下圖就明白這個方法怎麼使用了
-(void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint;
- 此處動畫結束後應該是箭頭變為了鉛筆 並且移動到了頂端,曲線也停止波動此時需要連貫動畫,繼續讓鉛筆移動到進度條起點,並開始進度動畫,鉛筆移動到起點的動畫也就是繪一段移動曲線,只要能算出起點終點就沒什麼問題,此處不再贅述。
- 連貫動畫使用的CoreAnimation的delegate,監聽前一段動畫結束後繼續下一段動畫。其實也可以使用CoreAnimation中的beginTime屬性,只要你能將動畫開始結束時間算的很清楚使用這個方法也很不錯。若你使用POP實現動畫的話,POP有動畫完成的completionBlock則更方便
123456789- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{if ([self.arrowLayer animationForKey:kArrowSpringAnimationKey] == anim) {if (flag) {// 鉛筆彈起到進度條起點 準備progress動畫[self readyForDownload];}}} - 此時前期準備動畫已經完成,鉛筆移動到起點可以開始設定進度移動鉛筆。移動鉛筆我使用的方法是設定arrowLayer(鉛筆)的transform,但是這樣做的缺點就是你在之後算一些貝塞爾曲線的路徑時,要注意layer的transform屬性
- 改變進度條顏色我的做法是建立一個新的layer然後通過不斷設定其path來實現進度條的變化
1234567//通過不斷設定transform和path來實現進度條變化- (void)downloadingWithProgress:(CGFloat)progress{progress = MIN(MAX(progress, 0.0), 1.0);self.progressLayer.path = [self progressPathWithProgress:progress].CGPath;self.arrowLayer.transform = CATransform3DMakeTranslation(progress*self.frame.size.width, 0, 0);}
5.到此處大致的思路和主要的實現方法已經都說完了,收尾和開始的準備動畫都是一樣的型別,按照其中的動畫組合實現即可,動畫其中有很多小細節,小動畫文章裡都沒有提到
最終效果:
總結一下:
主要使用到的類:CABasicAnimation,CAKeyFrameAnimation,CAAnimationGroup,UIBezierPath
將動畫通過Group,beginTime和delegate的方式按順序組合播放即可完成一系列複雜動畫
關於animation的keypath不清楚的可以看這裡
相對來說第二個素材的動畫更容易實現而且效果更平滑,有時間再寫一下把
程式碼放在github,喜歡的朋友歡迎star
寫在最後:看了程式碼的和實際效果的朋友若有什麼改進意見 歡迎探討,有幾個效果實現起來實在差,特別第二個動畫下載速度快慢時指示器角度的變化完全沒有實現思路。