用代碼來實現你喜歡的動畫效果 — 蟲兒跑

作者建議讀者在PC上閱讀,字數太多手機閱讀起來不是很好。

文檔更新說明

  • 2016-03-25 完成全文
  • 2016-03-24 完成4.3
  • 2016-03-22 完成4.1,4.2
  • 2016-03-21 初稿

前言

接觸iOS應用開發不久,對于很多我暫時不清楚怎么實現的功能效果都挺感興趣的.iOS開發中,動畫特效是必不可少的一部分,最近學習了一下動畫,順便花了點時間實現了一個蟲子嚅動的動畫效果并寫成一個指示器(HUD).本文將講述怎么學習動畫設計,最終的效果就是要做到看上一個喜歡的動畫,然后能用代碼在iOS中實現他,最后能夠在實際項目使用.

看上的動畫

這是一篇和動畫有關的文章,那我就在文章開頭放出這次想要實現的動畫吧,大家也好思考一下具體怎么去實現.下面是我在豆瓣網上看到的一張動畫圖,覺得挺有意思的,于是我打算用程序實現它.我給它取了一個名字,蟲兒跑,一只勤奮的蟲子.


a-diligent-worm.gif

思考

了解iOS中實現動畫的方法

iOS中的動畫主要有View級別,Layer級別.View級別動畫可以利用UIView的類中animation開頭的類方法實現,使用簡單方便,但是無法操作更多細節,它其實就是Core Animation的封裝.Layer級別的動畫相對于View級別來說要底層一些,主要是使用Core Animation來實現.相關CA類如下圖:

  
Core-Animation-Classes.png

CAAnimation:核心動畫的基礎類,不能直接使用,負責動畫運行時間、速度的控制,本身實現了CAMediaTiming協議。
CAPropertyAnimation:屬性動畫的基類(通過屬性進行動畫設置,注意是可動畫屬性),不能直接使用。
CAAnimationGroup:動畫組,動畫組是一種組合模式設計,可以通過動畫組來進行所有動畫行為的統一控制,組中所有動畫效果可以并發執行。
CATransition:轉場動畫,主要通過濾鏡進行動畫效果設置。
CABasicAnimation:基礎動畫,通過屬性修改進行動畫參數控制,只有初始狀態和結束狀態。
CAKeyframeAnimation:關鍵幀動畫,同樣是通過屬性進行動畫參數控制,但是同基礎動畫不同的是它可以有多個狀態控制。

根據實際場景,本文主要是在CALayer的子類CAShapeLayer(看名字很容易看出這是專門用作圖形處理的子類)中利用CABasicAnimation, CAAnimationGroup來實現動畫效果.更多iOS動畫知識,可以參考文后的閱讀推薦
  CABasicAnimation類有幾個屬性要特別留意,分別是fromValue和toValue,這兩個基本就是CABasicAnimation的核心了,其余大部分是繼承子父類的.這里要重點指出的是創建CABasicAnimation對象時候要指出keyPath,用來指明動畫是平移縮放旋轉還是繪制等.其中keyPath可以設置為strokeStart, strokeEnd,而CAShapeLayer也有兩個屬性分別是strokeStart, strokeEnd.網上很多文章介紹這些屬性的作用了.我這里就不重復描述了,感覺這種東西光自己描述真的是很難說清楚,我發現有一個特別便捷直觀的方法,那就是你在代碼中自己測試觀察.所以我特別寫了兩段測試的代碼.大家也可以在我的GitHub上找到可以直接運行的代碼.

CAShapeLayer *firstWormShapeLayer = [[CAShapeLayer alloc] init];
    firstWormShapeLayer.path = [self testPath];
    firstWormShapeLayer.lineWidth = 2;
    firstWormShapeLayer.lineCap = kCALineCapRound;
    firstWormShapeLayer.strokeColor = [UIColor redColor].CGColor;
    firstWormShapeLayer.fillColor = [UIColor clearColor].CGColor;
    firstWormShapeLayer.actions = [[NSDictionary alloc] initWithObjectsAndKeys:[NSNull null],@"strokeStart",[NSNull null],@"strokeEnd", nil];

//self 是一個UIView對象
//@property (nonatomic,strong) CAShapeLayer *firstWormShapeLayer;
[self.layer addSublayer:firstWormShapeLayer];
self.firstWormShapeLayer = firstWormShapeLayer;
[self testAnimation];
 
 #pragma mark - 測試用
 // 生成一條用于制作動畫的路徑
-(CGPathRef)testPath{
    //畫一個半圓
    UIBezierPath* wormPath = UIBezierPath.bezierPath;
    [wormPath moveToPoint: CGPointMake(5,20)];
    [wormPath addLineToPoint:CGPointMake(35, 20)];
    CGPathRef path = wormPath.CGPath;
    return path;
}

