作者:雪夜吐息
前幾天,朋友推薦了一款Loading動畫,感覺挺有意思,動畫是這樣的
正好這段時間在學習動畫,就試著實現了一版,
為了降低難度,我對動畫做了一些簡化,做完後是這樣的
考慮到拋磚引玉是最好的學習方式之一,我就分幾篇把自己的實現思路寫出來,請大家把更好的想法砸過來吧!
這個動畫乍一看很復雜,但我們相信一點:
一個復雜任務可以拆分成一組簡單任務。
因此,我把這段復雜動畫按時間拆分成了幾個階段,又把每個階段拆分成了幾個並行的簡單動畫。
怎麼拆分呢,如果我們有動畫的gif,我們可以用系統自帶的Preview看一下,像這樣
在gif中一幀一幀的看一下,心裡大約就有拆分的思路了。
每個人拆分的可能都不一樣,答案本來就不只一種,每個階段我會寫一篇文字,這一篇我們一起看看第一階段。
第一階段是這樣的,為了方便大家觀看,我放慢了動畫速度
看上去,它就是一段起點和終點不停變化的弧,於是我決定用重繪弧的方式實現。
關於繪制,我決定使用UIBezierPath,初次實現,我總是選擇自己熟悉的方式。
要畫弧,我們用到UIBezierPath的這個方法
+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise
為了後文的敘述方便,我要祭出UIBezierPath的官方文檔中的這張圖了,大家在後文看我手繪的丑圖時,可能需要參考此圖
開始了,
假設弧的起點為O(origin),終點為D(dest),動畫中弧是逆時針轉動的,那我們畫弧時也采用逆時針,也就是說,我們的弧是從O點逆時針畫到D點。
先看下這張圖,動畫開始和結束時的弧的樣子(注意:結束時的弧其實是個圓,為了方便說明,我故意留了個缺口)
再回頭觀察動畫可知,結束時,O點和D點在0(或2π)處重合,
因此,結束時可以認為弧是從2π逆時針畫到0(雖然0和2π在一個點,但從0到0、從0到2π、從2π到0畫弧是不一樣的,推薦大家大家動手畫一下)
結束時O、D我們確定了,那麼開始的時候呢,我們要看下動畫中O、D的運行軌跡了。
觀察動畫,我們可以得出O、D的運行軌跡是這樣的
可以看出,O點逆時針(逆時針可以認為角度在減小,可以再參考上文中UIBezierPath的官方文檔中的那張圖)繞了3/4圈到2π,D點逆時針繞了1.5圈到0;
因此我們可以得出O、D的角度變化,是這樣的
即O點從7/2的π減小到2π,D點從3π減小到0。
現在我們知道了O、D的起點,可以將前面圖上的文字補全了
由此我們可以得出,O、D在動畫階段中的角度(圖中的progress取值范圍0~1)
只要我們的progress從0逐漸變到1,我們O、D就逐漸從起點運動到終點了,每次變化的時候繪制從O到D逆時針的弧,我們動畫就實現了。
到了這一步,我們的動畫思路已經有了,重要節點的值也知道了,剩下的就是寫代碼了。
寫代碼
我們的思路可以認為是,屬性變化觸發重繪,
自定義CALayer的子類,重寫它的這兩個方法可以實現這個思路
+ (BOOL)needsDisplayForKey:(NSString *)key; - (void)drawInContext:(CGContextRef)ctx;
可以參考needsDisplayForKey:的官方文檔的這段文字
Discussion
Subclasses can override this method and return YES
if the layer should be redisplayed when the value of the specified attribute changes. Animations changing the value of the attribute also trigger redisplay.
我們定義progress屬性
@interface ArcToCircleLayer : CALayer @property (nonatomic) CGFloat progress; @end
重寫CALayer的這個方法
(下面代碼中的@dynamic progress;我們在本文最後解釋)
@implementation ArcToCircleLayer @dynamic progress; + (BOOL)needsDisplayForKey:(NSString *)key { if ([key isEqualToString:@"progress"]) { return YES; } return [super needsDisplayForKey:key]; }
這樣當progress的值改變的時候,CALayer會標記自己為需要重繪,
如果我們重寫了drawInContext:方法,
系統就會在適當的時候調用drawInContext:重繪Layer;
注意到我們上文引用的文檔中有這句
Animations changing the value of the attribute also trigger redisplay.
因此我們可以使用CA動畫來修改progress的值,就像下面這樣
self.arcToCircleLayer.progress = 1; // end status CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"progress"]; animation.duration = 5; animation.fromValue = @0.0; animation.toValue = @1.0; [self.arcToCircleLayer addAnimation:animation forKey:nil];
這樣當CA動畫執行時,progress值會不斷變化,從而觸發drawInContext:重繪,實現動畫。
(第一句代碼是為了讓動畫結束時,停留在動畫結束時的狀態。
簡單的說,動畫執行時改變的是presentation Layer的值,model Layer的值不會變化,
動畫結束後會顯示model Layer的值,因為model Layer的值沒有變化,看上去就是直接跳回了動畫開始時的值,上面第一句代碼的作用就是將model Layer的值修改為動畫結束時的值。
這部分內容可以參考Core Animation Programming Guided的這一節)
看到CABasicAnimation,大家可能覺得有很多屬性可以設置,比如,將代碼修改為這樣
self.arcToCircleLayer.progress = 0; // end status CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"progress"]; animation.duration = 5; animation.fromValue = @0.0; animation.toValue = @1.0; animation.autoreverses = YES; [self.arcToCircleLayer addAnimation:animation forKey:nil];
如你所料,動畫變成了這樣
我們發現了,這種方式可以充分利用CA動畫系統。
動畫流程已經明確了,接下來只要重寫drawInContext: 的繪制代碼就可以了。
前文已經得出了繪制思路和各節點的值,直接上代碼,為表達清晰,我聲明了多個局部變量,大家可以和這張圖對照一下
- (void)drawInContext:(CGContextRef)ctx { UIBezierPath *path = [UIBezierPath bezierPath]; CGFloat radius = MIN(CGRectGetWidth(self.bounds), CGRectGetHeight(self.bounds)) / 2 - kLineWidth / 2; CGPoint center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); // O CGFloat originStart = M_PI * 7 / 2; CGFloat originEnd = M_PI * 2; CGFloat currentOrigin = originStart - (originStart - originEnd) * self.progress; // D CGFloat destStart = M_PI * 3; CGFloat destEnd = 0; CGFloat currentDest = destStart - (destStart - destEnd) * self.progress; [path addArcWithCenter:center radius:radius startAngle: currentOrigin endAngle:currentDest clockwise:NO]; CGContextAddPath(ctx, path.CGPath); CGContextSetLineWidth(ctx, kLineWidth); CGContextSetStrokeColorWithColor(ctx, [UIColor blueColor].CGColor); CGContextStrokePath(ctx); }
至此,第一階段代碼的主要部分就完成了,
第一階段的完整代碼大家可以參考GitHub上這個項目的OneLoadingAnimationStep1目錄。
上文中的@dynamic progress;的解釋
由於我對property的了解還不深,對此的解釋之後會補上,
目前可參考CALayer.h的這段注釋
/** Property methods. **/ /* CALayer implements the standard NSKeyValueCoding protocol for all * Objective C properties defined by the class and its subclasses. It * dynamically implements missing accessor methods for properties * declared by subclasses. *
完整代碼
請參考GitHub上這個項目的OneLoadingAnimationStep1目錄。
鳴謝及推薦
Kitten's 時間膠囊
葉孤城_
相關鏈接
Core Animation Programming Guide
CALayer Class Reference
UIBezierPath Class Reference