一款Loading動(dòng)畫(huà)的實(shí)現(xiàn)思路(一)

?前幾天茅姜,朋友推薦了一款Loading動(dòng)畫(huà)闪朱,感覺(jué)挺有意思,動(dòng)畫(huà)是這樣的

正好這段時(shí)間在學(xué)習(xí)動(dòng)畫(huà),就試著實(shí)現(xiàn)了一版奋姿,
為了降低難度锄开,我對(duì)動(dòng)畫(huà)做了一些簡(jiǎn)化,做完后是這樣的

考慮到拋磚引玉是最好的學(xué)習(xí)方式之一胀蛮,我就分幾篇把自己的實(shí)現(xiàn)思路寫(xiě)出來(lái)院刁,請(qǐng)大家把更好的想法砸過(guò)來(lái)吧!

這個(gè)動(dòng)畫(huà)乍一看很復(fù)雜粪狼,但我們相信一點(diǎn):
一個(gè)復(fù)雜任務(wù)可以拆分成一組簡(jiǎn)單任務(wù)退腥。
因此,我把這段復(fù)雜動(dòng)畫(huà)按時(shí)間拆分成了幾個(gè)階段再榄,又把每個(gè)階段拆分成了幾個(gè)并行的簡(jiǎn)單動(dòng)畫(huà)狡刘。

怎么拆分呢,如果我們有動(dòng)畫(huà)的gif困鸥,我們可以用系統(tǒng)自帶的Preview看一下嗅蔬,像這樣


在gif中一幀一幀的看一下,心里大約就有拆分的思路了疾就。

每個(gè)人拆分的可能都不一樣澜术,答案本來(lái)就不只一種,每個(gè)階段我會(huì)寫(xiě)一篇文字?猬腰,這一篇我們一起看看第一階段鸟废。

?第一階段是這樣的,為了方便大家觀看姑荷,我放慢了動(dòng)畫(huà)速度


?看上去盒延,它就是一段起點(diǎn)和終點(diǎn)不停變化的弧,于是我決定用重繪弧的方式實(shí)現(xiàn)鼠冕。

關(guān)于繪制添寺,我決定使用UIBezierPath,初次實(shí)現(xiàn)懈费,我總是選擇自己熟悉的方式计露。

要畫(huà)弧,我們用到UIBezierPath的這個(gè)方法
+ (instancetype)bezierPathWithArcCenter:(CGPoint)center
radius:(CGFloat)radius
startAngle:(CGFloat)startAngle
endAngle:(CGFloat)endAngle
clockwise:(BOOL)clockwise

為了后文的敘述方便憎乙,我要祭出UIBezierPath的官方文檔中的這張圖了薄坏,大家在后文看我手繪的丑圖時(shí),可能需要參考此圖

開(kāi)始了寨闹,
假設(shè)弧的起點(diǎn)為O(origin),終點(diǎn)為D(dest)君账,動(dòng)畫(huà)中弧是逆時(shí)針轉(zhuǎn)動(dòng)的繁堡,那我們畫(huà)弧時(shí)也采用逆時(shí)針,也就是說(shuō),我們的弧是從O點(diǎn)逆時(shí)針畫(huà)到D點(diǎn)椭蹄。
先看下這張圖闻牡,動(dòng)畫(huà)開(kāi)始和結(jié)束時(shí)的弧的樣子(注意:結(jié)束時(shí)的弧其實(shí)是個(gè)圓,為了方便說(shuō)明绳矩,我故意留了個(gè)缺口)


再回頭觀察動(dòng)畫(huà)可知罩润,結(jié)束時(shí),O點(diǎn)和D點(diǎn)在0(或2π)處重合翼馆,
因此割以,結(jié)束時(shí)可以認(rèn)為弧是從2π逆時(shí)針畫(huà)到0(雖然0和2π在一個(gè)點(diǎn),但從0到0应媚、從0到2π严沥、從2π到0畫(huà)弧是不一樣的,推薦大家大家動(dòng)手畫(huà)一下)