// 開始動畫效果
-(void)testAnimation{
    CABasicAnimation *strokeStartAm = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    //時間函數,用于描述動畫的速度和時間的關系,可以先忽略
    strokeStartAm.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    strokeStartAm.toValue = [NSNumber numberWithFloat:0.2];
    strokeStartAm.fromValue = [NSNumber numberWithFloat:1];
    //一次動畫的執行時間
    strokeStartAm.duration = 2;
    //動畫重復次數
    strokeStartAm.repeatCount = 100;
    
    CABasicAnimation *strokeEndAm = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    strokeEndAm.toValue = [NSNumber numberWithFloat:1];
    strokeEndAm.fromValue = [NSNumber numberWithFloat:0];
    strokeEndAm.duration = 2;
    strokeEndAm.repeatCount = 100;
    
    [self.firstWormShapeLayer addAnimation:strokeEndAm forKey:nil];
}

哪些工具可以利用?

要實現一個動畫,最重要的就是確定動畫的速度與時間的關系,確定動畫的基本元素.比如動畫的基本形狀,字形,路徑等.這里我找到了幾種相當不錯的工具.

  1. CAMediaTimingFunction playground
    可視化觀察timingFunction的曲線
  2. Sketch
    這是一款Mac上的矢量圖繪制工具,可以到處SVG格式的文件
  3. PaintCode
    用于將SVG文件轉換成OC的路徑代碼(提醒一下,PaintCode這個軟件制作出來的路徑繪制順序可能不是你想要的)
      本文將要實現的動畫效果看上去像是一個半圓,所以應該是用不上路徑代碼生成工具.

iOS動畫的幾個重要概念

  1. 關鍵幀:一個動畫可能是單一的變化,等比例放大縮小,也可能是由好幾個變化組成,比如先放大,再縮小,再平移.在iOS動畫設計中,系統提供一些設置關鍵幀方法,你只要定義好關鍵幀和關鍵幀幀之間的時間分配,系統就會根據關鍵幀的區別自動補全過度幀,達到一個動畫流暢的效果.
  2. 層:4.1小節中提到,在CALayer上才能實現更多動畫的細節.View的層概念類似PhotoShop中的圖層,一個圖像的最終效果可以通過多個層重疊實現的,同樣的一個View在屏幕上顯示的效果除了View上面的內容之外,還有一個很大的因素就是這個View的層里內容.
  3. 時間函數:動畫總會在設定的時間長度內執行完,在這段時間內,動畫的變化有多大,變化的速度有多快,這主要取決于時間函數.如果時間函數是線性函數,那表示動畫的變化速度在這一段時間內是均勻變化的.如果是曲線的,那又是另一種效果,下面給出系統提供的5種時間函數的函數圖像,方便大家理解.


    kCAMediaTimingFunctionLinear.png

    線型函數

kCAMediaTimingFunctionEaseIn.png

緩入

kCAMediaTimingFunctionEaseOut.png

緩出

kCAMediaTimingFunctionEaseInEaseOut.png

慢入慢出

kCAMediaTimingFunctionDefault.png

系統默認的時間函數

系統還提供了一個設置時間函數的方法[CAMediaTimingFunction functionWithControlPoints::::],具體用法參考CAMediaTimingFunction playground

  1. fillMode:填充模式是CAMediaTiming協議里的屬性,所有的Core Animation動畫類都有,它非常重要又不好直接通過文檔理解,所以有必要解釋一下.
    kCAFillModeRemoved:默認是這個屬性,動畫完成后就從layer中移除了.你看到的效果就是動畫結束后,你在層中可以看到內容變成了你繪制的完整圖形,這經常發生在你有多個動畫對象要同時執行動畫,但是這些對象的時間長度不同,造成一些對象執行完了就顯示完整圖形了.
    kCAFillModeForwards:當動畫結束后,保留動畫結束時的模樣.
    kCAFillModeBackwards:動畫在開始前就顯示出動畫要開始時的模樣.比如你動畫延遲2秒執行,那么動畫在沒開始執行這段時間的模樣就是動畫開始執行時的樣子.
    kCAFillModeBoth:同時具備kCAFillModeForwards和kCAFillModeBackwards的效果
  2. 動畫組: iOS動畫中,可以使用CAAnimationGroup來實現動畫組功能.動畫組對象可以加入多個動畫對象,這些動畫對象將會在動畫組開始執行的時候同時執行動畫.CAAnimationGroup也有自己的長度,重復次數等屬性,這些和組中的每一個動畫屬性都不沖突.這個是很重要的,舉個例子.動畫對象A長度1s,重復2次;動畫對象B長度1.5s,重復2次;動畫對象C時間長度2s,重復次數3次;動畫組G,長度3秒,重復無數次.ABC加入G中,效果就是G在執行一次動畫時,A可以重復執行2次,然后空閑1s.B重復2次,沒有空閑.C執行完第一次,在重復第二次時執行了1秒就沒法繼續執行了,因為3秒時間一個周期到了,G又重復了整個這個動畫效果.
      iOS動畫還有很多概念和屬性,由于本文要實現的動畫沒有涉及到,所以就不講述了.具體的大家可以在推薦閱讀中繼續學習,然后嘗試實現更多效果

拆分動畫,簡化思路

不同的拆分思路,寫出來的代碼也不同,如果一開始拆分得不好,有可能代碼要多寫很多,而且還不一定能實現.觀察一下上面提供的GIF動畫,整個動畫是由一個半圓弧組成,一共是三種顏色,紅黃綠色.這里有幾種思路.
  一種是整個動畫由一個layer組成,layer上畫一個半圓弧,把圓弧分層三段,分別畫上不同的顏色.要注意的是動畫除了圓弧的伸張縮小外還有一個平移運動.這種思路看似簡單,但是有個地方不好實現,就是半圓弧的拉伸和縮小,顏色長度比例要保持一樣.半圓弧可以從0畫到完整的半圓弧,再從左端向右端消失,這個容易實現拉伸縮小效果,但是顏色跟著拉伸縮小就不好做了.
  另一種思路是拆分動畫,用多個layer組成,也是本文所使用的方法.通過拆分動畫,可以簡化思路和算法.觀察GIF,可以把動畫看成三個layer,每一個layer各自畫著自己的半圓弧,紅色保留完整弧長,微調黃色和綠色的可見弧長,其中綠色最短.注意一下,一開我也是把基礎圖形設定為半圓弧,但是做著做著發現沒法實現先頭紅尾綠,接著先綠尾紅這樣的效果,所以基礎圖形的設定是非常重要的,一定要考慮清楚,否則事倍功半.這里我們需要把基礎圖形設定成有兩個半圓弧連著的.如圖:


full-worm-red.png

左半圓弧可以實現頭紅尾綠,右半圓弧可以實現先綠尾紅的效果,動畫過程中始終只顯示最多一半的路徑(半圓弧).

開始動手

實現動畫的核心部分

根據動畫的拆分思路,先實現紅色部分的效果,再疊加黃色和綠色的圖層.下面是核心代碼和注釋.
CCWormView.h

//創建UIView的子類CCWormView
@interface CCWormView : UIView
//省略其他代碼
@end

CCWormView的實現

#define WORM_ANIMATION_KEY_FIRST @"WORM_ANIMATION_KEY_FIRST"
#define WORM_ANIMATION_KEY_SECOND @"WORM_ANIMATION_KEY_SECOND"
#define WORM_ANIMATION_KEY_THIRD @"WORM_ANIMATION_KEY_THIRD"
@interface CCWormView ()
//用戶實現紅色蟲動畫的層
@property (nonatomic,strong) CAShapeLayer *firstWormShapeLayer;
@end
@implementation CCWormView
//省略其他代碼
//...
//初始化層
-(instancetype)initWithFrame:(CGRect)frame HUDStyle:(CCWormHUDStyle)style{
    CAShapeLayer *firstWormShapeLayer = [[CAShapeLayer alloc] init];
    //設置動畫的路徑
    firstWormShapeLayer.path = [self wormRunLongPath];
    //畫筆寬度
    firstWormShapeLayer.lineWidth = CCWormHUDLineWidth;
    //設置線段頭尾為圓形
    firstWormShapeLayer.lineCap = kCALineCapRound;
    //設置線段拐角點為圓形
    firstWormShapeLayer.lineJoin = kCALineCapRound;
    //設置路徑繪制顏色為紅色
    firstWormShapeLayer.strokeColor = [UIColor redColor].CGColor;
    //設置填充顏色為透明
    firstWormShapeLayer.fillColor = [UIColor clearColor].CGColor;
    //
    firstWormShapeLayer.actions = [[NSDictionary alloc] initWithObjectsAndKeys:[NSNull null],@"strokeStart",[NSNull null],@"strokeEnd", nil];
    //將其加入到view的layer中.
    [self.layer addSublayer:firstWormShapeLayer];    
    self.firstWormShapeLayer = firstWormShapeLayer;
    //動畫部分,為CAShapeLayer添加動畫效果
    [self firstWormAnimation];
}
-(CGPathRef)wormRunLongPath{
    //確定路徑起點位置
    CGPoint center;
    center = CGPointMake(self.frame.size.width * 9 / 10, self.frame.size.height / 2);
    //兩個連著的半圓
    CGFloat radius = (CCWormHUDViewWith / 2.0) / 2.0;
    UIBezierPath *wormPath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:M_PI endAngle:2 * M_PI clockwise:YES];
    [wormPath addArcWithCenter:CGPointMake(center.x  + radius * 2, center.y) radius:radius startAngle:M_PI endAngle:2 * M_PI clockwise:YES];
    //返回CGPathRef,因為CAShapeLayer的path屬性類型為CGPathRef
    CGPathRef path = wormPath.CGPath;
    return path;
}

下面是firstWormAnimation部分
第一步 實現紅色蟲運動的前半部分

    ///蟲子拉伸
    CABasicAnimation *strokeEndAm = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    strokeEndAm.toValue = [NSNumber numberWithFloat:0.5];
    //fromValue 開始畫時已經存在的部分的量
    strokeEndAm.fromValue = [NSNumber numberWithFloat:0];
    strokeEndAm.duration = 0.75;
    strokeEndAm.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.42 :0.0 :1.0 :0.55];
    //動畫結束后,保留結束時的模樣
    strokeEndAm.fillMode = kCAFillModeForwards;
    //將動畫加入firstWormShapeLayer的代碼省略了.

將CABasicAnimation對象的動畫關鍵路徑為strokeEnd,fromValue=0,toValue=0.5.效果就是動畫從空開始畫,到一半路徑時結束,效果如下:

worm-strokeEndAm1.gif

第二步 實現紅色蟲收縮的動畫

    //蟲子縮小
    CABasicAnimation *strokeStartAm = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    strokeStartAm.toValue = [NSNumber numberWithFloat:0.5];
    strokeStartAm.fromValue = [NSNumber numberWithFloat:0];
    strokeStartAm.duration = 0.45;
    //如果不被Group加入的話,CACurrentMediaTime() + 0.75 才表示延遲0.75秒.
    strokeStartAm.beginTime = 0.75;//延遲一秒執行
    strokeStartAm.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    strokeStartAm.fillMode = kCAFillModeForwards;

將CABasicAnimation對象的動畫關鍵路徑為strokeStart,fromValue=0,toValue=0.5.效果就是動畫開始執行時就從起點也就是左端慢慢消失直到一半路徑的位置停止.設置延遲0.75秒的作用就是等待蟲子拉伸動畫結束,再執行縮小.效果如下:
(提醒一下,下圖是單獨執行縮小動畫,不包括上面的拉伸動畫的結果,由于動畫延遲0.75秒,所以這0.75秒會顯示完整路徑,結合拉伸動畫效果就正常了)

worm-strokeStartAm1.gif

將第一二步動畫放入一個組里同時執行,看看效果

    CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
    animationGroup.animations = [NSArray arrayWithObjects: strokeEndAm, strokeStartAm, nil];
    animationGroup.repeatCount = HUGE_VALF;
    //動畫總時間應該以組中動畫時間最長為標準
    animationGroup.duration = WormRunTime * 2;
    [self.firstWormShapeLayer addAnimation:animationGroup forKey:WORM_ANIMATION_KEY_FIRST];
worm-strokeEndAm1-strokeStartAm1.gif

第三步 實現蟲子第二階段的拉伸

    //蟲子拉伸2
    CABasicAnimation *strokeEndAm2 = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    strokeEndAm2.toValue = [NSNumber numberWithFloat:0.5 + 0.5];
    //fromValue 開始畫時已經存在的部分的量
    strokeEndAm2.fromValue = [NSNumber numberWithFloat:0.5 + 0];
    strokeEndAm2.duration = 0.75;
    strokeEndAm2.beginTime = 1.2;
    strokeEndAm2.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.42 :0.0 :1.0 :0.55];
    strokeEndAm2.fillMode = kCAFillModeForwards;

蟲子第二階段的拉伸和第一階段基本一樣,需要改變的是toValue,fromValue的值,還有動畫的開始時間必須是第一階段結束后立即執行,一共是延遲1.2s,看看效果.

worm-strokeEndAm2.gif

第四步 實現蟲子第二階段的縮小

    //蟲子縮小2
    CABasicAnimation *strokeStartAm2 = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    strokeStartAm2.toValue = [NSNumber numberWithFloat:0.5 + 0.5];
    strokeStartAm2.fromValue = [NSNumber numberWithFloat:0.5 + 0];
    strokeStartAm2.duration = 0.45;
    strokeStartAm2.beginTime = 0.75 + 1.2;//延遲一秒執行
    strokeStartAm2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

和第一階段類似,看看效果.

worm-strokeStartAm2.gif

現在把前四步都放入組中同時進行,看看效果.

    animationGroup.animations = [NSArray arrayWithObjects: strokeStartAm, strokeEndAm, strokeEndAm2, strokeStartAm2, nil];
worm-strokeEndAm1&2-strokeStartAm1&2.gif

第五步 實現蟲子持續移動的效果

    //平移動畫
    CABasicAnimation *xTranslationAm = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
    xTranslationAm.toValue = [NSNumber numberWithFloat: (40 / -1.0)];
    xTranslationAm.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    xTranslationAm.duration = 1.18;
    xTranslationAm.fillMode = kCAFillModeForwards;
    
    CABasicAnimation *xTranslationAm2 = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
    xTranslationAm2.toValue = [NSNumber numberWithFloat: (40 / -1.0) * 2];
    xTranslationAm2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    xTranslationAm2.duration = 1.18;
    xTranslationAm2.beginTime = 1.20;
    xTranslationAm2.fillMode = kCAFillModeForwards;

