天天看點

iOS CoreAnimation專題——技巧篇(二)CAShapeLayer with Bezier Path - Layer世界的神奇畫筆前言所有的CALayer子類CAShapeLayer貝塞爾曲線UIBezierPathCAShapeLayer的可動畫屬性總結

  • 前言
  • 所有的CALayer子類
  • CAShapeLayer
    • 矢量圖簡介
      • 矢量圖
    • 建構CAShapeLayer
  • 貝塞爾曲線
    • 貝塞爾曲線簡介
    • 線性貝塞爾曲線
    • 二階貝塞爾曲線
    • 三階貝塞爾曲線
    • 一般化
    • 控制點
  • UIBezierPath
    • 直接構造
    • 疊代構造
    • 函數圖像構造
    • 任意階貝塞爾曲線
  • CAShapeLayer的可動畫屬性
    • strokeStart
    • strokeEnd
    • path
  • 總結

前言

CALayer是CoreAnimation架構中的核心類,動畫是基于繪圖的,連圖都繪不了還動個毛的畫!而CALayer就是來解決繪圖問題的。

CoreAnimation架構為我們實作了許多CALayer的子類,它們用來解決特定的問題,比如CATextLayer可以用來顯示富文本,CAGradientLayer用來繪制顔色的線性漸變效果。既然它們都是CALayer的子類,它們就擁有CALayer所有的特點:可動畫屬性、隐式動畫、transform變形等。

所有的CALayer子類

在CoreAnimation架構中的所有的CALayer的子類如下所示:

CAShapeLayer,用來根據路徑繪制矢量圖形

CATextLayer,繪制文字資訊

CATransformLayer,使用單獨的圖層建立3D圖形

CAGradientLayer,繪制線性漸變色

CAReplicatorLayer,高效地建立多個相似的圖層并施加相似的效果或動畫

CAScrollLayer,沒有互動效果的滾動圖層,沒有滾動邊界,可以任意滾動上面的圖層内容

CATiledLayer,将大圖裁剪成多個小圖以提高記憶體和性能

CAEmitterLayer,各種炫酷的粒子效果

CAEAGLLayer,用來顯示任意的OpenGL圖形

AVPlayerLayer,用來播放視訊

而我們在開發中使用頻率最高的就是CAShapeLayer,我們将結合貝塞爾曲線詳細講解其使用。其他Specialized Layer請參閱這篇翻譯的文章

CAShapeLayer

CAShapeLayer是一個通過矢量圖形而不是bitmap(位圖)來繪制的CALayer子類。你指定諸如顔色和線寬等屬性,用CGPath來定義想要繪制的圖形,最後CAShapeLayer就自動渲染出來了。當然,你也可以用Core Graphics直接向原始的CALyer的内容中繪制一個路徑,相比直下,使用CAShapeLayer有以下一些優點:

渲染快速。CAShapeLayer使用了硬體加速,繪制同一圖形會比用Core Graphics快很多。

高效使用記憶體。一個CAShapeLayer不需要像普通CALayer一樣建立一個寄宿圖形(backing image),是以無論有多大,都不會占用太多的記憶體。

不會被圖層邊界剪裁掉。一個CAShapeLayer可以在邊界之外繪制。你的圖層路徑不會像在使用Core Graphics的普通CALayer一樣被剪裁掉。

不會出現像素化。當你給CAShapeLayer做3D變換時,它不像一個有寄宿圖的普通圖層一樣變得像素化。

矢量圖簡介

在圖形世界中有兩種圖形:位圖(bitmap)和矢量圖(vector)

位圖是通過排列像素點來構造的,像素點的資訊包括顔色+透明度(ARGB),顔色通過RGB來表示,是以一個像素一共有4個資訊(透明度、R、G、B),每個資訊的取值範圍是0-255,也就是一共256個數,剛好可以用8位二進制來表示,是以每個像素點的資訊通常通過32位(4位元組)編碼來表示,這種位圖叫做32位位圖,而一些位圖沒有Alpha通道,這樣的位圖每個像素點隻有RGB資訊,隻需要24位就可以表示一個像素點的資訊。

位圖在進行變形(縮放、3D旋轉等)時會重新繪制每個像素點的資訊,是以會造成圖形的模糊。

值得一提的是,對于GPU而言,它繪制位圖的效率是相當高的,是以如果你要提高繪制效率,可以想辦法把複雜的繪制内容轉換成位圖資料,然後丢給GPU進行渲染,比如使用CoreText來繪制文字。

關于位圖,這裡不做更詳細的介紹。

矢量圖