結(jié)束時(shí)O中姜、D我們確定了消玄,那么開(kāi)始的時(shí)候呢,我們要看下動(dòng)畫(huà)中O丢胚、D的運(yùn)行軌跡了翩瓜。
觀察動(dòng)畫(huà),我們可以得出O携龟、D的運(yùn)行軌跡是這樣的

可以看出兔跌,O點(diǎn)逆時(shí)針(逆時(shí)針可以認(rèn)為角度在減小,可以再參考上文中UIBezierPath的官方文檔中的那張圖)繞了3/4圈到2π骨宠,D點(diǎn)逆時(shí)針繞了1.5圈到0浮定;
因此我們可以得出O、D的角度變化层亿,是這樣的

即O點(diǎn)從7/2的π減小到2π桦卒,D點(diǎn)從3π減小到0。

現(xiàn)在我們知道了O匿又、D的起點(diǎn)方灾,可以將前面圖上的文字補(bǔ)全了


由此我們可以得出,O碌更、D在動(dòng)畫(huà)階段中的角度(圖中的progress取值范圍0~1)

只要我們的progress從0逐漸變到1裕偿,我們O、D就逐漸從起點(diǎn)運(yùn)動(dòng)到終點(diǎn)了痛单,每次變化的時(shí)候繪制從O到D逆時(shí)針的弧嘿棘,我們動(dòng)畫(huà)就實(shí)現(xiàn)了。

到了這一步旭绒,我們的動(dòng)畫(huà)思路已經(jīng)有了鸟妙,重要節(jié)點(diǎn)的值也知道了焦人,剩下的就是寫(xiě)代碼了。

寫(xiě)代碼

我們的思路可以認(rèn)為是重父,屬性變化觸發(fā)重繪花椭,
自定義CALayer的子類(lèi),重寫(xiě)它的這兩個(gè)方法可以實(shí)現(xiàn)這個(gè)思路
+ (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

重寫(xiě)CALayer的這個(gè)方法
(下面代碼中的@dynamic progress;我們?cè)诒疚淖詈蠼忉?

@implementation ArcToCircleLayer
@dynamic progress;
+ (BOOL)needsDisplayForKey:(NSString *)key {
    if ([key isEqualToString:@"progress"]) {
        return YES;
    }
    return [super needsDisplayForKey:key];
}

這樣當(dāng)progress的值改變的時(shí)候房午,CALayer會(huì)標(biāo)記自己為需要重繪矿辽,
如果我們重寫(xiě)了drawInContext:方法,
系統(tǒng)就會(huì)在適當(dāng)?shù)臅r(shí)候調(diào)用drawInContext:重繪Layer郭厌;

注意到我們上文引用的文檔中有這句

Animations changing the value of the attribute also trigger redisplay.

因此我們可以使用CA動(dòng)畫(huà)來(lái)修改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];

這樣當(dāng)CA動(dòng)畫(huà)執(zhí)行時(shí),progress值會(huì)不斷變化沪曙,從而觸發(fā)drawInContext:重繪奕污,實(shí)現(xiàn)動(dòng)畫(huà)。
(第一句代碼是為了讓動(dòng)畫(huà)結(jié)束時(shí)液走,停留在?動(dòng)畫(huà)結(jié)束時(shí)的狀態(tài)碳默。
簡(jiǎn)單的說(shuō),動(dòng)畫(huà)執(zhí)行時(shí)改變的是presentation Layer的值缘眶,model Layer的值?不會(huì)變化嘱根,
動(dòng)畫(huà)結(jié)束后會(huì)顯示model Layer的值,因?yàn)閙odel Layer的值沒(méi)有變化巷懈,看上去就是直接跳回了動(dòng)畫(huà)開(kāi)始時(shí)的值该抒,上面第一句代碼的作用就是將model Layer的值修改為動(dòng)畫(huà)結(jié)束時(shí)的值。
這部分內(nèi)容可以參考Core Animation Programming Guided的這一節(jié)

看到CABasicAnimation顶燕,大家可能覺(jué)得有很多屬性可以設(shè)置凑保,比如,將代碼修改為這樣

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];

如你所料涌攻,動(dòng)畫(huà)變成了這樣


我們發(fā)現(xiàn)了欧引,這種方式可以充分利用CA動(dòng)畫(huà)系統(tǒng)。

動(dòng)畫(huà)流程?已經(jīng)明確了恳谎,接下來(lái)只要重寫(xiě)drawInContext: 的繪制代碼就可以了芝此。
前文已經(jīng)得出了繪制思路和各節(jié)點(diǎn)的值,直接上代碼因痛,為表達(dá)清晰婚苹,我聲明了多個(gè)局部變量,大家可以和這張圖對(duì)照一下

- (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上這個(gè)項(xiàng)目的OneLoadingAnimationStep1目錄。

上文中的@dynamic progress;的解釋

由于我對(duì)property的了解還不深谭企,對(duì)此的解釋之后會(huì)補(bǔ)上廓译,
目前可參考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.
 *

完整代碼

請(qǐng)參考GitHub上這個(gè)項(xiàng)目的OneLoadingAnimationStep1目錄结胀。

本系列的?傳送門(mén)

鳴謝及推薦

相關(guān)鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市责循,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌攀操,老刑警劉巖院仿,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異速和,居然都是意外死亡歹垫,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)颠放,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)排惨,“玉大人,你說(shuō)我怎么就攤上這事碰凶∧喊牛” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵欲低,是天一觀的道長(zhǎng)峻呕。 經(jīng)常有香客問(wèn)我也搓,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任桶癣,我火速辦了婚禮,結(jié)果婚禮上还栓,老公的妹妹穿的比我還像新娘锦庸。我一直安慰自己,他們只是感情好闰非,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布膘格。 她就那樣靜靜地躺著,像睡著了一般河胎。 火紅的嫁衣襯著肌膚如雪闯袒。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天游岳,我揣著相機(jī)與錄音政敢,去河邊找鬼。 笑死胚迫,一個(gè)胖子當(dāng)著我的面吹牛喷户,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播访锻,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼褪尝,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼闹获!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起河哑,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤避诽,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后璃谨,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體沙庐,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年佳吞,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了拱雏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡底扳,死狀恐怖铸抑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情衷模,我是刑警寧澤鹊汛,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站算芯,受9級(jí)特大地震影響柒昏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜熙揍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一职祷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧届囚,春花似錦有梆、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至蛔添,卻和暖如春痰催,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背迎瞧。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工夸溶, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人凶硅。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓缝裁,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親足绅。 傳聞我的和親對(duì)象是個(gè)殘疾皇子捷绑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 前幾天韩脑,朋友推薦了一款Loading動(dòng)畫(huà),感覺(jué)挺有意思粹污,動(dòng)畫(huà)是這樣的 正好這段時(shí)間在學(xué)習(xí)動(dòng)畫(huà)段多,就試著實(shí)現(xiàn)了一版,為...
    王小賓閱讀 158評(píng)論 0 0
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,162評(píng)論 25 707
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)壮吩、插件衩匣、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,105評(píng)論 4 62
  • Duplicated Code(重復(fù)代碼) 常見(jiàn)問(wèn)題 同一個(gè)類(lèi)的兩個(gè)函數(shù)含有相同的表達(dá)式 兩個(gè)互為兄弟的子類(lèi)內(nèi)含相...
    chiguozi閱讀 932評(píng)論 3 5
  • 不要去追一匹馬,用追馬的時(shí)間種草粥航,待到春暖花開(kāi)時(shí),就會(huì)有一批駿馬任你挑選生百;不要去刻意巴結(jié)一個(gè)人递雀,用暫時(shí)沒(méi)有朋友的時(shí)...
    繁花落盡只為平淡如水閱讀 169評(píng)論 0 0