持續移動可以沿x軸平移來實現,平移要分兩部分,第一階段移動結束后稍微停留一下,再繼續第二階段的移動.每一個階段的移動停留時間和蟲子拉伸縮小總時長一樣.將平移和上面四步動畫結合,看看效果

worm-strokeEndAm1&2-strokeStartAm1&2-translation.x.gif

這樣,紅色蟲的動畫效果基本就實現了.

整合動畫實現最終效果

現在還差黃色和綠色部分的動畫.那么怎么實現黃色部分?注意觀察,如果蟲子在拉伸還沒結束的時候就開始縮小,是不是效果就是黃色那樣!如果黃色拉伸時間比紅色慢,其他時間比例都不變,是不是可以實現紅色先走隨后拉動黃色呢?答案是肯定的.
  黃色和綠色的動畫邏輯和紅色是一模一樣的,不同的就是動畫的參數設置不同,下面給出黃色和綠色的參數.
先看看黃色蟲子的參數設置.

    //蟲子拉伸
    CABasicAnimation *strokeEndAm = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    strokeEndAm.toValue = [NSNumber numberWithFloat:0.5];
    //fromValue 開始畫時已經存在的部分的量
    strokeEndAm.fromValue = [NSNumber numberWithFloat:0.010];
    strokeEndAm.duration = 0.75;
    strokeEndAm.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.42 :0.0 :1.0 :0.55];
    strokeEndAm.fillMode = kCAFillModeForwards;
    
    //蟲子縮小
    CABasicAnimation *strokeStartAm = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    strokeStartAm.toValue = [NSNumber numberWithFloat:0.490];
    strokeStartAm.fromValue = [NSNumber numberWithFloat:0];
    strokeStartAm.duration = 0.70;
    strokeStartAm.beginTime = 0.50;
    strokeStartAm.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    strokeStartAm.fillMode = kCAFillModeForwards;
    
    
    //蟲子拉伸2 拉伸的第二階段,必須讓上一層的第二階段先動
    CABasicAnimation *strokeEndAm2 = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    strokeEndAm2.toValue = [NSNumber numberWithFloat:0.5 + 0.5];
    //fromValue 開始畫時已經存在的部分的量
    strokeEndAm2.fromValue = [NSNumber numberWithFloat:0.5];
    strokeEndAm2.duration = 0.75;
    strokeEndAm2.beginTime = 1.2 + 0.15;
    strokeEndAm2.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.42 :0.0 :1.0 :0.55];
    strokeEndAm2.fillMode = kCAFillModeForwards;
    
    //蟲子縮小2
    CABasicAnimation *strokeStartAm2 = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    strokeStartAm2.toValue = [NSNumber numberWithFloat:0.5 + 0.5];
    strokeStartAm2.fromValue = [NSNumber numberWithFloat:0.5 + 0];
    strokeStartAm2.duration = 0.30;
    strokeStartAm2.beginTime =0.15 + 0.75 + 1.2;
    strokeStartAm2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    
    
    //平移動畫
    CABasicAnimation *xTranslationAm = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
    xTranslationAm.toValue = [NSNumber numberWithFloat: (40 / -1.0)];
    xTranslationAm.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    xTranslationAm.duration = 1.18;
    xTranslationAm.fillMode = kCAFillModeForwards;
    
    CABasicAnimation *xTranslationAm2 = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
    xTranslationAm2.toValue = [NSNumber numberWithFloat: (40 / -1.0) * 2];
    xTranslationAm2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    xTranslationAm2.duration = 1.18;
    xTranslationAm2.beginTime = 1.20;
    xTranslationAm2.fillMode = kCAFillModeForwards;
    
    
    
    CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
    animationGroup.animations = [NSArray arrayWithObjects: strokeStartAm, strokeEndAm, xTranslationAm, strokeEndAm2, strokeStartAm2, xTranslationAm2, nil];
    animationGroup.repeatCount = HUGE_VALF;
    //動畫總時間應該以組中動畫時間最長為標準
    animationGroup.duration = 1.2 * 2;
    [self.secondWormShapeLayer addAnimation:animationGroup forKey:nil];

將黃色和紅色兩個層合并在一起同時執行動畫,看看效果

worm-first&secondLayer.gif

下面是綠色蟲子的參數.

    //蟲子拉伸
    CABasicAnimation *strokeEndAm = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    strokeEndAm.toValue = [NSNumber numberWithFloat:0.5];
    //fromValue 開始畫時已經存在的部分的量
    strokeEndAm.fromValue = [NSNumber numberWithFloat:0.010];
    strokeEndAm.duration = 0.75;
    strokeEndAm.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.42 :0.0 :1.0 :0.55];
    strokeEndAm.fillMode = kCAFillModeForwards;
    
    //蟲子縮小
    CABasicAnimation *strokeStartAm = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    strokeStartAm.toValue = [NSNumber numberWithFloat:0.490 ];
    strokeStartAm.fromValue = [NSNumber numberWithFloat:0];
    strokeStartAm.duration = 0.90;
    strokeStartAm.beginTime = 0.25;
    strokeStartAm.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    strokeStartAm.fillMode = kCAFillModeForwards;
    
    
    //蟲子拉伸2 拉伸的第二階段,必須讓上一層的第二階段先動
    CABasicAnimation *strokeEndAm2 = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    strokeEndAm2.toValue = [NSNumber numberWithFloat:0.5 + 0.5];
    //fromValue 開始畫時已經存在的部分的量
    strokeEndAm2.fromValue = [NSNumber numberWithFloat:0.5];
    strokeEndAm2.duration = 0.75;
    strokeEndAm2.beginTime = 1.2 + 0.15 + 0.20;
    strokeEndAm2.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.42 :0.0 :1.0 :0.55];
    strokeEndAm2.fillMode = kCAFillModeForwards;
    
    //蟲子縮小2
    CABasicAnimation *strokeStartAm2 = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    strokeStartAm2.toValue = [NSNumber numberWithFloat:0.5 + 0.5];
    strokeStartAm2.fromValue = [NSNumber numberWithFloat:0.5 + 0];
    strokeStartAm2.duration = 0.30;
    strokeStartAm2.beginTime =0.15 + 0.75 + 1.2;
    strokeStartAm2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    
    
    //平移動畫
    CABasicAnimation *xTranslationAm = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
    xTranslationAm.toValue = [NSNumber numberWithFloat: (HUDWith / -1.0 + 10)];
    xTranslationAm.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    xTranslationAm.duration = 1.18;
    xTranslationAm.fillMode = kCAFillModeForwards;
    
    CABasicAnimation *xTranslationAm2 = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
    xTranslationAm2.toValue = [NSNumber numberWithFloat: (HUDWith / -1.0 + 10) * 2];
    xTranslationAm2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    xTranslationAm2.duration = 1.18;
    xTranslationAm2.beginTime = 1.20;
    xTranslationAm2.fillMode = kCAFillModeForwards;
    
    
    
    CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
    animationGroup.animations = [NSArray arrayWithObjects: strokeStartAm, strokeEndAm, xTranslationAm, strokeEndAm2, strokeStartAm2, xTranslationAm2, nil];
    animationGroup.repeatCount = HUGE_VALF;
    //動畫總時間應該以組中動畫時間最長為標準
    animationGroup.duration = 1.2 * 2;
    [self.thirdWormShapeLayer addAnimation:animationGroup forKey:nil];

最終將紅黃綠三個蟲子合并,看看合成的效果

worm-first&second&thirdLayer.gif

  .

優化代碼,實現HUD的功能

三個蟲子的動畫邏輯都是一下的,所以這里可以優化一下代碼結構,最后加上UIView動畫,實現一個指示器(HUD),這樣就可以提供給別人使用了~
  先把蟲子動畫中變化的參數提取出來,封裝在一個方法里,然后三個蟲分別給出自己的參數調用一下方法就可以得到動畫對象了.封裝后的代碼如下.(其中有一些是全局靜態變量和宏定義,不用太在意,具體可參考Git上的完整源碼)


/**
 *  第三條蟲子嚅動
 */
-(void)thirdWormAnimation{
    
    CAAnimationGroup *animationGroup = [self baseWormAnimationWithEnd1From:0.010 end1To:0.5 end1Duration:0.75 start1From:0 start1To:0.490 start1Duration:0.9 start1Begin:0.25 end2From:0.5 end2To:0.5 + 0.5 end2Duration:0.75 end2Begin:WormRunTime + 0.15 + 0.20 start2From:0.5 start2To:0.5 + 0.5 start2Duration:0.30 start2Begin:0.15 + 0.75 + WormRunTime];
    
    [self.thirdWormShapeLayer addAnimation:animationGroup forKey:WORM_ANIMATION_KEY_THIRD];
}

/**
 *  第二條蟲子嚅動
 */
-(void)secondWormAnimation{
    
    CAAnimationGroup *animationGroup = [self baseWormAnimationWithEnd1From:0.010 end1To:0.5 end1Duration:0.75 start1From:0 start1To:0.490 start1Duration:0.70 start1Begin:0.50 end2From:0.5 end2To:0.5 + 0.5 end2Duration:0.75 end2Begin:WormRunTime + 0.15 start2From:0.5 start2To:0.5 + 0.5 start2Duration:0.30 start2Begin:0.15 + 0.75 + WormRunTime];
    
    [self.secondWormShapeLayer addAnimation:animationGroup forKey:WORM_ANIMATION_KEY_SECOND];
}

/**
 *  第一條蟲子嚅動 (最底部的那條)
 */
-(void)firstWormAnimation{
    CAAnimationGroup *animationGroup = [self baseWormAnimationWithEnd1From:0 end1To:0.5 end1Duration:0.75 start1From:0 start1To:0.5 start1Duration:0.45 start1Begin:0.75 end2From:0.5 end2To:0.5 + 0.5 end2Duration:0.75 end2Begin:1.2 start2From:0.5 start2To:0.5 + 0.5 start2Duration:0.45 start2Begin:0.75 + WormRunTime];
    
    [self.firstWormShapeLayer addAnimation:animationGroup forKey:WORM_ANIMATION_KEY_FIRST];
}

-(CAAnimationGroup *)baseWormAnimationWithEnd1From:(CGFloat)end1FromValue end1To:(CGFloat)end1ToValue end1Duration:(CGFloat)end1Duration start1From:(CGFloat)start1FromValue start1To:(CGFloat)start1ToValue start1Duration:(CGFloat)start1Duration start1Begin:(CGFloat)start1BeginTime end2From:(CGFloat)end2FromValue end2To:(CGFloat)end2ToValue end2Duration:(CGFloat)end2Duration end2Begin:(CGFloat)end2BeginTime start2From:(CGFloat)start2FromValue start2To:(CGFloat)start2ToValue start2Duration:(CGFloat)start2Duration start2Begin:(CGFloat)start2BeginTime{
    
    
    //蟲子拉伸1
    CABasicAnimation *strokeEndAm = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    strokeEndAm.toValue = [NSNumber numberWithFloat:end1ToValue];
    //fromValue 開始畫時已經存在的部分的量
    strokeEndAm.fromValue = [NSNumber numberWithFloat:end1FromValue];
    strokeEndAm.duration = end1Duration;
    strokeEndAm.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.42 :0.0 :1.0 :0.55];
    strokeEndAm.fillMode = kCAFillModeForwards;
    
    //蟲子縮小1
    CABasicAnimation *strokeStartAm = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    strokeStartAm.toValue = [NSNumber numberWithFloat:start1ToValue];
    strokeStartAm.fromValue = [NSNumber numberWithFloat:start1FromValue];
    strokeStartAm.duration = start1Duration;
    //如果不被Group加入的話,CACurrentMediaTime() + 1 才表示延遲1秒.
    strokeStartAm.beginTime = start1BeginTime;//延遲執行
    strokeStartAm.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    strokeStartAm.fillMode = kCAFillModeForwards;
    
    
    //蟲子拉伸2 拉伸的第二階段,必須讓上一層的第二階段先動
    CABasicAnimation *strokeEndAm2 = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    strokeEndAm2.toValue = [NSNumber numberWithFloat:end2ToValue];
    //fromValue 開始畫時已經存在的部分的量
    strokeEndAm2.fromValue = [NSNumber numberWithFloat:end2FromValue];
    strokeEndAm2.duration = end2Duration;
    strokeEndAm2.beginTime = end2BeginTime;
    strokeEndAm2.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.42 :0.0 :1.0 :0.55];
    strokeEndAm2.fillMode = kCAFillModeForwards;
    
    //蟲子縮小2
    CABasicAnimation *strokeStartAm2 = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    strokeStartAm2.toValue = [NSNumber numberWithFloat:start2ToValue];
    strokeStartAm2.fromValue = [NSNumber numberWithFloat:start2FromValue];
    strokeStartAm2.duration = start2Duration;
    //如果不被Group加入的話,CACurrentMediaTime() + 1 才表示延遲1秒.
    strokeStartAm2.beginTime = start2BeginTime;//延遲一秒執行
    strokeStartAm2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    
    
    //平移動畫
    CABasicAnimation *xTranslationAm = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
    xTranslationAm.toValue = [NSNumber numberWithFloat: ( (CCWormHUDViewWith / 2.0) / -1.0)];
    xTranslationAm.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    xTranslationAm.duration = 1.18;
    xTranslationAm.fillMode = kCAFillModeForwards;
    
    CABasicAnimation *xTranslationAm2 = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
    xTranslationAm2.toValue = [NSNumber numberWithFloat: ( (CCWormHUDViewWith / 2.0) / -1.0) * 2];
    xTranslationAm2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    xTranslationAm2.duration = 1.18;
    xTranslationAm2.beginTime = 1.20;
    xTranslationAm2.fillMode = kCAFillModeForwards;
    
    
    
    CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
    animationGroup.animations = [NSArray arrayWithObjects: strokeStartAm, strokeEndAm, xTranslationAm, strokeEndAm2, strokeStartAm2, xTranslationAm2, nil];
    animationGroup.repeatCount = HUGE_VALF;
    //動畫總時間應該以組中動畫時間最長為標準
    animationGroup.duration = WormRunTime * 2;
    
    return animationGroup;
}

接下來為這個動畫所在的視圖添加UIView級別動畫,做成一個HUD,并且提供兩個公開的實例方法用于顯示和隱藏HUD
  講了半天Core Animation動畫,不知道你是不是累了.往下看,接下來你一定會覺得使用UIView級別的動畫是多么輕松,讓我們開始輕松一下.下面看一下怎么在顯示和隱藏視圖時使用動畫.

-(void)startLodingWormHUD{
    self.isShowing = YES;
    [self.presentingView addSubview:self];
    //動起來.顯示指示器的時候才開始設置動畫效果
    [self firstWormAnimation];
    [self secondWormAnimation];
    [self thirdWormAnimation];
    
    self.transform = CGAffineTransformMakeScale(0.1, 0.1);
    [UIView animateKeyframesWithDuration:0.6 delay:0.0 options:0 animations:^{
        [UIView addKeyframeWithRelativeStartTime:0 relativeDuration:0.5 animations:^{
            self.transform = CGAffineTransformMakeScale(1.3, 1.3);
        }];
        [UIView addKeyframeWithRelativeStartTime:0.5 relativeDuration:0.5 animations:^{
            self.transform = CGAffineTransformIdentity;
        }];
    } completion: nil];
}

-(void)endLodingWormHUD{
    //隱藏指示器,同時移除動畫
    [UIView animateKeyframesWithDuration:0.6 delay:0 options:0 animations:^{
        [UIView addKeyframeWithRelativeStartTime:0 relativeDuration:0.5 animations:^{
            self.transform = CGAffineTransformMakeScale(1.2, 1.2);
        }];
        [UIView addKeyframeWithRelativeStartTime:0.5 relativeDuration:0.5 animations:^{
            self.transform = CGAffineTransformMakeScale(0.1, 0.1);
        }];
        
    } completion:^(BOOL finished){
        self.isShowing = NO;
        [self.firstWormShapeLayer removeAnimationForKey:WORM_ANIMATION_KEY_FIRST];
        [self.secondWormShapeLayer removeAnimationForKey:WORM_ANIMATION_KEY_SECOND];
        [self.thirdWormShapeLayer removeAnimationForKey:WORM_ANIMATION_KEY_THIRD];
        [self removeFromSuperview];
    }];
}

如果在此之前你還不了解UIView動畫的基本變化,可參考理解iOS中CGAffineTransform與矩陣的關系

最后看一下結合指示器后勤奮蟲子的最終效果

a-diligent-worm-in-iOS.gif

后語

從一開始接觸動畫到實現一個完整的動畫中間,肯定會經歷很多困惑與不解,然后就要不停搜索查文檔了,這個過程雖然很累,但是一旦你把效果實現了,原理想明白了,那是多么享受的一件事情.這里我給一個建議,由于iOS動畫中有太多類和屬性,方法,你不可能一下子就把他們都明白透徹.一開始肯定是要先找準幾個基本的概念先理解了,然后嘗試動手寫demo,實現一些基礎效果.再慢慢將他們結合起來朝著你的目標效果前進.如果中途發現走不動了,效果實現很久也不能完成,那很可能是少用了某些對象,屬性,方法,或者你的基礎圖形都是錯的.比如本文,一開始沒有使用動畫組,發現很多效果做不出,然后我查了一下文檔資料發現可以使用動畫組實現,效果很容易就出來了.基礎圖形也是,從一個半圓弧到兩個半圓弧,實現起來事半功倍.這個過程需要時間和經驗,慢慢來吧,一起進步!
  至此,勤奮蟲子的完整效果都已經實現了.還有沒多具體細節上面并沒有提到.本文所實現的指示器源碼和使用Demo可以從本文源碼:GitHub中找到,可以的話幫忙點一下Star,好讓更多人看到.
  希望大家能從本文中得到自己需要的東西,如果你有更好的想法或發現本文的不足,歡迎指出來.更多文章我還來不及發到簡書上,可在我的博客查看

參考閱讀

本文源碼:GitHub
iOS開發系列--讓你的應用“動”起來
按鈕打勾動畫特效
理解iOS中CGAffineTransform與矩陣的關系
下面是工具
CAMediaTimingFunction playground
Sketch下載 密碼:5d2b
PaintCode下載 密碼:ftpt

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,048評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,414評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,169評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,722評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,465評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,823評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,813評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,000評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,554評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,295評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,513評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,722評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,125評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,430評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,237評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,482評論 2 379

推薦閱讀更多精彩內容

  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,170評論 4 61
  • 在iOS中隨處都可以看到絢麗的動畫效果,實現這些動畫的過程并不復雜,今天將帶大家一窺ios動畫全貌。在這里你可以看...
    每天刷兩次牙閱讀 8,550評論 6 30
  • 在中國自由知識分子階層里面,吳曉波無疑是優秀代表。我以前陸續看過他的幾本書,比如《大敗局》,《激蕩三十年》。。。那...
    愛吃香蕉的猴閱讀 205評論 1 2
  • 第七篇 時間不早了,但還得忙著交作業,有點小小的為難,但依然拿起手機隨意寫。 今天的主題是占星,我不太熟悉,但知道...
    正念此心閱讀 317評論 0 1
  • 看了加勒比海盜5,好好看。買了屈臣氏漱口水,試試效果。吃了今年夏天第一個冰激凌和雪碧,爽爆了哈哈哈。明天大掃除,扔...
    明天你好郭郭閱讀 265評論 0 0