矢量圖是通過對多個點進行布局然後按照一定規則進行連線後形成的圖形。矢量圖的資訊總共隻有兩個:點屬性和線屬性。點屬性包括點的坐标、連線順序等;線屬性包括線寬、描線顔色等。

每當矢量圖進行變形的時候,隻會把所有的點進行重新布局,然後重新按點屬性和線屬性進行連線。是以每次變形都不會影響線寬,也不會讓圖變得模糊。

如何重新布局是通過把所有點坐标轉換成矩陣資訊,然後通過矩陣乘法重新計算新的矩陣,再把矩陣轉換回點資訊。比如要對一個矢量圖進行旋轉,就先把這個矢量圖所有的點轉換成一個矩陣(x,y,0),然後乘以旋轉矩陣:

(

cosa sina 0

-sina cosa 0

0 0 1)

得到新的矩陣(x·cosa-y·sina, x·sina+y·cosa, 0)

然後把這個矩陣轉換成點坐标(x·cosa-y·sina, x·sina+y·cosa)這就是新的點了。對矢量圖所有的點進行這樣的操作後,然後重新連線,出現的新的圖形就是旋轉後的矢量圖了。

關于矩陣計算和自定義矢量圖的繪制,可以檢視我的這個git項目:

DHVectorDiagram

建構CAShapeLayer

建構一個CAShapeLayer非常簡單,對于所有CALayer的子類,它們的初始化都是一個簡單的便利構造,像這樣:

像普通的CALayer一樣,接下來你可以設定它的frame、背景顔色、寄宿圖等,當然我們的CAShapeLayer肯定不是一個普通的layer,它是用來繪制矢量圖的,通過傳遞給它的對象一個CGPathRef,CAShapeLayer就能以矢量圖的形式将這個路徑所表示的資訊繪制出來。

在讓CAShapeLayer渲染之前,我們可以先設定好線屬性,比如我們設定線寬和描線顔色:

shapeLayer.lineWidth = ;
shapeLayer.strokeColor = [UIColor redColor].CGColor;
           
stroke是描線的意思,我們後面還會接觸到strokeStart和strokeEnd等更多的描線屬性。

設定好了渲染資訊後,我們可以構造一個路徑來讓CAShapeLayer幫我們繪制出來,這裡我們先直接使用UIKit裡面的貝塞爾曲線來構造一個簡單的矩形路徑:

這裡需要注意的是,路徑的坐标是相對于shapeLayer的左上角

然後把它的CGPath屬性指派給shapeLayer:

最後把shapeLayer加到層級上來顯示:

運作一下會發現,我們的紅色方框确實是畫出來了,但是中間被填充成了黑色。這是因為CAShapeLayer的fillColor屬性預設為黑色,fillColor表示的是填充顔色,将一個CAShapeLayer的路徑的所有封閉區間填充成該顔色,如果你不想要填充的效果,你可以設定其為透明色:

貝塞爾曲線

貝塞爾曲線簡介

貝塞爾曲線于1962年,由法國工程師皮埃爾·貝塞爾(Pierre Bézier)所廣泛發表,他運用貝塞爾曲線來為汽車的主體進行設計。貝塞爾曲線最初由Paul de Casteljau于1959年運用de Casteljau算法開發,以穩定數值的方法求出貝塞爾曲線。

— 維基百科

線性貝塞爾曲線

給定點 P0、P1 ,線性貝塞爾曲線隻是一條兩點之間的直線。這條線由下公式給出:

B(t)=P0+(P1−P0)t=(1−t)P0+tP1,t∈[0,1]

且其等同于線性插值

二階貝塞爾曲線

二階貝塞爾曲線的路徑由給定點 P0、P1、P2 的函數B(t)追蹤:

B(t)=(1−t)2P0+2t(1−t)P1+t2P2,t∈[0,1]

其中 P1 又叫做控制點

TrueType字型就運用了以貝塞爾樣條組成的二階貝塞爾曲線。

三階貝塞爾曲線

P0、P1、P2、P3 四個點在平面或在三維空間中定義了三次方貝塞爾曲線。曲線起始于 P0 走向 P1 ,并從 P2 的方向來到 P3 。一般不會經過 P1 或 P2 ;這兩個點隻是在那裡提供方向資訊。 P0 和 P1 之間的間距,決定了曲線在轉而趨進 P2 之前,走向 P1 方向的“長度有多長”。

曲線的參數形式為:

B(t)=P0(1−t)3+P13t(1−t)2+P23t2(1−t)+P3t3,t∈[0,1]

現代的成象系統,如PostScript、Asymptote和Metafont,運用了以貝塞爾樣條組成的三次貝塞爾曲線,用來描繪曲線輪廓。

