iOS CALayer組合動畫實現loadingView

前言

hihi,勇敢的小夥伴兒們,大家好,loadingView在生活中已經是很常見了,各大平臺都有不同的loadingView,最常見的瀏覽器就能看到的如下圖這種Google Chrome的等待動畫,

還有很多種其他的loading動畫需要我們學習,

有時候我們使用做好的gif就可以實現,那麼我們爲什麼要學習動畫的繪製呢?

因爲CALayer動畫可以實現比UIView動畫更豐富、更底層、效率更高的動畫。能用代碼實現的儘量不用資源文件,實際上也是對我們應用的一種瘦身。

話不多說,送上我的Demo地址:CALayerDemo1 (名字起得很隨意hhhh),然後開始我們今天的學習吧~

正文

這篇文章通過兩個加載動畫向大家介紹CALayer的動畫。按照面向對象的思想說,Layer其實就是一個模型類,它包含若干屬性,並沒有任何處理邏輯的方法,這些屬性影響着顯示在Layer中的內容。我們先來看看UIView和CALayer之間有什麼區別和聯繫。

  • 聯繫:Layer是View背後的那個女人。每一個UIView後面都有對應的CALayer,大家看到的在UIView中顯示的內容其實是在CALayer中。
  • 區別:
    • View有複雜的、各種組合的佈局機制。Layer只有極簡單的佈局。
    • View可以響應用戶交互。Layer不能響應用戶交互。
    • View中的繪畫邏輯有CPU執行。Layer中的繪畫直接有GPU執行。
    • View有豐富的、功能強大的子類。Layer只有很少的幾個子類。
    • View動畫屬性較少,侷限性較大。Layer由於更底層、動畫屬性更多,所以可以實現出更靈活、更豐富的動畫。

第一個CALayer動畫

Layer動畫系列的文章,我不準備系統的從簡單到複雜的知識進行講解,我會通過各種實戰示例,示例中用到什麼知識點就講什麼知識點。

第一個動畫讓我們來實現Google Chrome瀏覽器加載時頁簽上的等待動畫~

新建項目CALayerDemo1,打開Main.storyboard,拖拽一個UIView到ViewController中,添加好約束,自行設置ViewController和UIView的背景色,這裏UIView的背景色我設置爲無色:

然後添加該View的Outlet到ViewController中,這個UIView就是要展示加載動畫的View:

@property (weak, nonatomic) IBOutlet UIView *loadingView;

打開ViewController.m,申明一個常量屬性ovalShapleLayer

CAShapeLayer *ovalShapeLayer = [CAShapeLayer layer];

ovalShapleLayer的類型是CAShapleLayer,它是CALayer的爲數不多的子類之一。它的作用是在屏幕上畫出各種形狀,不論是簡單的圓形、方形還是複雜的五角星或不規則圖形都難不住它。CAShapeLayer有如下一些主要屬性:

  • strokeColor:筆畫顏色。
  • strokeStart:筆畫開始位置。
  • strokeEnd:筆畫結束位置。
  • fillColor:圖形填充顏色。
  • lineWidth:筆畫寬度,即筆畫的粗細程度。
  • lineDashPattern:虛線模式。
  • path:圖形的路徑。
  • lineCap:筆畫未閉合位置的形狀。

我們之所要申明一個CAShapeLayer,是因爲要用它在屏幕上畫出一個圓形。下面在viewDidLoad()方法中添加如下代碼:

    ovalShapeLayer.strokeColor = [UIColor whiteColor].CGColor;
    ovalShapeLayer.fillColor = [UIColor clearColor].CGColor;
    ovalShapeLayer.lineWidth = 7;

這幾個屬性剛纔已經向大家介紹過了,這三行代碼的意思是我們畫出的圓形筆畫顏色是白色,沒有填充色,筆畫的寬度爲7。接着我們申明這個圓形的半徑,使這個圓形的大小爲容納它視圖大小的80%:

    CGFloat ovalRadius = _loadingView.frame.size.width / 2. * 0.8;

最後我們設置ovalShapeLayer的路徑,這是最關鍵的一步,因爲你要告知CAShapeLayer按照什麼路徑繪製圖形,讓我們接着添加如下代碼:

    CGPathRef path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(_loadingView.frame.size.width/2 - ovalRadius , _loadingView.frame.size.height/2 - ovalRadius,  ovalRadius * 2,  ovalRadius * 2)].CGPath;
    ovalShapeLayer.path = path;

這裏出現了新面孔UIBezierPath,它可以創建基於矢量的路徑,是Core Graphics框架關於path的封裝。UIBezierPath可以定義簡單的形狀路徑,如橢圓、矩形,或者有多個直線和曲線段組成的形狀。在這裏我們要使用它的初始化方法init(ovalInRect rect: CGRect)定義一個正圓的路徑。設置完路徑後,將ovalShapeLayer添加到loadingView視圖的Layer中,它就可以按照設定好的路徑在loadingView中繪製圖形了:

[_loadingView.layer addSublayer:ovalShapeLayer];

編譯運行看看效果:

完美的一個圓形。接下來我們要做的是讓這個圓只顯示一部分,因爲Google的加載動畫只有大概五分之二的圓形輪廓。讓我們繼續將目光集中在viewDidLoad()方法中,在[_loadingView.layer addSublayer:ovalShapeLayer]這行代碼上面添加另一行代碼:

ovalShapeLayer.strokeEnd = 0.4;

上面的代碼將ovalShapeLayerstrokeEnd屬性設置爲0.4,意思是ovalShapeLayer在繪製圓形時只畫整個圓形的五分之二,即筆畫結束的位置在整個圓形輪廓的五分之二處。編譯運行看看效果:

看來是我們想要的效果,但是仍有一處細節需要我們完善,看看Google的加載動畫,藍色的部分圓形輪廓兩頭是圓形的,而我們的圓形輪廓兩頭是方形的。這個問題很好解決,仍然在[_loadingView.layer addSublayer:ovalShapeLayer]這行代碼上面添加一行代碼:

ovalShapeLayer.lineCap = kCALineCapRound;

這行代碼的意思是將筆畫兩頭的形狀設置爲圓形,對應的還有兩個常量kCALineCapButt ,kCALineCapSquare,大家可以試試。再次編譯運行看看效果:

到目前爲止,我們通過CALayer繪製出了動畫的主體,接下來要讓它動起來。在ViewController.m中添加beginSimpleAnimate()方法:

- (void)beginSimpleAnimation {
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
    animation.duration = 1.5;
    animation.fromValue = 0;
    animation.toValue = @(M_PI * 2);
    animation.repeatCount = HUGE;
    [_loadingView.layer addAnimation:animation forKey:nil];
}

在這個方法中,我們又看到了新面孔CABasicAnimation,該類提供了基本的、單關鍵幀的Layer屬性動畫,通過animationWithKeyPath:初始化方法,根據keyPath創建不同的CAPropertyAnimation實例。常用的keyPath有如下一些:

  • transform.rotation:旋轉動畫。
  • transform.ratation.x:按x軸旋轉動畫。
  • transform.ratation.y:按y軸旋轉動畫。
  • transform.ratation.z:按z軸旋轉動畫。
  • transform.scale:按比例放大縮小動畫。
  • transform.scale.x:在x軸按比例放大縮小動畫。
  • transform.scale.y:在y軸按比例放大縮小動畫。
  • transform.scale.z:在z軸按比例放大縮小動畫。
  • position:移動位置動畫。
  • opacity:透明度動畫。

以上只是一部分常用的動畫keyPath,更多的希望大家在實際運用中去挖掘。在beginSimpleAnimation()方法中,我們使用了transform.rotation,創建了一個旋轉動畫的實例,然後給該動畫設置了四個屬性:

  • duration:動畫持續時間。
  • fromValue:動畫起始值。
  • toValue:動畫結束值。
  • repeatCount:重複次數。

該方法設置這幾個屬性的含義爲使動畫主體不停的旋轉,旋轉一圈的時間爲1.5秒。以上這幾個概念在UIView的動畫中同樣存在,大家應該都已經比較熟悉了。然後使用Layer的addAnimation方法將旋轉動畫實例添加到目標Layer中,該方法的key是用來標示添加的動畫,便於以後重複使用時能方便的檢索,如果沒有需求可以傳值nil。最後viewWillAppear方法中調用beginSimpleAnimation()方法:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [self beginSimpleAnimation];
}

編譯運行看看效果:

至此我們的第一個簡單的CALayer動畫就完成了,在下一節我們一起實現一個更加有意思的加載動畫,從而向大家介紹新的動畫類型及動畫組合。

Stroke Animation與Animation Group

讓我們先看看要實現的效果:

這種加載動畫在很多應用中都出現過,比如網易新聞、Win版的谷歌瀏覽器中都有使用。下面就讓我們一步一步來實現吧,首先打開Main.storyboard,新添加一個UIView,在ViewController.m中添加Outlet:

@property (weak, nonatomic) IBOutlet UIView *animationView;

然後定義一個新的CAShapeLayer:

 CAShapeLayer *aniShapeLayer = [CAShapeLayer layer];

viewDidLoad()方法中對它進行設置,並將其添加到剛纔創建的animationView中:

    aniShapeLayer.strokeColor = [UIColor whiteColor].CGColor;
    aniShapeLayer.fillColor = [UIColor clearColor].CGColor;
    aniShapeLayer.lineWidth = 7;
    CGFloat aniRadius = _animationView.frame.size.width / 2. * 0.8;
    CGPathRef aniPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(_animationView.frame.size.width/2 - aniRadius , _animationView.frame.size.height/2 - aniRadius,  aniRadius * 2,  aniRadius * 2)].CGPath;
    aniShapeLayer.path = aniPath;
    aniShapeLayer.lineCap = kCALineCapRound;
    [_animationView.layer addSublayer:aniShapeLayer];

這些操作在上一個動畫都已經做過一遍了,這裏就不再解釋。編譯運行看看是否屏幕上又出現了一個圓圈呢:

接下來在ViewController.m中添加一個方法beginComplexAnimation()

- (void)beginComplexAnimation {
    /*
     strokeStartAnimate動畫讓繪製圓圈的筆畫起始位置從–0.5開始,目的是讓筆畫起始繪製時等待一段時間,也就是起始位置延遲繪製。而strokeEndAnimate動畫讓繪製圓圈的筆畫終止位置正常的從0繪製到1。這樣一來筆畫兩頭繪製的時間就會不一樣,會有一個時間差,這樣就有圓圈不斷繪製又不斷被擦除的效果。
     */
    //筆畫開始動畫
    CABasicAnimation *strokeStartAnimate = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    //畫到0.5的時候筆畫結束的動畫執行形成在半圓處出現擦除效果0.25則爲在1/4圓處出現擦除效果
    strokeStartAnimate.fromValue = @(-0.5);
    strokeStartAnimate.toValue = @(1);
    
    //筆畫結束動畫
    CABasicAnimation *strokeEndAnimate = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    //0代表繪製路徑的起始位置
    strokeEndAnimate.fromValue = @(0);
    //1代表繪製路徑的終止位置
    strokeEndAnimate.toValue = @(1);
    
    CAAnimationGroup *group = [[CAAnimationGroup alloc] init];
    group.duration = 1.5;
    group.repeatCount = HUGE;
    group.animations = @[strokeStartAnimate,strokeEndAnimate];
    //用_animationView.layer 添加動畫 不可行 需要加在layer上
    [aniShapeLayer addAnimation:group forKey:nil];
}

這裏出現了兩個新的動畫類型,筆畫開始動畫和筆畫結束動畫,我們雖然使用CAShapeLayer繪製了一個圓圈,但是它也存在筆畫起始位置和筆畫終止位置,只不過它倆在同一個位置而已,筆畫動畫的位置取值在0–1之間,0代表繪製路徑的起始位置,1代表繪製路徑的終止位置。

所以strokeStartAnimate動畫讓繪製圓圈的筆畫起始位置從–0.5開始,目的是讓筆畫起始繪製時等待一段時間,也就是起始位置延遲繪製。而strokeEndAnimate動畫讓繪製圓圈的筆畫終止位置正常的從0繪製到1。這樣一來筆畫兩頭繪製的時間就會不一樣,會有一個時間差,這樣就有圓圈不斷繪製又不斷被擦除的效果。

strokeStartAnimatestrokeEndAnimate是兩個動畫,如何作用於一個Layer上呢?這時就要用到CAAnimationGroup,顧名思義它是將多個動畫組成一個組,在一個動畫組裏,子動畫會同時進行。動畫組可以設置動畫持續時間、重複次數以及子動畫數組。最後將動畫組加在Layer上即可。

最後在viewWillAppear()方法中調用beginComplexAnimation()方法:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [self beginSimpleAnimation];
    [self beginComplexAnimation];
}

編譯運行看看效果:

總結

CALayer動畫可以實現比UIView動畫更豐富、更底層、效率更高的動畫。但是在實際的應用開發中,我們應該按需所用,能用UIView動畫實現的我們就可以不用CALayer動畫,它倆沒有誰優誰劣之分。這篇文章只是CALayer動畫的引子,讓大家對CALayer動畫有初步的瞭解和認識,之後我在文章中會通過更多的實例幫大家更深入的認識CALayer動畫,從而提升自己應用的用戶體驗。

感謝大家看到最後,這篇文章是我按照付宇軒(@DevTalking的博文裏一步一步學習,改成OC的代碼並添加自己的學習筆記做了自己的Demo完成的,在這裏非常感謝前輩的分享,Swift的原文地址:CALayer Animation - Loading Indicator,如果有問題,還請大家留言給我,感激不盡~

另外,第二個loading圖的實現也希望大家能去動腦想一下~

經過半小時的努力,emmm,我的實現效果如下,代碼在demo裏更新了~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章