你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 動畫黃金搭檔:CADisplayLink & CAShapeLayer

動畫黃金搭檔:CADisplayLink & CAShapeLayer

編輯:IOS開發基礎

我們在開發中有時會遇到一些看似非常復雜的動畫,不知該如何下手,今天的這篇文章中我會講到如何利用CADisplayLink和CAShapeLayer來構建一些復雜的動畫,希望能在你下次構建動畫中,給你一些啟發。

在接下來的文章中,我們會構建如下的一個動畫:

8f7a6fe0gw1f6lb4ym7ovg207002z3ym.gif

該動畫是在du的輪廓中進行,類似一個镂空效果,輪廓的填充是用雙波浪的形式,類似於水流慢慢注入容器的過程。
動畫使用CADisplayLink來進行刷新,保證了動畫的流程性,利用CAShapeLayer來構建波浪的輪廓,最後利用CALayer的mask屬性來實現逐漸填充的過程。

背景知識介紹

在動畫創建過程的講解之前,先介紹一下會使用到的一些知識點:

  • CADisplayLink

  • UIBezierPath

  • CAShapeLayer

  • mask

如果你已經對這些概念有了充分的了解,可以略過背景知識介紹這一節。

1、CADisplayLink

用繪制的方式構建的動畫,必然需要不斷的刷新繪制的內容來呈現流暢的動畫,CADisplayLink就像是一個定時器,每隔幾毫秒刷新一次屏幕。能讓我們以和屏幕刷新頻率相同的頻率去刷新我們繪制到屏幕上的內容。
CADisplayLink的使用方式如下:

  _displayLink = [CADisplayLink displayLinkWithTarget:self
                                            selector:@selector(updateContent:)];
   [_displayLink addToRunLoop:[NSRunLoop currentRunLoop]
                      forMode:NSRunLoopCommonModes];

當CADisplayLink注冊到runloop以後,屏幕刷新的時候就會調用綁定到它上面的target所擁有的selector方法。停止CADisplayLink的運行非常的簡單,只需要調用它的invalidate方法。

NSTimer和CADisplayLink有什麼不同?

iOS設備的屏幕每秒會刷新60次,正常情況下CADisplayLink在屏幕每次刷新時都會調用,精確度非常高,並且CADisplayLink的使用場合相對專一,適合做UI的不停重繪,比如動畫的連續繪制。

NSTimer的使用范圍要廣泛很多,可以做單次或者循環處理某個任務,精度相比CADisplayLink要低。

2、UIBezierPath

使用UIBezierPath類可以創建基於矢量的路徑,它是Core Graphics框架關於CGPathRef類型數據的封裝,利用它創建直線或者曲線來構建我們想要的形狀,每一個直線段或者曲線段的結束位置就是下一個線段開始的地方。這些連接的直線或者曲線的集合成為subpath。一個UIBezierPath對象的完整路徑包括一個或者多個subpath。

創建一個完整的UIBezierPath對象的完整步驟如下:

  • 創建一個Bezier Path對象。

  • 使用方法moveToPoint:去設置初始線段的起點。

  • 添加line或者curve去定義一個或者多個subpath。

  • 修改UIBezierPath對象跟繪圖相關的屬性。

3、CAShapeLayer

CAShapeLayer是一個通過矢量圖形而不是bitmap來繪制的圖層子類。CAShapeLayer可以用來繪制所有通過CGPath來表示的形狀,上面講到了可以用UIBezierPath來創建任何你想要的路徑,使用CAShapeLayer的屬性path配合UIBezierPath創建的路徑,就可以呈現出我們想要的形狀。
這個形狀不一定要閉合,圖層路徑也不一定是連續不斷的,你可以在CAShapeLayer的圖層上繪制好幾個不同的形狀,但是你只有一次機會去設置它的path、lineWith、lineCap等屬性,如果你想同時設置幾個不同顏色的多個形狀,你就需要為每個形狀准備一個圖層。