一般化

n階貝塞爾曲線可如下推斷。給定點 P0、P1、…、Pn ,其貝塞爾曲線即

B(t)=∑i=0n(ni)Pi(1−t)n−iti=(n0)P0(1−t)nt0+(n1)P1(1−t)n−1t1+…+(nn−1)Pn−1(1−t)1tn−1+(nn)Pn(1−t)0tn

如上公式可如下遞歸表達: 用 BP0P1…Pn 表示由點 P0、P1、…、Pn 所決定的貝塞爾曲線,則

B(t)=BP0P1…Pn(t)=(1−t)BP0P1…Pn−1(t)+tBP1P2…Pn(t)

用平常話來說,n階的貝塞爾曲線,即雙n-1階貝塞爾曲線之間的插值。

控制點

所有的 Pi 叫做貝塞爾曲線的控制點,起始點和結束點( P0、Pn )是特殊的控制點,在有些情況可以把它們和控制點分開來了解(也就是當我們說控制點的時候,不包括起始點和結束點)。

UIBezierPath

在UIKit架構中蘋果用面向對象為我們封裝了一個用來表示抽象貝塞爾曲線的類:UIBezierPath。我們可以使用它來很友善的表示一條曲線。

UIBezierPath實際上是廣義上的曲線,它可以用來構造各種各樣的曲線,比如我們之前使用過的表示一個矩形的線,接下來我們來看看它能構造哪些曲線出來。

直接構造

UIBezierPath提供了直接構造某種曲線的方法

// 構造一個空的曲線
path = [UIBezierPath bezierPath];
           
// 構造一個矩形
path = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 40, 40)];
           
// 構造一個矩形内切圓
path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, 40, 40)];
           
是以如果要快速構造一個圓形出來的話,直接用正方形的内切圓就行了。如果傳入的是一個長方形,那麼構造出來的将是一個橢圓。
// 構造一個圓角矩形
path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 40, 40) cornerRadius:3];
           
你也可以使用這種方式構造一個圓形,隻需要設定圓角半徑為正方形邊長的一半即可。
// 構造一個圓角矩形并指定哪幾個角是圓角
// 比如這裡指定左下角和右上角這兩個角變圓
path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(, , , ) byRoundingCorners:UIRectCornerBottomLeft | UIRectCornerTopRight cornerRadii:CGSizeMake(90, 100)];
           
這個方法的第三個參數傳入的是一個CGSize,它的width成員就是你要設定的圓角半徑,height有什麼用我目前還沒弄明白。值得注意的是,如果你設定的半徑大于其寬或高的一半,那麼系統會自動幫我們修正到一個不錯的效果,你們可以試一試
// 構造一段圓弧

path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(, ) radius: startAngle:M_PI_2 endAngle:M_PI clockwise:YES];
           

第一個參數center表示的是圓弧的圓心

第二個參數radius表示圓弧的半徑

第三個參數startAngle表示的是圓弧的起始點

第四個參數endAngle表示的是圓弧的終止點

第五個參數clockwise表示是否以順時針的方向連接配接起始點和終止點

注意startAngle和endAngle所代表的隻是兩個點,0則表示圓的最右邊那個點,是以如果是 π2 的話就表示圓上最下面那個點。

最終将會從起始點到終止點連一段圓弧出來,最後一個參數決定了這次連接配接是順時針的還是逆時針的。具體如圖所示

iOS CoreAnimation專題——技巧篇(二)CAShapeLayer with Bezier Path - Layer世界的神奇畫筆前言所有的CALayer子類CAShapeLayer貝塞爾曲線UIBezierPathCAShapeLayer的可動畫屬性總結
iOS CoreAnimation專題——技巧篇(二)CAShapeLayer with Bezier Path - Layer世界的神奇畫筆前言所有的CALayer子類CAShapeLayer貝塞爾曲線UIBezierPathCAShapeLayer的可動畫屬性總結

疊代構造

所有的UIBezierPath對象都能夠通過對其添加子曲線來變得更為複雜。UIBezierPath通過控制一支虛拟的畫筆來勾勒出各種你想要的形狀。

想象你手裡拿着一支用來繪制貝塞爾路徑的筆,現在你想畫出一條折線,應該怎麼畫呢?沒錯,先把筆放到一個地方,然後畫一條線,然後筆不離開繼續畫一條線。

把筆放到一個地方可以通過調用moveToPoint方法,畫一條線則調用addLineToPoint方法,比如像這樣來畫一個直角:

UIBezierPath * path = [UIBezierPath bezierPath];
// 把筆放在10,10的位置
[path moveToPoint:CGPointMake(, )];
// 将筆移動到100,10的位置,路過的地方将會留下一條路徑
[path addLineToPoint:CGPointMake(, )];
// 筆現在已經在100,10的位置了,然後再畫一條線到100,100
[path addLineToPoint:CGPointMake(, )];

shapeLayer.path = path.CGPath;
           

這樣畫的效果是“一橫一豎”,像個“7”。注意我們在畫的過程中并沒有再次調用moveToPoint,一旦調用了moveToPoint就相當于目前繪制點移動到了這個方法的參數指定的點。

任何貝塞爾曲線都可以随時添加各種子路徑

比如你用直接構造法畫了一個圓,然後想在裡面再畫一條橫線,你可以這樣做:

// 直接構造一個圓出來
UIBezierPath * path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(, , , )];

// 畫一條橫線
[path moveToPoint:CGPointMake(, )];
[path addLineToPoint:CGPointMake(, )];

shapeLayer.path = path.CGPath;
           
iOS CoreAnimation專題——技巧篇(二)CAShapeLayer with Bezier Path - Layer世界的神奇畫筆前言所有的CALayer子類CAShapeLayer貝塞爾曲線UIBezierPathCAShapeLayer的可動畫屬性總結

除了使用move和add方法來添加新的路徑外,還可以使用appendPath方法來拼接子路徑。上面的效果還可以這樣來實作:

// 直接構造一個圓出來
UIBezierPath * path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(, , , )];

// 構造一個子路徑
UIBezierPath * subpath = [UIBezierPath bezierPath];

// 畫一條橫線
[subpath moveToPoint:CGPointMake(, )];
[subpath addLineToPoint:CGPointMake(, )];

// 拼接路徑
// 把subpath拼接到path上
[path appendPath:subpath];

shapeLayer.path = path.CGPath;
           

除了可以使用addLineToPoint來在目前路徑上添加直線外,還可以添加曲線。

// 添加一段圓弧

// 構造一個空的路徑
UIBezierPath * path = [UIBezierPath bezierPath];
// 添加一段圓弧
// 注意我們沒有調用moveToPoint,這樣我們的筆就直接從圓弧的起始點畫到結束點
// 你們可以試試看在下面這行代碼之前調用moveToPoint會發生什麼事情
[path addArcWithCenter:CGPointMake(, ) radius: startAngle: endAngle:M_PI clockwise:YES];
// 現在我們的筆處在endAngle所代表的點(簡單計算一下,圓心200,200,半徑100,endAngle是π,那麼結束點就是100,200),如果我們繼續添加直線的話,就會直接從結束點開始畫
[path addLineToPoint:CGPointMake(, )];
           
iOS CoreAnimation專題——技巧篇(二)CAShapeLayer with Bezier Path - Layer世界的神奇畫筆前言所有的CALayer子類CAShapeLayer貝塞爾曲線UIBezierPathCAShapeLayer的可動畫屬性總結

我們還可以添加正統的貝塞爾曲線

UIBezierPath * path = [UIBezierPath bezierPath];
// 将筆置于40,40
[path moveToPoint:CGPointMake(, )];
// 從40,40到300,200畫一條貝塞爾曲線,其控制點為120,360,也就是說P0是40,40,P1是120,360,P3是300,200
[path addQuadCurveToPoint:CGPointMake(, ) controlPoint:CGPointMake(, )];
           
iOS CoreAnimation專題——技巧篇(二)CAShapeLayer with Bezier Path - Layer世界的神奇畫筆前言所有的CALayer子類CAShapeLayer貝塞爾曲線UIBezierPathCAShapeLayer的可動畫屬性總結

一個控制點的貝塞爾曲線是二階貝塞爾曲線,系統還提供了三階貝塞爾曲線的實作:

UIBezierPath * path = [UIBezierPath bezierPath];

[path moveToPoint:CGPointMake(40, 40)];

[path addCurveToPoint:CGPointMake(350, 600) controlPoint1:CGPointMake(10, 220) controlPoint2:CGPointMake(380, 380)];
           
iOS CoreAnimation專題——技巧篇(二)CAShapeLayer with Bezier Path - Layer世界的神奇畫筆前言所有的CALayer子類CAShapeLayer貝塞爾曲線UIBezierPathCAShapeLayer的可動畫屬性總結

這就是系統提供了所有構造UIBezierPath的方法了。

函數圖像構造

現在我們想要畫一條sin曲線(正弦曲線),應該怎麼畫呢?這裡就要發揮我們自己的聰明才智了。

在數學上我們的函數圖像都是一系列滿足函數表達式的連續的點,而計算機是沒法處理“連續”的(比如數字音頻沒法處理模拟信号,隻能用采樣的方式以數字信号的形式進行離散處理),是以我們可以使用上一章我們逐幀繪制動畫的方法,通過“足夠近的離散的點”來模拟一條連續的曲線。

我們考慮任何一個函數 y = f(x),要怎樣畫出它的圖像呢?我們按照離散的思想,肯定是每隔一個足夠短的距離取一個點,然後把這些點全部拼接到一起就行了。

好現在我們至少有實作的思路了,就拿y = f(x) = sinx開刀吧。

- (void)viewDidLoad {
    [super viewDidLoad];

    // 使用一個shapeLayer來顯示函數圖像
    CAShapeLayer * shapeLayer = [CAShapeLayer layer];
    shapeLayer.strokeColor = [UIColor redColor].CGColor;
    shapeLayer.lineWidth = ;
    shapeLayer.fillColor = [UIColor clearColor].CGColor;

    // 構造函數圖像

    CGFloat width = CGRectGetWidth(self.view.bounds);
    CGFloat height = CGRectGetHeight(self.view.bounds);

    // 先構造一個空的路徑
    UIBezierPath * path = [UIBezierPath bezierPath];
    // 第一個點需要moveToPoint,是以放到for循環之前來
    // 當x=0的時候sinx=0
    [path moveToPoint:CGPointMake(, )];

    for (int i = ; i < width; i++) {

        CGPoint point = CGPointMake(i, sin(i));

        [path addLineToPoint:point];

    }

    shapeLayer.path = path.CGPath;
    [self.view.layer addSublayer:shapeLayer];

}
           

看起來似乎是沒有問題的,運作看一下效果吧

iOS CoreAnimation專題——技巧篇(二)CAShapeLayer with Bezier Path - Layer世界的神奇畫筆前言所有的CALayer子類CAShapeLayer貝塞爾曲線UIBezierPathCAShapeLayer的可動畫屬性總結

嗨呀,這個波浪線真是最騷的。上面的代碼我們犯了兩個錯誤:

UIKit的坐标系y軸正方向向下,而正規的用來畫函數圖像的直角坐标系y軸正方向是向上的

y = sin(x)的值域是[-1,1],周期是2π,如果我們直接使用這樣的值域和周期在畫路徑,那麼這裡的[-1,1]就是像素大小,整個圖像的高度畫出來就倆像素的高度。

第一個問題可以通過用參考系高度減去函數計算出來的y來得到最終要畫到螢幕上面的y,這裡的參考系是螢幕,是以我們最終畫到螢幕上的y’ = height - y。

第二個問題可以通過對函數圖像進行變形操作(拉伸和平移),現在我們想把值域變為[0,height],周期變為100,應該怎樣操作呢?

值域:先把y值變為原來的height/2倍,這樣值域就變成了[-height/2, height/2],然後再加上height/2,值域就變成了[0,height],這樣操作的結果就相當于函數圖像垂直方向拉伸了height/2倍并且向上平移了height/2的高度

周期:相當于函數圖像水準方向拉伸 100/(2π)倍,那麼傳進函數表達式的x就應該變為原來的2π/100倍,也就是說我們應該使用sin(2πx/100)來代替sin(x)作為函數表達式。

是以我們将上面構造路徑的代碼修改為:

UIBezierPath * path = [UIBezierPath bezierPath];
    // 第一個點需要moveToPoint,是以放到for循環之前來
    // 根據新的函數圖像,當x=0的時候f(x)=height/2
    [path moveToPoint:CGPointMake(, height/)];

    for (int i = ; i < width; i++) {

        // 對sinx圖像進行變形
        CGFloat y = height/ * sin( * M_PI * i / ) + height/;
        // 解決坐标軸方向相反的問題
        CGPoint point = CGPointMake(i, height - y);

        [path addLineToPoint:point];

    }
           
iOS CoreAnimation專題——技巧篇(二)CAShapeLayer with Bezier Path - Layer世界的神奇畫筆前言所有的CALayer子類CAShapeLayer貝塞爾曲線UIBezierPathCAShapeLayer的可動畫屬性總結
總結一下,要繪制一般函數圖像,就是在一般函數表達式注意上面的兩個問題:坐标系轉換和圖像變形。坐标系轉換通過參考系高度減去函數表達式算出來的值來得到繪圖的y值,注意要把這個操作放在圖像變形計算之後;圖像變形是中學數學的内容,對于函數y = f(x),若要對其圖像垂直方向拉伸n倍,向上平移a,水準方向拉伸m倍,向右平移b,則新的表達式為 y = nf((x-b)/m)+a,其中m和n若小于1則圖像會被壓縮,a和b若小于0則向負方向平移。

任意階貝塞爾曲線

在我們實作UIBezierPath的時候大家可能已經注意到了,系統提供的貝塞爾曲線最多隻有三階貝塞爾曲線(兩個控制點),如果要實作任意階貝塞爾曲線怎麼辦呢?答案顯而易見:用貝塞爾曲線的構造函數表達式一個點一個點的自己構造:

B(t)=∑i=0n(ni)Pi(1−t)n−iti=(n0)P0(1−t)nt0+(n1)P1(1−t)n−1t1+…+(nn−1)Pn−1(1−t)1tn−1+(nn)Pn(1−t)0tn

其中 (ni) 表示從n當中選出i個,也就是排列組合中的組合。

我們可以這樣來實作這個函數:

// 組合
- (CGFloat)choose:(CGFloat)t in:(CGFloat)n
{
    if (t == ) {
        return ;
    }
    if (t == ) {
        return n;
    }
    if (n == t) {
        return ;
    }
    CGFloat x = f ;
    CGFloat y = f ;
    for (int i = n; i > n-t; i--) {
        x = x * i;
    }

    for (int i = t; i > ; i--) {
        y = y * i;
    }
    return x/y;
}
           

貝塞爾曲線是一個關于t的函數B(t),根據公式我們可以在代碼中實作這個函數關于t的表達式:

- (CGPoint)bezierPointMakeWithT:(CGFloat)t
{
    CGPoint bezierPoint = CGPointZero;

    NSInteger rank = [self.controlPoints count]+;

    //  http://en.wikipedia.org/wiki/Bezier_curve#Generalization
    bezierPoint.x = [self choose: in:rank] * (self.startPoint.x * pow((-t), rank)*pow(t, ));
    bezierPoint.y = [self choose: in:rank] * (self.startPoint.y * pow((-t), rank)*pow(t, ));

    for (int i = ; i < rank; i++) {

        CGPoint p = [[self.controlPoints objectAtIndex:i-] CGPointValue];

        bezierPoint.x = bezierPoint.x + [self choose:i in:rank] * (p.x * pow((-t), rank-i)*pow(t, i));
        bezierPoint.y = bezierPoint.y + [self choose:i in:rank] * (p.y * pow((-t), rank-i)*pow(t, i));
    }

    bezierPoint.x = bezierPoint.x + [self choose:rank in:rank] * (self.endPoint.x * pow((-t), )*pow(t, rank));
    bezierPoint.y = bezierPoint.y + [self choose:rank in:rank] * (self.endPoint.y * pow((-t), )*pow(t, rank));

    return bezierPoint;
}
           

每一個t的值代表貝塞爾曲線上一個點的坐标,而t的取值範圍是[0,1],是以我們可以使用一個for循環來構造一條貝塞爾曲線:

- (void)update
{
    [_bezierPath removeAllPoints];
    [_bezierPath moveToPoint:self.startPoint];
    if (self.controlPoints.count >= ) {
        for (float ti = ; ti <= ; ti += ) {
            CGPoint p = [self bezierPointMakeWithT:ti];
            [_bezierPath addLineToPoint:CGPointMake(p.x, p.y)];
        }
    }
}
           

封裝起來以後就成了這個樣子:

// DHBezierCurve.h
@interface DHBezierCurve : NSObject

- (id)initWithStartPoint:(CGPoint)start endPoint:(CGPoint)end controlPoints:(NSArray <NSValue *>*)points;

// return bezier path
- (UIBezierPath *)bezierPath;

@end
           
// DHBezierCurve.m

#import "DHBezierCurve.h"

@interface DHBezierCurve ()
{
    UIBezierPath * _bezierPath;
}

@property (nonatomic,strong) NSMutableArray * controlPoints;
@property (nonatomic,assign) CGPoint startPoint;
@property (nonatomic,assign) CGPoint endPoint;

@end


@implementation DHBezierCurve

- (id)initWithStartPoint:(CGPoint)start endPoint:(CGPoint)end controlPoints:(NSArray <NSValue *>*)points
{
    self = [super init];

    self.startPoint = CGPointMake(start.x, start.y);
    self.endPoint = CGPointMake(end.x, end.y);
    self.controlPoints = [NSMutableArray arrayWithArray:points];
    _bezierPath = [UIBezierPath bezierPath];

    [self update];

    return self;
}

- (void)update
{
    [_bezierPath removeAllPoints];
    [_bezierPath moveToPoint:self.startPoint];
    if (self.controlPoints.count >= ) {
        for (float ti = ; ti <= ; ti += ) {
            CGPoint p = [self bezierPointMakeWithT:ti];
            [_bezierPath addLineToPoint:CGPointMake(p.x, p.y)];
        }
    }
}

- (UIBezierPath *)bezierPath
{

    return _bezierPath;
}

#pragma mark - private

// used in - update
- (CGPoint)bezierPointMakeWithT:(CGFloat)t
{
    CGPoint bezierPoint = CGPointZero;

    NSInteger rank = [self.controlPoints count]+;

    //  http://en.wikipedia.org/wiki/Bezier_curve#Generalization
    bezierPoint.x = [self choose: in:rank] * (self.startPoint.x * pow((-t), rank)*pow(t, ));
    bezierPoint.y = [self choose: in:rank] * (self.startPoint.y * pow((-t), rank)*pow(t, ));

    for (int i = ; i < rank; i++) {

        CGPoint p = [[self.controlPoints objectAtIndex:i-] CGPointValue];

        bezierPoint.x = bezierPoint.x + [self choose:i in:rank] * (p.x * pow((-t), rank-i)*pow(t, i));
        bezierPoint.y = bezierPoint.y + [self choose:i in:rank] * (p.y * pow((-t), rank-i)*pow(t, i));
    }

    bezierPoint.x = bezierPoint.x + [self choose:rank in:rank] * (self.endPoint.x * pow((-t), )*pow(t, rank));
    bezierPoint.y = bezierPoint.y + [self choose:rank in:rank] * (self.endPoint.y * pow((-t), )*pow(t, rank));

    return bezierPoint;
}

- (CGFloat)choose:(CGFloat)t in:(CGFloat)n
{
    if (t == ) {
        return ;
    }
    if (t == ) {
        return n;
    }
    if (n == t) {
        return ;
    }
    CGFloat x = f ;
    CGFloat y = f ;
    for (int i = n; i > n-t; i--) {
        x = x * i;
    }

    for (int i = t; i > ; i--) {
        y = y * i;
    }
    return x/y;
}

@end
           

當然你也可以考慮使用UIBezierPath的Category進行封裝

CAShapeLayer的可動畫屬性

作為CALayer大家族中的一員,CAShapeLayer擁有許多它自己的可動畫屬性,我們來幾個比較關鍵的屬性,剩下的屬性大家可以點進CAShapeLayer的類聲明裡面進行檢視。

strokeStart

strokeStart是一個被标記為Animatable的屬性,它表示描線開始的地方占總路徑的百分比,預設值是0,取值範圍[0,1]。

比如你從(0,0)點畫了一條直線到(100,0),(moveToPoint:(0,0);addLineToPoint:(100,0)),那麼當strokeStart = 0.5的話,畫出來的線就相當于從(50,0)畫到(100,0)。

注意,如果你是從(100,0)畫到了(0,0),那麼繪制開始的點是(100,0),當strokeStart = 0.5的時候,畫出來的線就相當于從(50,0)畫到(0,0)。

我們來畫一段圓弧并為strokeStart添加動畫來試一試

- (void)viewDidLoad {
    [super viewDidLoad];

    // 構造一個圓弧路徑,從圓的底部順時針畫到圓的右部(/圓)

    CAShapeLayer * shapeLayer = [CAShapeLayer layer];
    shapeLayer.strokeColor = [UIColor redColor].CGColor;
    shapeLayer.lineWidth = ;
    shapeLayer.fillColor = [UIColor clearColor].CGColor;
    [self.view.layer addSublayer:shapeLayer];

    UIBezierPath * path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(, ) radius: startAngle:M_PI_2 endAngle: clockwise:YES];

    shapeLayer.path = path.CGPath;

    // 為strokeStart添加動畫

    CABasicAnimation * animation = [CABasicAnimation animation];
    animation.keyPath = @"strokeStart";
    animation.duration = ;
    animation.fromValue = @0;

    // 直接修改modelLayer的屬性來代替toValue,見原理篇第四篇
    // 這樣shapeLayer的strokeStart屬性就會在秒内從變到,可以觀察動畫的過程和你自己想象的是否一緻

    // 添加一個延遲這樣看得更明白些
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)( * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        shapeLayer.strokeStart = ;
        [shapeLayer addAnimation:animation forKey:nil];
    });
}
           
iOS CoreAnimation專題——技巧篇(二)CAShapeLayer with Bezier Path - Layer世界的神奇畫筆前言所有的CALayer子類CAShapeLayer貝塞爾曲線UIBezierPathCAShapeLayer的可動畫屬性總結

strokeEnd

類似于strokeStart,隻不過它代表了繪制結束的地方站總路徑的百分比,預設值是1,取值範圍是[0,1]。如果小于等于strokeStart,則繪制不出任何内容。你們可以把它和strokeStart聯系起來對比認識。

我們把上面的動畫代碼中的keyPath改為@”strokeEnd”然後删掉shapeLayer.strokeStart = 1;這一行。再運作看看

iOS CoreAnimation專題——技巧篇(二)CAShapeLayer with Bezier Path - Layer世界的神奇畫筆前言所有的CALayer子類CAShapeLayer貝塞爾曲線UIBezierPathCAShapeLayer的可動畫屬性總結

path

有意思的是,path這個屬性也被标記為了Animatable。可動畫的路徑,可能會比較難以想象是怎樣的效果,我們用一個例子來進行說明。

如果我們要實作這樣的一個動畫:

iOS CoreAnimation專題——技巧篇(二)CAShapeLayer with Bezier Path - Layer世界的神奇畫筆前言所有的CALayer子類CAShapeLayer貝塞爾曲線UIBezierPathCAShapeLayer的可動畫屬性總結

實際上就是我們用一個填充顔色為橙色的shapeLayer将它的路徑按如下做變化:

iOS CoreAnimation專題——技巧篇(二)CAShapeLayer with Bezier Path - Layer世界的神奇畫筆前言所有的CALayer子類CAShapeLayer貝塞爾曲線UIBezierPathCAShapeLayer的可動畫屬性總結

是以我們隻需要一個CABasicAnimation,from左邊的路徑to右邊的路徑,CABasicAnimation就自動幫我們插值計算出中間的每幀的路徑并動畫顯示出來了。

- (void)viewDidLoad {
    [super viewDidLoad];

    CAShapeLayer * shapeLayer = [CAShapeLayer layer];
    shapeLayer.fillColor = [UIColor orangeColor].CGColor;
    [self.view.layer addSublayer:shapeLayer];

    // 構造fromPath
    UIBezierPath * fromPath = [UIBezierPath bezierPath];
    // 從左上角開始畫
    [fromPath moveToPoint:CGPointZero];

    // 因為我的模拟器是6plus,是以螢幕寬度是414

    // 向下拉一條直線
    [fromPath addLineToPoint:CGPointMake(, )];
    // 向右拉一條曲線,因為是向下彎的并且是從中間開始彎的,是以控制點的x是寬度的一半,y比起始點和結束點的y要大
    [fromPath addQuadCurveToPoint:CGPointMake(, ) controlPoint:CGPointMake(, )];

    // 向上拉一條直線
    [fromPath addLineToPoint:CGPointMake(, )];
    // 封閉路徑,會從目前點向整個路徑的起始點連一條線
    [fromPath closePath];

    shapeLayer.path = fromPath.CGPath;

    // 構造toPath
    UIBezierPath * toPath = [UIBezierPath bezierPath];

    // 同樣從左上角開始畫
    [toPath moveToPoint:CGPointZero];
    // 向下拉一條線,要拉到螢幕外
    [toPath addLineToPoint:CGPointMake(, )];
    // 向右拉一條曲線,同樣因為彎的地方在正中間并且是向上彎,是以控制點的x是寬的一半,y比起始點和結束點的y要小
    [toPath addQuadCurveToPoint:CGPointMake(, ) controlPoint:CGPointMake(, )];
    // 再向上拉一條線
    [toPath addLineToPoint:CGPointMake(, )];
    // 封閉路徑
    [toPath closePath];

    // 構造動畫
    CABasicAnimation * animation = [CABasicAnimation animation];
    animation.keyPath = @"path";
    animation.duration = ;

    // fromValue應該是一個CGPathRef(因為path屬性就是一個CGPathRef),它是一個結構體指針,使用橋接把結構體指針轉換成OC的對象類型
    animation.fromValue = (__bridge id)fromPath.CGPath;

    // 同樣添加一個延遲來友善我們檢視效果
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)( * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    // 直接修改modelLayer的值來代替toValue
        shapeLayer.path = toPath.CGPath;
        [shapeLayer addAnimation:animation forKey:nil];
    });
}
           

運作看一下,怎麼樣,CABasicAnimation就是這麼不講道理。

總結

我們這一章中的内容比較多,首先我們介紹了CALayer的各種子類,然後講解了如何簡單的構造一個CAShapeLayer,接下來我們花了大量的時間來介紹貝塞爾曲線,包括數學推導,這樣我們就能自己實作任意階的貝塞爾曲線了。最後我們看了一下CAShapeLayer的可動畫屬性,使用這些可動畫屬性能夠實作很多很多的效果。

繼續閱讀