下面的示例代碼是通過UIBezierPath和CAShapeLayer來創建一個簡單的火柴人。

  - (void)viewDidLoad {
    [super viewDidLoad];
    
    UIBezierPath *path = [[UIBezierPath alloc] init];
    [path moveToPoint:CGPointMake(175, 100)];
    
    [path addArcWithCenter:CGPointMake(150, 100) radius:25 startAngle:0 endAngle:2*M_PI clockwise:YES];
    [path moveToPoint:CGPointMake(150, 125)];
    [path addLineToPoint:CGPointMake(150, 175)];
    [path addLineToPoint:CGPointMake(125, 225)];
    [path moveToPoint:CGPointMake(150, 175)];
    [path addLineToPoint:CGPointMake(175, 225)];
    [path moveToPoint:CGPointMake(100, 150)];
    [path addLineToPoint:CGPointMake(200, 150)];
    
    //create shape layer
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.strokeColor = [UIColor colorWithRed:147/255.0 green:231/255.0 blue:182/255.0 alpha:1].CGColor;
    shapeLayer.fillColor = [UIColor clearColor].CGColor;
    shapeLayer.lineWidth = 5;
    shapeLayer.lineJoin = kCALineJoinRound;
    shapeLayer.lineCap = kCALineCapRound;
    shapeLayer.path = path.CGPath;
    //add it to our view
    [self.view.layer addSublayer:shapeLayer];
}

顯示的形狀如下:

69.jpg

4、mask

CALayer有一個屬性叫做mask,通常被稱為蒙版圖層,這個屬性本身也是CALayer類型,有和其他圖層一樣的繪制和布局屬性。它類似於一個子視圖,相對於父圖層(即擁有該屬性的圖層)布局,但是它卻不是一個普通的子視圖。不同於一般的subLayer,mask定義了父圖層的可見區域,簡單點說就是最終父視圖顯示的形態是父視圖自身和它的屬性mask的交集部分。

8f7a6fe0gw1f6lb50rgqbj20go06mwet.jpg

mask圖層的color屬性是無關緊要的,真正重要的是它的輪廓,mask屬性就像一個切割機,父視圖被mask切割,相交的部分會留下,其他的部分則被丟棄。
CALayer的蒙版圖層真正厲害的地方在於蒙版圖層不局限於靜態圖,任何有圖層構成的都可以作為mask屬性,這意味著蒙版可以通過代碼甚至是動畫實時生成。這也為我們實現示例中波浪的變化提供了支持。

繪制波浪輪廓

我們利用UIBezierPath來繪制波浪的輪廓,通過正弦函數和余弦函數來創建頂部的波浪曲線,在單位為1的右手直角坐標系中的曲線變化如下:

8f7a6fe0gw1f6lb51vscuj20v007sq4f.jpg

可以看到在(-2π , 2π )的范圍類,y值在[-1, 1]之間變化。
以正弦曲線為例,它可以表示為y=Asin(ωx+φ)+k,公式中各符號表示的含義:

  • A–振幅,即波峰的高度。

  • (ωx+φ)–相位,反應了變量y所處的位置。

  • φ–初相,x=0時的相位,反映在坐標系上則為圖像的左右移動。

  • k–偏距,反映在坐標系上則為圖像的上移或下移。

  • ω–角速度,控制正弦周期(單位角度內震動的次數)。

通過上面的函數,我們就能計算出波浪曲線上任意位置的坐標點。通過UIBezierPath的函數addLineToPoint來把這些點連接起來,就構建了波浪形狀的path。只要我們的設定相鄰兩點的間距夠小,就能構建出平滑的正弦曲線,構建正弦波浪的代碼如下:

  - (UIBezierPath *)createSinPath
{
    UIBezierPath *wavePath = [UIBezierPath bezierPath];
    CGFloat endX = 0;
    for (CGFloat x = 0; x < self.waveWidth + 1; x += 1) {
        endX=x;
        CGFloat y = self.maxAmplitude * sinf(360.0 / _waveWidth * (x  * M_PI / 180) * self.frequency + self.phase * M_PI/ 180) + self.maxAmplitude;
        if (x == 0) {
            [wavePath moveToPoint:CGPointMake(x, y)];
        } else {
            [wavePath addLineToPoint:CGPointMake(x, y)];
        }
    }
   
    CGFloat endY = CGRectGetHeight(self.bounds) + 10;
    [wavePath addLineToPoint:CGPointMake(endX, endY)];
    [wavePath addLineToPoint:CGPointMake(0, endY)];
    
    return wavePath;
}

顯示的效果如下:

330.jpg

在這裡我們設定了兩個正弦曲線上的點的橫坐標間距是1,現在來解釋一下通過橫坐標x來得出y的計算過程:

y = self.maxAmplitude * sinf(360.0 / _waveWidth * (x  * M_PI / 180) * self.frequency + self.phase * M_PI/ 180) + self.maxAmplitude;

第一個self.maxAmplitude表示曲線的波峰值,360.0 / _waveWidth計算出單位間距1pixel代表的度數,x * M_PI / 180表示將橫坐標值轉換為角度。self.frequency表示角速度,即單位面積內波動次數,波浪的大小。self.phase * M_PI/ 180代表上面公式中的初相,通過規律的變化初相,可以制造出曲線上的點動起來的效果,self.maxAmplitude代表偏距,由於我們需要讓波浪曲線的波峰在layer的范圍內顯示,所以需要將整個曲線向下移動波峰大小的單位,因為CALayer使用左手坐標系,所以向下移動需要加上波峰的大小。

讓波浪曲線動起來

正弦或者余弦曲線上的點,不論角度如何,在y軸上的變化范圍在它的波峰與波谷之間。拿單位正交直角坐標系來說,只要我們規律性的增加角度值,那麼曲線上的點就會在[1, -1]之間變化,我們以曲線上x=0的點為例,角度的不斷增加,會讓它的y值規律性的來回變化:

39.gif

如若曲線上的點都能這樣規律的變化,我們就能讓波浪曲線浪起來。
要讓曲線上所有的點都動起來,在這裡我們使用CADisplayLink來不斷刷新由UIBezierPath創建的形狀,兩次刷新之間曲線的變化通過增加初相來實現,代碼如下:

- (void)startLoading
{
    [_displayLink invalidate];
    self.displayLink = [CADisplayLink displayLinkWithTarget:self
                                                   selector:@selector(updateWave:)];
    [_displayLink addToRunLoop:[NSRunLoop currentRunLoop]
                       forMode:NSRunLoopCommonModes];
}

- (void)updateWave:(CADisplayLink *)displayLink
{
    self.phase += 8;//逐漸累加初相
    self.waveSinLayer.path = [self createSinPath].CGPath;
}
 
- (UIBezierPath *)createSinPath
{
    UIBezierPath *wavePath = [UIBezierPath bezierPath];
    CGFloat endX = 0;
    for (CGFloat x = 0; x < self.waveWidth + 1; x += 1) {
        endX=x;
        CGFloat y = self.maxAmplitude * sinf(360.0 / _waveWidth * (x  * M_PI / 180) * self.frequency + self.phase * M_PI/ 180) + self.maxAmplitude;
        if (x == 0) {
            [wavePath moveToPoint:CGPointMake(x, y)];
        } else {
            [wavePath addLineToPoint:CGPointMake(x, y)];
        }
    }
    
    CGFloat endY = CGRectGetHeight(self.bounds) + 10;
    [wavePath addLineToPoint:CGPointMake(endX, endY)];
    [wavePath addLineToPoint:CGPointMake(0, endY)];
    
    return wavePath;
}

把CAShapeLayer的背景色設置為淡紅色,波浪曲線會在Layer的bounds類波動,動起來的波浪曲線如下:

36.gif

構建波浪上升的镂空效果

到目前為止,我們利用CAShapeLayer、UIBezierPath以及CADisplayLink實現了動起來的波浪效果,下面我們需要實現的是在du的輪廓裡,水波不斷上升填充的過程,整個動畫過程中,會呈現一個du的镂空效果,在它輪廓之外的水波則不會顯示,這樣的效果需要借助CALayer的mask屬性來實現。

我們需要的動畫素材如下:

8f7a6fe0gw1f6lb55xrpgj20bm04iaa5.jpg

將這三個圖片位置設置為一樣,底層放置動畫中一直顯示的底層輪廓,中間層用以實現余弦波浪,最外層用於實現正弦波浪,將中間層和最外層圖片的mask屬性賦值為我們創建的用來呈現余弦波浪和正弦波浪的CAShapeLayer,這樣動畫開始後,利用CADisplayLink來不斷刷新兩個CAShapeLayer的path來讓波浪浪起來,再利用CABasicAnimation來對兩個CAShapeLayer的position進行動畫,實現從下到上的填充效果。我們想要的效果就完成了:

8f7a6fe0gw1f6lb4ym7ovg207002z3ym.gif

完整的代碼示例在這裡

參考

  • UIBezierPath

  • CAShapeLayer

  • 圖層蒙版

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved