Core Animation

參考文檔:https://pan.baidu.com/s/1HaQRu8c8bNfKTSbxF5__Sw
相同內(nèi)容網(wǎng)頁版:https://www.kancloud.cn/manual/ios/97798

Core Animation不單是用來做動(dòng)畫的,實(shí)際上它是從Layer Kit這么一個(gè)不怎么和動(dòng)畫有關(guān)的名字演變而來,所以做動(dòng)畫只是Core Animation特性的冰山一角。
Core Animation是一個(gè)復(fù)合引擎,它的職責(zé)就是盡可能快地組合屏幕上不同的可視內(nèi)容,這個(gè)內(nèi)容是被分解成獨(dú)立的圖層,存儲在一個(gè)叫做視圖樹的體系之中。于是這個(gè)樹形成了UIKit以及在iOS應(yīng)用程序當(dāng)中你所能在屏幕上看見的一切的基礎(chǔ)。

CALayer

CALayer類在概念上和UIVIew類似,同樣是一些被層級關(guān)系樹管理的矩形塊,同樣也可以包含一些內(nèi)容(例如圖片、文本或者背景色),管理子圖層的位置。它們有一些方法和屬性用來做動(dòng)畫和變換。和UIView最大的區(qū)別是CALayer不能處理用戶交互。

平行的層級關(guān)系

每一個(gè)UIView都有一個(gè)CALayer示例的圖層屬性,也就是所謂的backing layer,視圖的職責(zé)就是創(chuàng)建并管理這個(gè)圖層,以確保當(dāng)子視圖在層級關(guān)系中添加或者被移除的時(shí)候,它們關(guān)聯(lián)的圖層也同樣對應(yīng)在層級關(guān)系樹中有相同的操作。
實(shí)際上這些背后關(guān)聯(lián)的圖層才是真正用來在屏幕上顯示和做動(dòng)畫,UIView僅僅是對它的一個(gè)封裝,提供了一些iOS類似于處理觸摸的具體功能,以及Core Animation底層方法的高級接口。
之所以iOS提供兩個(gè)平行的層級關(guān)系,在于要做職責(zé)分離,這樣也能避免很多重復(fù)的代碼。實(shí)際上這里不是兩個(gè)層級關(guān)系,而是四個(gè),視圖層級、圖層樹、呈現(xiàn)樹和渲染樹。

圖層的能力

UIView沒有暴露出來的CALayer的功能:

  • 陰影、圓角、帶顏色的邊框
  • 3D變換
  • 非矩形范圍
  • 透明遮罩
  • 多級非線性動(dòng)畫
CALayer的contents屬性

這個(gè)屬性被定義為id,意味著它可以是任何類型的對象,在這種情況下,你可以給contents屬性賦任何值,你的app仍然能夠編譯通過。但是,在實(shí)踐中,如果你給contents賦的不是CGImage,那么你得到的圖層將是空白。
實(shí)際上,真正賦值給contents的類型應(yīng)該是CGImageRef,它是一個(gè)指向CGImage結(jié)構(gòu)的指針。UIImage有一個(gè)CGImage屬性,它返回一個(gè)“CGImage”,如果你想把這個(gè)值直接賦值給CALayer的contents,那你將會得到一個(gè)編譯錯(cuò)誤。因?yàn)镃GImageRef并不是一個(gè)真正的Cocoa對象,而是一個(gè)Core Foundation類型。所以在將UIImage的CGImage屬性值賦值給contents屬性的時(shí)候要通過bridged關(guān)鍵字轉(zhuǎn)換

layer.contents = (_bridge id)image.CGImage;
CALayer的contentsGravity屬性

UIView大多數(shù)視覺相關(guān)的屬性比如contentMode,對這些屬性的操作其實(shí)是對對應(yīng)圖層的操作。CALayer與contentMode對應(yīng)的屬性叫做contentsGravity,它是一個(gè)NSString類型。contentsGravity可選的常量值有以下一些:

  • contentsGravity
  • kCAGravityTop
  • kCAGravityBottom
  • kCAGravityLeft
  • kCAGravityRight
  • kCAGravityTopLeft
  • kCAGravityTopRight
  • kCAGravityBottomLeft
  • kCAGravityBottomRight
  • kCAGravityResize
  • kCAGravityResizeAspect
  • kCAGravityResizeAspectFill
CALayer的contentsScale屬性

contentsScale屬性定義了寄宿圖的像素尺寸和視圖大小的比例,默認(rèn)情況下它是一個(gè)值為1.0的浮點(diǎn)數(shù)。該值越大,顯示的寄宿圖的視覺效果越小。contentsScale的目的并不那么明顯,它并不是總會對屏幕上的寄宿圖有影響。例如,如果layer.contentsGravity = kCAGravityResizeAspect;則contentsScale屬性不起效果。如果layer.contentsGravity = kCAGravityCenter;則contentsScale對寄宿圖的顯示有影響。
contentsScale屬性其實(shí)屬于支持高分辨率(又稱Hi-DPI或者Retina)屏幕機(jī)制的一部分。它用來判斷在繪制圖層的時(shí)候應(yīng)該為寄宿圖創(chuàng)建的空間大小,和需要顯示的圖片的拉伸度(假設(shè)并沒有設(shè)置contentsGravity屬性)。UIView有一個(gè)類似的功能,但非常少用到的contentScaleFactor屬性。
如果contentScale設(shè)置為1.0,將會以每個(gè)點(diǎn)1個(gè)像素繪制圖片,如果設(shè)置為2.0,則會以每個(gè)點(diǎn)2個(gè)像素繪制圖片,這就是我們熟知的Retina屏。
layer.contentsScale = image.scale;如果如此設(shè)置,則會根據(jù)圖片是1倍圖、2倍圖或者3倍圖進(jìn)行相應(yīng)繪制。
當(dāng)用代碼的方式來處理寄宿圖的時(shí)候,需要進(jìn)行如下設(shè)置,否則圖片在Retina設(shè)備上就顯示的不正確

layer.contentsScale = [UIScreen mainScreen].scale
CALayer的maskToBounds屬性

圖層的maskToBounds相當(dāng)于UIView的clipsToBounds

CALayer的contentsRect屬性

contentsRect屬性允許我們在圖層邊框里顯示寄宿圖的一子域。和bounds、frame不同,contentRect不是按點(diǎn)來計(jì)算的,它使用單位坐標(biāo),單位坐標(biāo)指定在0~1之間,是一個(gè)相對寄宿圖尺寸的值
例如,如果設(shè)置layer.contentsRect = CGRectMake(0, 0, 0.5, 0.5);,則效果如下:

Snip20180619_1.png

CALayer的contentsCenter屬性

contentsCenter屬性只有在圖片被拉伸后才會起作用,contentsCenter可以用來定義全面拉伸的范圍。
如果contentsCenter屬性是上圖中間的藍(lán)色方框,那么當(dāng)這個(gè)圖片被拉伸后,contentsCenter屬性定義的區(qū)域會被全面拉伸(也就是從四個(gè)方向進(jìn)行放大或者縮?。贿@個(gè)方框分割后的其他方格會按照下圖所表示的進(jìn)行橫向或者縱向的拉伸,或者某些方框根本不拉伸!這就是contentsCenter屬性的意義。


Snip20180619_2.png

其相當(dāng)于xib中如下位置的配置


Snip20180619_3.png
CALayerDelegate

如果設(shè)置了代理,則當(dāng)需要被重繪的時(shí)候,CALayer會請求它的代理給它一個(gè)寄宿圖來顯示。它通過調(diào)用下面的方來來做到:

- (void)displayLayer:(CALayer *)layer;

如果過代理想要直接設(shè)置contents屬性的話,就可以在上面的代理方法中實(shí)現(xiàn)。如果代理不實(shí)現(xiàn)-displayLayer:方法,CALayer會轉(zhuǎn)而嘗試調(diào)用下面的方法:

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
Demo
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    CALayer *blueLayer = [CALayer layer];
    blueLayer.frame = CGRectMake(150, 150, 100, 100);
    blueLayer.backgroundColor = [UIColor blueColor].CGColor;
    blueLayer.delegate = self;
    //ensure that layer backing image use correct scale
    blueLayer.contentsScale = [UIScreen mainScreen].scale;
    [self.view.layer addSublayer:blueLayer];
    
    //force layer to redraw。如果不實(shí)現(xiàn)下面代碼,不會調(diào)用代理方法
    [blueLayer display];
}

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
    //draw a thick red circle
    CGContextSetLineWidth(ctx, 10.0f);
    CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
    CGContextStrokeEllipseInRect(ctx, layer.bounds);
}
上面Demo的效果圖
  • 當(dāng)UIView創(chuàng)建它的宿主圖層的時(shí)候,它就會自動(dòng)把圖層的delegate設(shè)置為它自己,并提供一個(gè)-displayLayer:的實(shí)現(xiàn)。
  • 不同于UIView,當(dāng)圖層顯示在屏幕上的時(shí)候,CALayer不會自動(dòng)重繪它的內(nèi)容,它把重繪的決定權(quán)交給來開發(fā)者
  • 盡管上面的demo沒有使用masksToBounds屬性,繪制的那個(gè)圓仍然沿邊界被裁剪了。因?yàn)橥ㄟ^CALayerDelegate繪制寄宿圖的時(shí)候,并沒有對超出邊界外的內(nèi)容提供繪制支持

UIView和CALayer布局

UIView有三個(gè)比較重要的布局屬性:fame,bounds和center,CALayer對應(yīng)地叫做frame,bounds和position。center和position都代表了相對于父圖層anchorPoint所在的位置。


Snip20180620_2.png

當(dāng)操縱視圖的frame,實(shí)際上是該表了視圖下方的CALayer的frame,不能夠獨(dú)立于圖層之外改變視圖的frame。
對于視圖或者圖層來說,frame其實(shí)是一個(gè)虛擬的屬性,是更具bounds,position和transform計(jì)算而來。所以當(dāng)其中任何一個(gè)值發(fā)生改變,frame都會變化。相反,改變frame的值同樣會影響到它們當(dāng)中的值。
當(dāng)對圖層做變換的時(shí)候,比如旋轉(zhuǎn)或者縮放,frame實(shí)際上代表覆蓋在圖層旋轉(zhuǎn)之后的整個(gè)軸對齊的矩形區(qū)域,也就是說frame的寬高可能和bounds的寬高不再一致了。


Snip20180620_3.png
錨點(diǎn)anchorPoint

圖層的anchorPoint通過position來控制它的frame的位置,可以認(rèn)為anchorPoint是用來移動(dòng)圖層的把柄。anchorPoint用單位坐標(biāo)來描述,也就是圖層的相對坐標(biāo),圖層左上角是{0,0},右下角是{1,1},默認(rèn)坐標(biāo)是{0.5,0.5}。anchorPoint可以通過指定x和y值小于0或者大于1,使得它被放置在圖層范圍之外。
當(dāng)anchorPoint改變的時(shí)候,position屬性保持不變,但是frame卻改變了,實(shí)際上就是將anchorPoint點(diǎn)移動(dòng)到原圖層的中心點(diǎn)。示例如下:


Snip20180620_4.png
Z坐標(biāo)軸

和UIView嚴(yán)格的二維坐標(biāo)系不同,CALayer存在于一個(gè)三維空間當(dāng)中。除了position和anchorPoint屬性行之外,CALayer還有另外兩個(gè)屬性zPosition和anchorPointZ,二者都是Z軸上描述圖層位置的浮點(diǎn)類型。通過增加圖層的zPosition,可以把圖層前置,于是它就在所有其他圖層的前面了(或者至少是小于它的zPosition值的圖層的前面)。zPosition并不需要增加太多,一般增加一個(gè)像素就可以把對應(yīng)的圖層前置了。

Hit Testing

CALayer并不關(guān)心任何響應(yīng)鏈?zhǔn)录?,所以不能直接處理觸摸事件或者手勢,但通過-containspoint:和-hitTest:方法可以實(shí)現(xiàn)類似效果。
-containspoint:接受一個(gè)本圖層坐標(biāo)系下的CGPoint,如果這個(gè)點(diǎn)在圖層的frame范圍內(nèi)就返回YES。所以使用的時(shí)候需要把觸摸坐標(biāo)轉(zhuǎn)換成對應(yīng)圖層坐標(biāo)系下的坐標(biāo)。
-hitTest:方法同樣接受一個(gè)CGPoint類型參數(shù),它返回圖層本身,或者包含這個(gè)坐標(biāo)點(diǎn)的葉子節(jié)點(diǎn)圖層。
注意:當(dāng)調(diào)用圖層的-hitTest:方法的時(shí)候,測算順序嚴(yán)格依賴于圖層樹當(dāng)中的圖層順序,上面提到的zPosition屬性可以改變屏幕上圖層的順序,當(dāng)不能改變事件傳遞的順序。

自動(dòng)布局

如果想要隨意控制CALayer的布局,就需要手動(dòng)操作。最簡單的方法就是使用CALayerDelegate的入校函數(shù):

- (void)layoutSublayerOfLayer:(CALayer *)layer;

當(dāng)圖層的bounds發(fā)生改變,或者圖層的-setneedsLayout方法被調(diào)用的時(shí)候,這個(gè)函數(shù)將會被執(zhí)行。這使得你可以手動(dòng)地重新擺放或者調(diào)整子圖層的大小,但是不能像UIView的autoresizingMask和constraints屬性做到自適應(yīng)屏幕旋轉(zhuǎn)。這也是為什么最好使用視圖而不是單獨(dú)的圖層來構(gòu)建應(yīng)用程序的另一個(gè)重要原因。

視覺效果

圓角

通過CALayer的cornerRadius和masksToBounds屬性就可以實(shí)現(xiàn)圓角功能

圖層邊框

通過CALayer的borderWidth和borderColor屬性就可以實(shí)現(xiàn)邊框功能

陰影

給shadowOpacity屬性一個(gè)大于默認(rèn)值(也就是0)的值,陰影就可以顯示在任意圖層之下。shadowOpacity是一個(gè)必須在0.0(不可見)和1.0(完全不透明)之間的浮點(diǎn)數(shù)。如果設(shè)置為1.0,將會顯示一個(gè)有輕微模糊的黑色陰影稍微在圖層之上。
通過shadowColor、shadowOffset和shadowRadius三個(gè)屬性可以控制陰影的表現(xiàn)。shadowColor控制陰影的顏色;shadowOffset控制陰影的方向和距離;shadowRadius控制陰影的模糊度,當(dāng)它的值是0的時(shí)候,陰影就喝視圖一樣有一個(gè)非常明確的邊界線,當(dāng)值越來越大的時(shí)候,邊界線看上去就會越來越模糊和自然。

陰影裁剪問題

如果設(shè)置masksToBounds為YES,陰影無法顯示出來,解決方法是另外用一個(gè)layer實(shí)現(xiàn)陰影。

shadowPath屬性

通過shadowPath屬性可以自定義陰影形狀,demo如下:

self.layerView1.layer.shadowOpacity = 0.5f;
    self.layerView2.layer.shadowOpacity = 0.5f;
    self.layerView1.backgroundColor = [UIColor clearColor];
    self.layerView2.backgroundColor = [UIColor clearColor];
    
    //create a square shadow
    CGMutablePathRef squarePath = CGPathCreateMutable();
    CGPathAddRect(squarePath, NULL, self.layerView1.bounds);
    self.layerView1.layer.shadowPath = squarePath;
    CGPathRelease(squarePath);
    
    //create a circular shadow
    CGMutablePathRef circlePath = CGPathCreateMutable();
    CGPathAddEllipseInRect(circlePath, NULL, self.layerView2.bounds);
    self.layerView2.layer.shadowPath = squarePath;
    CGPathRelease(squarePath);
Snip20180620_6.png

如果是矩形或者圓之類的,用CGPath會相當(dāng)簡單明了,但是如果是更加復(fù)雜的一些圖形,UIBezierPath類會更適合。

圖層蒙板

CALayer有一個(gè)屬性叫做mask,這個(gè)屬性本身就是一個(gè)CALayer類型。它類似于一個(gè)字圖層,相對于父圖層(即擁有該屬性的圖層)布局。不同于那些繪制在父圖層中的字圖層,mask圖層定義了父圖層的部分可見區(qū)域。
mask圖層的color屬性是無關(guān)緊要的,真正重要的是圖層的輪廓,mask圖層實(shí)心的部分會被保留下來,其他的則會被拋棄。


Snip20180620_7.png
Demo
@interface SecondViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imgView;

@end

@implementation SecondViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    CALayer *maskLayer = [CALayer layer];
    maskLayer.frame = self.imgView.bounds;
    UIImage *maskImage = [UIImage imageNamed:@"u6594"];
    maskLayer.contents = (__bridge id)maskImage.CGImage;
    
    self.imgView.image = [UIImage imageNamed:@"u6414"];
    self.imgView.layer.mask = maskLayer;
}

@end
Snip20180620_10.png

CALayer蒙板圖層真正厲害的地方在于蒙板圖不局限于靜態(tài)圖。任何有圖層構(gòu)成的都可以作為mask屬性,這意味著蒙板可以通過代碼甚至是動(dòng)畫實(shí)時(shí)生成。

拉伸過濾

CALayer提供三種拉伸過濾算法:

  • kCAFilterLinear
  • kCAFilterNearest
  • kCAFilterTrilinear

minificationFilter和magnificationFilter默認(rèn)的過濾器都是kCAFilterLinear,這個(gè)過濾器采用雙線性濾波算法。kCAFilterNearest優(yōu)點(diǎn)在于保留像素的差異,使用于少斜線或是曲線輪廓的圖片。kCAFilterTrilinear和kCAFilterLinear非常相似,適用于多斜線或曲線輪廓的圖片。

組透明

CALayer對應(yīng)于UIView的alpha屬性的是opacity屬性,這兩個(gè)屬性都是影響子層級的。也就是說,如果你給一個(gè)圖層設(shè)置了opacity屬性,那它的字圖層都會受此影響。
可以通過設(shè)置shouldRasterize主 sing來實(shí)現(xiàn)組透明的效果,如果它被設(shè)置為yes,在應(yīng)用透明度之前,圖層及其字圖層都會被 整合成一個(gè)整體的圖片,這樣就沒有透明度混合的問題了。
為了啟用shouldRasterize屬性,需要設(shè)置resterizationScale屬性。默認(rèn)情況下,所有圖層拉伸都是1.0,所以如果你使用了shouldRasterize屬性,需要確保resterizationScale屬性匹配屏幕,以防止出現(xiàn)Retina屏幕像素化的問題。

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    UIButton *button2 = [self customButton];
    button2.center = CGPointMake(250, 150);
    button2.alpha = 0.5;
    [self.view addSubview:button2];
    button2.layer.shouldRasterize = YES;
    button2.layer.rasterizationScale = [UIScreen mainScreen].scale;
}

- (UIButton *)customButton
{
    CGRect frame = CGRectMake(0, 0, 150, 50);
    UIButton *button = [[UIButton alloc] initWithFrame:frame];
    button.backgroundColor = [UIColor whiteColor];
    button.layer.cornerRadius = 10;
    
    frame = CGRectMake(20, 10, 110, 30);
    UILabel *label = [[UILabel alloc] initWithFrame:frame];
    label.text = @"Hello world";
    label.textAlignment = NSTextAlignmentCenter;
    [button addSubview:label];
    return button;
}

變換,CGAffineTransform、CATransform3D

UIView的transform類型是CGAffineTransform,CALayer的transform類型是CATransform3D,CALayer的affineTransform屬性對應(yīng)的是CGAffineTransform

CGAffineTransform的實(shí)質(zhì)
struct CGAffineTransform {
  CGFloat a, b, c, d;
  CGFloat tx, ty;
};
Snip20180621_1.png
混合變換
CGAffineTransformConcat(CGAffineTransform t1,
  CGAffineTransform t2)
3D變換
Snip20180625_1.png

Snip20180625_2.png
透視投影

在真實(shí)世界中,當(dāng)物體遠(yuǎn)離我們的時(shí)候,有事視覺的原因看是來會變小,理論上遠(yuǎn)離我們的視圖的邊要比靠近視角的邊更短,但實(shí)際上3D變換中并沒有發(fā)生這種情況,因?yàn)?D變換中依然保持平行。在等距離投影中,遠(yuǎn)處的物體和近處的物體保持同樣的縮放比例。
CATransform3D的透視效果通過一個(gè)矩陣中的一個(gè)很簡單的元素控制:m34 。m34的默認(rèn)值是0,我們可以通過設(shè)置m34為-1.0/d來應(yīng)用透視效果,d代表了想象中視覺相機(jī)和屏幕之間的距離,以像素為單位。

滅點(diǎn)

當(dāng)在透視角度繪圖的時(shí)候,遠(yuǎn)離相機(jī)視覺的物體將會變小變遠(yuǎn),當(dāng)遠(yuǎn)離到一個(gè)極限距離,它們可能就縮成一個(gè)點(diǎn),于是所有的物體最后都匯聚消失在同一個(gè)點(diǎn)。
在現(xiàn)實(shí)中,這個(gè)點(diǎn)通常是視圖的中心,于是為了在應(yīng)用中創(chuàng)建擬真效果的透視,這個(gè)點(diǎn)應(yīng)該聚在屏幕中心,或者至少是包含所有3D對象的視圖中心。


Snip20180626_1.png

Core Animation定義了這個(gè)點(diǎn)位于變換圖層的anchorPoint(通常位于圖層的中心,但也有例外)。這就是說,當(dāng)圖層發(fā)生變換時(shí),這個(gè)點(diǎn)永遠(yuǎn)位于圖層變換之前anchorPoint的位置。
當(dāng)改變一個(gè)圖層的position,你也改變了它的滅點(diǎn),做3D變換的時(shí)候要時(shí)刻記住這一點(diǎn),當(dāng)你視圖通過調(diào)整m34來讓它更加有3D效果,應(yīng)該首先把它放置于屏幕中央,然后通過平移來把它移動(dòng)到指定位置(而不是直接改變它的position),這樣所有的3D圖層都共享一個(gè)滅點(diǎn)。

sublayerTransform

如果有多個(gè)視圖或者圖層,每個(gè)都做3D變換,那就需要分別設(shè)置相同的m34值,并且確保在變換之前都在屏幕中央共享同一個(gè)position。
CALayer有一個(gè)屬性叫做sublayerTransform,它也是CATransform3D類型,但和對一個(gè)圖層的變換不同,它影響到所有子圖層。這意味著可以一次性對包含這些圖層的容器做變換,于是所有的字圖層都自動(dòng)繼承了這個(gè)變換方法。
相較而言,通過在一個(gè)地方設(shè)置透視變換會很方便,同時(shí)它會帶著另一個(gè)顯著的優(yōu)勢:滅點(diǎn)被設(shè)置在容器圖層的中心,從而不需要再對字圖層分別設(shè)置了,這意味著可以隨意使用position和frame來放置字圖層,而不需要把它們放置在屏幕中點(diǎn),然后為了保證統(tǒng)一的滅點(diǎn)用變換來做平移。

背面

圖層是雙面繪制的,反面顯示的是正面的一個(gè)鏡像圖片。圖層的雙面繪制會造成資源浪費(fèi),在看不見圖層背面的情況下,為什么還要浪費(fèi)GPU來繪制它們呢。
CALayer有一個(gè)叫做doubleSided的屬性來控制圖層的背面是否要繪制,這是一個(gè)Bool類型,默認(rèn)為YES,如果設(shè)置為NO,那么當(dāng)圖層正面從相機(jī)視覺消失的時(shí)候,它將不會被繪制。

扁平化

盡管Core Animation圖層存在于3D控件之內(nèi),但它們并不都是在同一個(gè)3D空間,每個(gè)圖層的3D場景其實(shí)都是扁平化的,當(dāng)你從正面觀察一個(gè)圖層,看到的實(shí)際上由子圖層創(chuàng)建的想象出來的3D場景,但當(dāng)你傾斜這個(gè)圖層,你會發(fā)現(xiàn)實(shí)際上這個(gè)3D場景僅僅是被繪制在圖層的表面。所以單純對一個(gè)視圖的layer層做3D變換并不會影響到子視圖

專用圖層

CAShapeLayer

CAShapeLayer是一個(gè)通過矢量圖形而不是bitmap來繪制的圖層子類。你指定諸如顏色和線寬燈屬性,用CGPath來定義想要繪制的圖形,最后CAShapeLayer就自動(dòng)渲染出來了。當(dāng)然,也可以用Core Graphics直接向原始的CALayer的內(nèi)容中繪制一個(gè)路徑,相比之下,使用CAShapeLayer有以下優(yōu)點(diǎn):

  • 渲染快速。CAShapeLayer使用了硬件加速,繪制同一圖形會比Core Graphics快很多
  • 高效使用內(nèi)容。一個(gè)CAShapeLayer不需要向普通CALayer一樣創(chuàng)建一個(gè)寄宿圖形,所以無論有多大,都不會占用太多的內(nèi)存。
  • 不會被圖層邊界裁剪掉。一個(gè)CAShapeLayer可以在邊界之外繪制。
  • 不會出現(xiàn)像素化。當(dāng)你給CAShapeLayer做3D變換時(shí),它不像一個(gè)有寄宿圖的普通圖層一樣變得像素化。
Demo
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)];
    
    CAShapeLayer *shapLayer = [CAShapeLayer layer];
    shapLayer.strokeColor = [UIColor whiteColor].CGColor;
    shapLayer.fillColor = [UIColor clearColor].CGColor;
    shapLayer.lineWidth = 5;
    shapLayer.lineJoin = kCALineJoinRound;
    shapLayer.lineCap = kCALineCapRound;
    shapLayer.path = path.CGPath;
    [self.outerView.layer addSublayer:shapLayer];
Snip20180627_1.png
圓角

創(chuàng)建圓角矩形,其實(shí)就是人工繪制單獨(dú)的直線和弧度,但是事實(shí)上UIBezierPath有自動(dòng)繪制圓角矩形的構(gòu)造方法。Demo如下,繪制一個(gè)有三個(gè)圓角一個(gè)直角的矩形:

CGRect rect = CGRectMake(50, 50, 100, 100);
    CGSize radii = CGSizeMake(20, 20);
    UIRectCorner corners = UIRectCornerTopRight | UIRectCornerTopLeft | UIRectCornerBottomLeft;
    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:radii];
    
    CAShapeLayer *shapLayer = [CAShapeLayer layer];
    shapLayer.strokeColor = [UIColor whiteColor].CGColor;
    shapLayer.fillColor = [UIColor clearColor].CGColor;
    shapLayer.lineWidth = 5;
    shapLayer.lineJoin = kCALineJoinRound;
    shapLayer.lineCap = kCALineCapRound;
    shapLayer.path = path.CGPath;
    [self.outerView.layer addSublayer:shapLayer];
Snip20180627_2.png

如果想依照圖形來裁剪視圖內(nèi)容,可以把CAShapeLayer作為視圖的宿主圖層,而不是添加一個(gè)字視圖。

CATextLayer

CATextLayer以圖層的形式包含來UILabel幾乎所有的繪制特性,并且額外提供了一些心特性。
CATextLayer的string屬性并不是NSString類型,而是id類型,所以既可以用NSString也可以用NSAttributeString來指定文本

Demo:
//create a text layer
    CATextLayer *textLayer = [CATextLayer layer];
    textLayer.contentsScale = [UIScreen mainScreen].scale;
    textLayer.frame = self.outerView.bounds;
    [self.outerView.layer addSublayer:textLayer];
    
    //set text attributes
    textLayer.foregroundColor = [UIColor blackColor].CGColor;
    textLayer.alignmentMode = kCAAlignmentJustified;
    textLayer.wrapped = YES;
    
    //choose a font
    UIFont *font = [UIFont systemFontOfSize:15];
    
    //set layer font
    CFStringRef fontName = (__bridge CFStringRef)font.fontName;
    CGFontRef fontRef = CGFontCreateWithFontName(fontName);
    textLayer.font = fontRef;
    textLayer.fontSize = font.pointSize;
    CGFontRelease(fontRef);
    
    //choose some text
    NSString *text = @"辣椒開始的法律監(jiān)督減肥了空間阿里的開發(fā)價(jià)值,。春夏女快放假了哦 i 將凱迪拉克你的,v來肯德基法拉第健身房";
    //set layer text
    textLayer.string = text;
Snip20180627_3.png
行距和字距

由于繪制的實(shí)現(xiàn)機(jī)制不同(Core Text和WebKit),用CATextLayer渲染和用UILabel渲染出的文本行距和字距也不盡相同。

UILabel的替代品

CATextLayer比UILabel有著更好的性能表現(xiàn),但是如果是給UILabel添加一個(gè)CATextLayer子圖層,由于UILabel仍然會使用-drawRect:方法創(chuàng)建空寄宿圖層,而且由于CALayer不支持自動(dòng)縮放和自動(dòng)布局,字圖層并不是主動(dòng)跟蹤視圖邊界的大小,所以每次視圖大小被更改,就不得不手動(dòng)更新子圖層的邊界。
每一個(gè)UIView都是寄宿在一個(gè)CALayer的示例上,這個(gè)圖層由視圖自動(dòng)創(chuàng)建和管理。繼承UIView,并重寫+layerClass方法可以返回一個(gè)不同的圖層子類。UIView會在初始化的時(shí)候調(diào)用+layerClass方法,然后用它返回類型來創(chuàng)建宿主圖層。
CATextLayer作為宿主圖層,視圖會自動(dòng)設(shè)置contentsScale屬性。

自定義Label Demo CATextLayer的frame會隨著label的frame改變
#import "LayerLabel.h"

@implementation LayerLabel

+ (Class)layerClass
{
    return [CATextLayer class];
}

- (CATextLayer *)textLayer
{
    return (CATextLayer *)self.layer;
}

- (void)setup
{
    self.text = self.text;
    self.textColor = self.textColor;
    self.font = self.font;
    [self textLayer].alignmentMode = kCAAlignmentJustified;
    [self textLayer].wrapped = YES;
    [self.layer display];
}

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        [self setup];
    }
    return self;
}

- (void)awakeFromNib
{
    [super awakeFromNib];
    [self setup];
}

- (void)setText:(NSString *)text
{
    super.text = text;
    [self textLayer].string = text;
}

- (void)setTextColor:(UIColor *)textColor
{
    super.textColor = textColor;
    [self textLayer].foregroundColor = textColor.CGColor;
}

- (void)setFont:(UIFont *)font
{
    super.font = font;
    CFStringRef fontName = (__bridge CFStringRef)font.fontName;
    CGFontRef fontRef = CGFontCreateWithFontName(fontName);
    [self textLayer].font = fontRef;
    [self textLayer].fontSize = font.pointSize;
    CGFontRelease(fontRef);
}

CATransformLayer

CATransformLayer不同于普通的CALayer,它不能顯示自己的內(nèi)容,只有存在了一個(gè)作用于子圖層的變換,它才真正存在。CATransformLayer并不平面化它的字圖層,所以它能夠用于構(gòu)造一個(gè)層級的3D結(jié)構(gòu),比如我的手臂示例。

Demo
- (void)viewDidLoad {
    [super viewDidLoad];
    [self create];
}

- (void)create
{
    CATransform3D pt = CATransform3DIdentity;
    pt.m34 = -1.0 / 500.0;
    self.testImageView.layer.sublayerTransform = pt;
    
    CATransform3D c1t = CATransform3DIdentity;
    c1t = CATransform3DTranslate(c1t, -100, 0, 0);
    CALayer *cube1 = [self cubeWithTransform:c1t];
    [self.testImageView.layer addSublayer:cube1];
    
    CATransform3D c2t = CATransform3DIdentity;
    c2t = CATransform3DTranslate(c2t, 100, 0, 0);
    c2t = CATransform3DRotate(c2t, -M_PI_4, 1, 0, 0);
    c2t = CATransform3DRotate(c2t, -M_PI_4, 0, 1, 0);
    CALayer *cube2 = [self cubeWithTransform:c2t];
    [self.testImageView.layer addSublayer:cube2];
}

- (CALayer *)faceWithTransform:(CATransform3D)transform {
    CALayer *face = [CALayer layer];
    face.frame = CGRectMake(-50, -50, 100, 100);
    
    CGFloat red = (rand()/(double)INT_MAX);
    CGFloat green = (rand()/(double)INT_MAX);
    CGFloat blue = (rand()/(double)INT_MAX);
    face.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1].CGColor;
    face.transform = transform;
    return face;
}

- (CALayer *)cubeWithTransform:(CATransform3D)transform {
    CATransformLayer *cube = [CATransformLayer layer];
    
    CATransform3D ct = CATransform3DMakeTranslation(0, 0, 50);
    [cube addSublayer:[self faceWithTransform:ct]];
    
    ct = CATransform3DMakeTranslation(50, 0, 0);
    ct = CATransform3DRotate(ct, M_PI_2, 0, 1, 0);
    [cube addSublayer:[self faceWithTransform:ct]];
    
    ct = CATransform3DMakeTranslation(0, -50, 0);
    ct = CATransform3DRotate(ct, M_PI_2, 1, 0, 0);
    [cube addSublayer:[self faceWithTransform:ct]];
    
    ct = CATransform3DMakeTranslation(0, 50, 0);
    ct = CATransform3DRotate(ct, M_PI_2, 1, 0, 0);
    [cube addSublayer:[self faceWithTransform:ct]];
    
    ct = CATransform3DMakeTranslation(-50, 0, 0);
    ct = CATransform3DRotate(ct, M_PI_2, 0, 1, 0);
    [cube addSublayer:[self faceWithTransform:ct]];
    
    ct = CATransform3DMakeTranslation(0, 0, -50);
    ct = CATransform3DRotate(ct, M_PI_2, 0, 1, 0);
    [cube addSublayer:[self faceWithTransform:ct]];
    
    CGSize containerSize = self.testImageView.bounds.size;
    cube.position = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
    cube.transform = transform;
    return cube;
}
同一視覺下的兩不同變換的立方體
CAGradientLayer

CAGradientLayer是用來生成兩種或多種顏色平滑漸變的。用Core Graphics復(fù)制一個(gè)CAGrandientLayer并將內(nèi)容繪制到一個(gè)普通圖層的寄宿圖也是有可能的,但是CAGradientLayer的真正好處是繪制使用了硬件加速。

Demo
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
    gradientLayer.frame = _testImageView.bounds;
    [self.testImageView.layer addSublayer:gradientLayer];
    
    gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor,(__bridge id)[UIColor yellowColor].CGColor,(__bridge id)[UIColor blueColor].CGColor];
    
    //如果不設(shè)置locations屬性,則各種顏色平均分布
    gradientLayer.locations = @[@0.0,@0.25,@0.5];

    //startPoint 和 endPoint是單位坐標(biāo)
    gradientLayer.startPoint = CGPointMake(0, 0);
    gradientLayer.endPoint = CGPointMake(1, 1);
Snip20180628_4.png

CAReplicatorLayer

CAReplicatorLayer的目的是為了高效生成許多相似的圖層,它會繪制一個(gè)多個(gè)圖層的字圖層,沒在每個(gè)復(fù)制體上應(yīng)用不同的變換。
變換時(shí)會逐步增加的,每一個(gè)實(shí)例都是相對于前一個(gè)實(shí)例布局。instanceCount屬性指定了圖層需要重復(fù)多少次,instanceTransform指定一個(gè)CATransform3D 3D變換

Demo旋轉(zhuǎn)時(shí)繞CAReplicatorLayer中心點(diǎn)旋轉(zhuǎn)
CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
    replicator.frame = self.outerView.bounds;
    [self.outerView.layer addSublayer:replicator];
    
    replicator.instanceCount = 10;
    
    //apply a transform for each instance
    CATransform3D transform = CATransform3DIdentity;
    transform = CATransform3DRotate(transform, M_PI/5.0, 0, 0, 1);
    replicator.instanceTransform = transform;
    
    //apply a color shift for each instance
    replicator.instanceBlueOffset = -0.1;
    replicator.instanceGreenOffset = -0.1;
    
    //create a sublayer and place it inside the replicator
    CALayer *layer = [CALayer layer];
    layer.frame = CGRectMake(107.5, 0, 50.0, 50.0);
    layer.backgroundColor = [UIColor greenColor].CGColor;
    
    CATextLayer *textLayer = [CATextLayer layer];
    textLayer.frame = CGRectMake(0, 0, 50, 50);
    textLayer.string = @"xy";
    [layer addSublayer:textLayer];
    
    [replicator addSublayer:layer];
反射

使用CAReplicatorLayer并應(yīng)用一個(gè)負(fù)比例變換于一個(gè)復(fù)制圖層,你就可以創(chuàng)建指定視圖內(nèi)容的景象圖片。

自定義反射view Demo
#import "ReflectionView.h"

@implementation ReflectionView

+ (Class)layerClass
{
    return [CAReplicatorLayer class];
}

- (void)setup
{
    CAReplicatorLayer *repLayer = (CAReplicatorLayer *)self.layer;
    repLayer.instanceCount = 2;
    
    CATransform3D transform = CATransform3DIdentity;
    CGFloat verticalOffset = self.bounds.size.height + 2;
    transform = CATransform3DTranslate(transform, 0, verticalOffset, 0);
    transform = CATransform3DScale(transform, 1, -1, 0);
    repLayer.instanceTransform = transform;
    
    repLayer.instanceAlphaOffset = -0.6;
}

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        [self setup];
    }
    return self;
}

- (void)awakeFromNib
{
    [super awakeFromNib];
    [self setup];
}

@end
CAScrollLayer

CAScrollLayer實(shí)現(xiàn)圖層的滾動(dòng),不過需要自定義手勢。

CATileLayer

CATileLayer可以解決載入大圖的性能問題。CATileLayer將大圖分解成小片,然后將它們大度按需載入。
為了能夠從CATiledLayer中受益,我們需要預(yù)先把圖片切成許多小一些的圖片(可以用代碼完成這件事)。如果是在運(yùn)行時(shí)讀入這個(gè)那個(gè)圖片并裁切,那CATileLayer的所有性能優(yōu)點(diǎn)就損失殆盡了。CATileLayer的默認(rèn)小圖大小是256*256.
CATileLayer可以很好的和UIScrollView結(jié)合使用。除了設(shè)置圖層和滑動(dòng)視圖以適配整個(gè)圖片大小,我們真正要做的就是實(shí)現(xiàn)-drawLayer:incontext:方法,當(dāng)需要載入新的小圖時(shí),CATileLayer就會調(diào)用這個(gè)方法。

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    CATiledLayer *tileLayer = [CATiledLayer layer];
    tileLayer.delegate = self;
    [self.scrollView.layer addSublayer:tileLayer];
    
    self.scrollView.contentSize = tileLayer.frame.size;
    [tileLayer setNeedsDisplay];
}

- (void)drawLayer:(CATiledLayer *)layer inContext:(CGContextRef)ctx
{
    //determine tile coordinate
    CGRect bounds = CGContextGetClipBoundingBox(ctx);
    NSInteger x = floor(bounds.origin.x / layer.tileSize.width);
    NSInteger y = floor(bounds.origin.y / layer.tileSize.height);
    
    //load tile image
    NSString *imageName = [NSString stringWithFormat:@"picture_%ld_%ld",x,y];
    UIImage *tileImage = [UIImage imageNamed:imageName];
    UIGraphicsPushContext(ctx);
    [tileImage drawInRect:bounds];
    UIGraphicsPopContext();
}

小圖并不是以Retina的分辨率顯示的,為了以屏幕的原生分辨率來渲染CATileLayer,我們需要設(shè)置圖層的contentScale來匹配UIScreen的scale屬性:
tileLayer.contentsScale = [UIScreen mainScreen].scale;
tileSize是以像素為單位,而不是點(diǎn),所以增大了contentsScale就自動(dòng)有了默認(rèn)的小圖尺寸(現(xiàn)在是128128的點(diǎn)而不是256256),所以不需要手工更新小圖的尺寸或者在Retina分辨率下指定一個(gè)不同的小圖,我們需要做的是適應(yīng)小圖渲染代碼以對應(yīng)安排scale的變化

//determine tile coordinate
    CGRect bounds = CGContextGetClipBoundingBox(ctx);
    CGFloat scale = [UIScreen mainScreen].scale;
    NSInteger x = floor(bounds.origin.x / layer.tileSize.width * scale);
    NSInteger y = floor(bounds.origin.y / layer.tileSize.height * scale);

通過這個(gè)方法糾正scale也意味著圖片將以原先一半的大小渲染在Retina設(shè)備上。

CAEmitterLayer

CAEmitterLayer是一個(gè)高性能的粒子引擎,被用來創(chuàng)建實(shí)時(shí)例子動(dòng)畫如:煙霧、火、雨等類似效果
CAEmitterLayer看上去像是許多CAEmitterCell的容器,這些CAEmitterCell定義了一個(gè)例子效果。我們需要為不同的例子效果定義一個(gè)或多個(gè)CAEmitterCell作為模版,同時(shí)CAEmitterLayer負(fù)責(zé)基于這些模版實(shí)例化一個(gè)粒子流。一個(gè)CAEmitterCell類似于一個(gè)CALayer:它有一個(gè)contents屬性可以定義為一個(gè)CGImage,另外還有一些可設(shè)置屬性控制著變現(xiàn)和行為。

CAEmitterLayer *emitter = [CAEmitterLayer layer];
    emitter.frame = self.outerView.bounds;
    [self.outerView.layer addSublayer:emitter];
    
    //configure emitter
    emitter.renderMode = kCAEmitterLayerAdditive;
    emitter.emitterPosition = CGPointMake(emitter.frame.size.width/2.0, CGRectGetHeight(emitter.frame)/2.0);
    
    //create a particle template
    CAEmitterCell *cell = [CAEmitterCell new];
    cell.contents = (__bridge id)([UIImage imageNamed:@"u3264"].CGImage);
    cell.birthRate = 150;
    cell.lifetime = 5.0;
    cell.color = [UIColor redColor].CGColor;
    cell.alphaSpeed = -0.4;
    cell.velocity = 50;
    cell.velocityRange = 50;
    cell.emissionRange = M_PI * 2.0;
    
    //add particle template to emitter
    emitter.emitterCells = @[cell];
實(shí)際效果是動(dòng)畫過程,小正方形不斷向外擴(kuò)散
CAEmitterCell的屬性基本上可以分為三種:
  • 這種粒子的某一屬性的初始值。比如,color屬性置頂了一個(gè)可以混合圖片內(nèi)容顏色的混合顏色
  • 粒子某一屬性的變化范圍。比如emissionRange屬性值是M_PI * 2.0,這意味著粒子可以從360度任意位置發(fā)射出去
  • 指定值在時(shí)間線上的變化。比如,上面例子中,我們將alphaSpeed設(shè)置為-0.4,就是說例子的透明度每秒就是較少0.4,這就是發(fā)射出去后逐漸消失的效果。

CAEmitterLayer的屬性它自己控制著整個(gè)例子系統(tǒng)的位置和形狀,一些屬性比如birthRate、lifetime和celocity,這些屬性在CAEmitterCell中也有。這些屬性會以相乘的方式作用在一起,這樣就可以用一個(gè)值來加速或者擴(kuò)大整個(gè)例子系統(tǒng)。其他需要注意的屬性有如下這些:

  • preservesDepth,是否將3D例子系統(tǒng)平面化到一個(gè)圖層(默認(rèn)值)或者可以在3D空間中混合其他的圖層
  • renderMode,控制著在視覺上粒子圖片是如何混合的。上面例子中kCAEmitterLayerAdditive實(shí)現(xiàn)的效果為:合并例子重疊部分的亮度使得看上去更亮。
CAEAGLLayer

OpenGL提供了Core Animation的基礎(chǔ),它是底層的C接口。iOS5中,蘋果引入了一個(gè)新的框架叫做GLKit,它去掉了一些設(shè)置OpenGL的復(fù)雜性,提供了一個(gè)叫做CLKView的UIView子類,幫你處理大部分的設(shè)置和繪制工作。前提是各種各樣的OpenGL繪圖緩沖的底層可配置項(xiàng)仍需要你用CAEAGLLayer完成,它是CALayer的一個(gè)子類,用來顯示任意的OpenGL圖形。

@interface SecondViewController ()<CALayerDelegate>

@property (nonatomic, weak) IBOutlet UIView *glView;
@property (nonatomic, strong) EAGLContext *glContext;
@property (nonatomic, strong) CAEAGLLayer *glLayer;
@property (nonatomic, assign) GLuint framebuffer;
@property (nonatomic, assign) GLuint colorRenderbuffer;
@property (nonatomic, assign) GLint framebufferWidth;
@property (nonatomic, assign) GLint framebufferHeight;
@property (nonatomic, strong) GLKBaseEffect *effect;

@end

@implementation SecondViewController

- (void)setUpBuffers
{
    //set up frame buffer
    glGenFramebuffers(1, &_framebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
    
    //set up color render buffer
    glGenRenderbuffers(1, &_colorRenderbuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorRenderbuffer);
    [self.glContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.glLayer];
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_framebufferWidth);
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_framebufferHeight);
    
    //check success
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
        NSLog(@"Failed to make complete framebuffer object: %i", glCheckFramebufferStatus(GL_FRAMEBUFFER));
    }
}

- (void)tearDownBuffers
{
    if (_framebuffer) {
        //delete framebuffer
        glDeleteFramebuffers(1, &_framebuffer);
        _framebuffer = 0;
    }
    
    if (_colorRenderbuffer) {
        //delete color render buffer
        glDeleteRenderbuffers(1, &_colorRenderbuffer);
        _colorRenderbuffer = 0;
    }
}

- (void)drawFrame {
    //bind framebuffer & set viewport
    glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
    glViewport(0, 0, _framebufferWidth, _framebufferHeight);
    
    //bind shader program
    [self.effect prepareToDraw];
    
    //clear the screen
    glClear(GL_COLOR_BUFFER_BIT); glClearColor(0.0, 0.0, 0.0, 1.0);
    
    //set up vertices
    GLfloat vertices[] = {
        -0.5f, -0.5f, -1.0f, 0.0f, 0.5f, -1.0f, 0.5f, -0.5f, -1.0f,
    };
    
    //set up colors
    GLfloat colors[] = {
        0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
    };
    
    //draw triangle
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    glEnableVertexAttribArray(GLKVertexAttribColor);
    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 0, vertices);
    glVertexAttribPointer(GLKVertexAttribColor,4, GL_FLOAT, GL_FALSE, 0, colors);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    
    //present render buffer
    glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
    [self.glContext presentRenderbuffer:GL_RENDERBUFFER];
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    //set up context
    self.glContext = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES2];
    [EAGLContext setCurrentContext:self.glContext];
    
    //set up layer
    self.glLayer = [CAEAGLLayer layer];
    self.glLayer.frame = self.glView.bounds;
    [self.glView.layer addSublayer:self.glLayer];
    self.glLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking:@NO, kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8};
    
    //set up base effect
    self.effect = [[GLKBaseEffect alloc] init];
    
    //set up buffers
    [self setUpBuffers];
    
    //draw frame
    [self drawFrame];
}

- (void)viewDidUnload
{
    [self tearDownBuffers];
    [super viewDidUnload];
}

- (void)dealloc
{
    [self tearDownBuffers];
    [EAGLContext setCurrentContext:nil];
}

@end
Snip20180629_2.png
AVPlayerLayer

AVPlayerLayer不是Core Animation框架的一部分,其是AVFoundation的一部分。AVPlayerLayer與Core Animation緊密地結(jié)合在一起,提供一個(gè)CALayer子類來顯示自定義的內(nèi)容類型。
AVPlayerLayer是用來在iOS上播放視頻的,它是高級接口如MPMoviePlayer的底層實(shí)現(xiàn),提供了顯示視頻的底層控制。

NSString *urlPath = [[NSBundle mainBundle] pathForResource:@"aaa" ofType:@"mp4"];
    AVPlayerItem *item = [AVPlayerItem playerItemWithURL:[NSURL fileURLWithPath:urlPath]];
    self.player = [AVPlayer playerWithPlayerItem:item];
    AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player];
    playerLayer.frame = CGRectMake(0, 0, CGRectGetWidth([UIScreen mainScreen].bounds), 200);
    [_bgView.layer addSublayer:playerLayer];
    [self.player play];

隱式動(dòng)畫

事務(wù)

當(dāng)改變CALayer的一個(gè)可做動(dòng)畫的屬性,它并不能立刻在屏幕上體現(xiàn)出來。相反,它是從先前的值平滑過渡到新的值,這一切都是默認(rèn)的行為。這就是所謂的隱式動(dòng)畫。
當(dāng)改變一個(gè)屬性Core Animation是如何判斷動(dòng)畫類型和持續(xù)時(shí)間的呢?實(shí)際上動(dòng)畫執(zhí)行的時(shí)間取決于當(dāng)前事務(wù)的設(shè)置,動(dòng)畫類型取決于圖層行為。
事務(wù)實(shí)際上是Core Animation用來包含一系列屬性動(dòng)畫集合的機(jī)制,任何用指定事務(wù)去改變可以做動(dòng)畫的圖層屬性都不會立刻發(fā)生變化,而是當(dāng)事務(wù)一旦提交的時(shí)候開始用一個(gè)動(dòng)畫過渡到新值。
事務(wù)是通過CATransaction類來做管理,這個(gè)類設(shè)計(jì)有些奇怪,不像你從它命名預(yù)期的那樣去管理一個(gè)簡單的事務(wù),而是管理類一疊你不能訪問的事務(wù)。CATransaction沒有屬性或者實(shí)例方法,并且也不能用+alloc和-init方法創(chuàng)建。但可以用+begin和+commit分別來入?;蛘叱鰲!?br> 任何可以做動(dòng)畫的圖層屬性都會被添加到棧頂?shù)氖聞?wù),你可以通過+setAnimationDuration:方法設(shè)置當(dāng)前事務(wù)的動(dòng)畫時(shí)間,或者通過+animationDuration方法來獲取值(默認(rèn)0.25秒)。

Demo
- (void)viewDidLoad
{
    [super viewDidLoad];
    //set up context
    
    self.colorLayer = [CALayer layer];
    self.colorLayer.frame = self.colorView.bounds;
    [self.colorView.layer addSublayer:self.colorLayer];
}

- (IBAction)buttonOnClickAction:(id)sender {
    //begin a new transaction
    [CATransaction begin];
    //set the animation duration to 1 second
    [CATransaction setAnimationDuration:1.0];
    
    CGFloat red = arc4random() /(CGFloat)INT_MAX;
    CGFloat green = arc4random() /(CGFloat)INT_MAX;
    CGFloat blue = arc4random() /(CGFloat)INT_MAX;
    self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1].CGColor;
    
    //commit the transaction
    [CATransaction commit];
}

實(shí)際上,UIView的+beginAnimations: context:和+commitAnimations之間所有視圖或者圖層屬性的改變而做的動(dòng)畫都是由于設(shè)置了CATransaction的原因。UIView的+animateWithDuration: animations:方法內(nèi)部也會自動(dòng)調(diào)用CATransaction的+begin和+commit方法

給事務(wù)設(shè)置CompletionBlock
[CATransaction begin];
    //set the animation duration to 1 second
    [CATransaction setAnimationDuration:1.0];
    //add the spin animation on completion
    [CATransaction setCompletionBlock:^{
        
    }];
    
    CGFloat red = arc4random() /(CGFloat)INT_MAX;
    CGFloat green = arc4random() /(CGFloat)INT_MAX;
    CGFloat blue = arc4random() /(CGFloat)INT_MAX;
    self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1].CGColor;
    
    //commit the transaction
    [CATransaction commit];
圖層行為

Core Animation通常對CALayer的所有屬性(可動(dòng)畫的屬性)做動(dòng)畫,但是UIView把和它關(guān)聯(lián)的圖層的這個(gè)特性關(guān)閉了。
當(dāng)CALayer的屬性被修改的時(shí)候,它會調(diào)用-actionForKey:方法,傳遞屬性的名稱。剩下的操作都是CALayer的頭文件中有詳細(xì)的說明,實(shí)質(zhì)上是如下幾步:

  • 圖層首先檢測它是否有委托,并且是否實(shí)現(xiàn)CALayerDelegate協(xié)議指定的-actionForLayer:forkey:方法。如果有,直接調(diào)用并返回結(jié)果。
  • 如果沒有委托,或者委托沒有實(shí)現(xiàn)-actionForLayer:forkey:方法,圖層接著檢查包含屬性名稱對應(yīng)映射的actions字典。
  • 如果actions字典沒有包含對應(yīng)的屬性,那么圖層接著在它的style字典接著搜索屬性名。
  • 最后,如果在style里面也找不到對應(yīng)的行為,那么圖層將會直接調(diào)用定義了每個(gè)屬性的標(biāo)準(zhǔn)行為的-defaultActionForKey:方法。
    所以一輪完整的搜索結(jié)束之后,-actionForKey:要么返回空(這種情況下將不會有動(dòng)畫發(fā)生),要么是CAAction協(xié)議對應(yīng)的對象,最后CALayer拿這個(gè)結(jié)果去和先前和當(dāng)前的值做動(dòng)畫。
    UIView對它關(guān)聯(lián)的圖層禁用隱式動(dòng)畫的實(shí)現(xiàn)方法就是:當(dāng)不在一個(gè)動(dòng)畫塊的實(shí)現(xiàn)中,-actionForKey:方法返回空,但是在動(dòng)畫block范圍之內(nèi),它就返回一個(gè)非空值。
//test layer action when outside of animation block,返回空值
    NSLog(@"Outside: %@",[self.colorView actionForLayer:self.colorView.layer forKey:@"xxx"]);
    
    //bing animation block
    [UIView beginAnimations:nil context:nil];
    //test layer action when inside of animation block,返回非空值
    NSLog(@"Inside: %@",[self.colorView actionForLayer:self.colorView.layer forKey:@"xxx"]);
    //end animation block
    [UIView commitAnimations];
呈現(xiàn)與模型

每個(gè)圖層屬性的顯示值都被存儲在一個(gè)叫做呈現(xiàn)圖層的獨(dú)立圖層當(dāng)中,它可以通過-presentationLayer方法來訪問,這個(gè)呈現(xiàn)圖層實(shí)際上是模型圖層的復(fù)制,但是它的屬性值代表了任何指定時(shí)刻當(dāng)前外觀效果。換句話說,你可以通過呈現(xiàn)圖層的值來獲取當(dāng)前屏幕上真正顯示出來的值。
前面提到除來圖層樹之外還有呈現(xiàn)樹。呈現(xiàn)樹就是通過圖層樹中所有圖層的呈現(xiàn)圖層形成的。

顯示動(dòng)畫

像所有NSObject子類一樣,CAAnimation實(shí)現(xiàn)KVC協(xié)議,于是你可以用-setValue:forKey:和-valueForKey:方法來存取屬性。但是CAAnimation有一個(gè)不同的性能,它更像一個(gè)NSDictionary,可以讓你隨意設(shè)置鍵值對,即使和你使用的動(dòng)畫類所聲明的屬性并不匹配。這意味著可以對動(dòng)畫用任意類型打標(biāo)簽,從而在代理方法中識別動(dòng)畫。

關(guān)鍵幀動(dòng)畫

CAKeyframeAnimation是另一種UIKit沒有暴露出來但功能強(qiáng)大的累,和CABasicAnimation類似,CAKeyframeAnimation同樣是CAProperyAnimation的一個(gè)子類,它依然作用于單一的一個(gè)屬性,但是和CABasicAnimation不一樣的是,它不限制于設(shè)置一個(gè)起始和結(jié)束的值,而是可以根據(jù)一連串隨意的值來做動(dòng)畫。

動(dòng)畫組CAAnimationGroup

CABasicAnimation和CAKeyframeAnimation僅僅作用于單獨(dú)的屬性,而CAAnimationGroup可以把這些動(dòng)畫組合在一起。CAAnimationGroup是另一個(gè)繼承于CAAnimation的子類,它添加了一個(gè)animations數(shù)組的屬性,用來組合別的動(dòng)畫。關(guān)鍵代碼如下:

CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];
    groupAnimation.animations = @[animation1,animation2];
    groupAnimation.duration = 4.0;
    [self.colorView.layer addAnimation:groupAnimation forKey:nil];
過渡

有時(shí)候?qū)τ趇OS應(yīng)用程序來說,希望通過屬性動(dòng)畫來對比較難做的動(dòng)畫的布局進(jìn)行一些改變,比如交換一段文本和圖片,或者用一段網(wǎng)格視圖來替換,等等。屬性動(dòng)畫只對圖層的可動(dòng)畫屬性起作用,所以如果要改變一個(gè)不能動(dòng)畫的屬性(比如圖片),或者從層級關(guān)系匯總添加或者移除圖層,屬性動(dòng)畫將不起作用。
于是就有了過渡的概念,過渡并不像屬性動(dòng)畫那樣平滑地在兩個(gè)值之間動(dòng)畫,而是影響到整個(gè)圖層的變化。過渡動(dòng)畫首先展示之前的圖層外觀,然后通過一個(gè)交換過渡到了新的外觀。
為了創(chuàng)建一個(gè)過渡動(dòng)畫,我們將使用CATransition,同樣是另一個(gè)CAAnimation的子類,和別的子類不同,CATransition有一個(gè)type和subtype來表示變換效果。type屬性是一個(gè)NSString類型,可以被設(shè)置成如下類型:

kCATransitionFade(默認(rèn)) 淡入淡出
kCATransitionMoveIn 從頂部滑動(dòng)進(jìn)入
kCATransitionPush 從邊緣的一側(cè)滑動(dòng)進(jìn)來
kCATransitionReveal 把原始的圖層滑動(dòng)出去來顯示新的外觀

后面三種過渡類型都有一個(gè)默認(rèn)的動(dòng)畫方向,它們都從左側(cè)滑入,但可以通過subtype來控制它們的方向,提供如下四種類型:

kCATransitionFromRight
kCATransitionFromLeft
kCATransitionFromTop
kCATransitionFromBottom

自定義動(dòng)畫

UIView的+transitionFromView: toView: duration: options: completion:和+transitionWithView: duration: options: animations: completion:方法提供了Core Animation的過渡特性。UIView過渡方法中options參數(shù)可以由如下常量指定:

UIViewAnimationOptionTransitionNone 默認(rèn)
UIViewAnimationOptionTransitionFlipFromLeft ,
UIViewAnimationOptionTransitionFlipFromRight ,
UIViewAnimationOptionTransitionCurlUp ,
UIViewAnimationOptionTransitionCurlDown ,
UIViewAnimationOptionTransitionCrossDissolve ,
UIViewAnimationOptionTransitionFlipFromTop ,
UIViewAnimationOptionTransitionFlipFromBottom ,

除了UIViewAnimationOptionTransitionCrossDissolve之外,剩下的值和CATransition類型完全沒關(guān)系

通過使用CALayer的-renderInContext:方法把它繪制到Core Graphics的上下文中捕獲當(dāng)前內(nèi)容的圖片,然后在另外的視圖中顯示出來。如果把這個(gè)截圖置于原始視圖之上,就可以遮住真實(shí)視圖的所有變化,于是自定義一個(gè)過渡效果。

在動(dòng)畫過程中取消動(dòng)畫

終止指定的動(dòng)畫:
- (void)removeAnimationForKey:(NSString *)key;
移除所有動(dòng)畫:
- (void)removeAllAnimations;
動(dòng)畫一旦被移除,圖層的外觀就立刻更新到當(dāng)前的模型層的值。一般來說,動(dòng)畫在結(jié)束之后被自動(dòng)移除,除非設(shè)置removedOnCompletion為NO。如果設(shè)置動(dòng)畫結(jié)束之后不被自動(dòng)移除,那么當(dāng)它不需要的時(shí)候要手動(dòng)移除它,否則它會一直存在于內(nèi)存中,直到圖層被銷毀。

圖層時(shí)間

CAMediaTiming協(xié)議

在“顯式動(dòng)畫”中提到過的duration就是CAMediaTiming的屬性之一,duration為將要進(jìn)行的動(dòng)畫的一次迭代指定了時(shí)間。CAMediaTiming另外一個(gè)屬性叫做repeatCount,代表動(dòng)畫重復(fù)的迭代次數(shù)。完整的動(dòng)畫時(shí)間是duration * repeatCount。
創(chuàng)建重復(fù)動(dòng)畫的另一種方式是使用repeatDuration屬性,它讓動(dòng)畫重復(fù)一個(gè)指定的時(shí)間,而不是指定次數(shù)。甚至可以設(shè)置autoreverses屬性,使得每次間隔交替循環(huán)過程中自動(dòng)回放。
將repeatDuration或者repeatCount設(shè)置為INFINITY可以實(shí)現(xiàn)無限循環(huán)

相對時(shí)間

關(guān)于Core Animation的時(shí)間是相對的,每個(gè)動(dòng)畫都有它自己描述的時(shí)間,可以獨(dú)立地加速、延時(shí)或者偏移。
beginTime指定了動(dòng)畫開始之前的延遲時(shí)間,這里的延遲從動(dòng)畫添加到可見圖層的那一刻開始測量,默認(rèn)是0
speed是一個(gè)時(shí)間的倍數(shù),默認(rèn)1.0,減少它會減慢圖層/動(dòng)畫的時(shí)間,增加它會加快速度。如果speed為2.0,那么對于一個(gè)duration為1的動(dòng)畫,實(shí)際上在0.5秒的時(shí)候就已經(jīng)完成了。
timeOffset是讓動(dòng)畫快進(jìn)到某一時(shí)間點(diǎn)。例如,對于一個(gè)持續(xù)1秒的動(dòng)畫來說,設(shè)置timeOffset為0.5意味著動(dòng)畫將從一半的地方開始。
和beginTime不同的是timeOffset并不受speed的影響,所以如果你把speed設(shè)置為2.0,吧timeOffset設(shè)置為0.5,那么對于duration為1的動(dòng)畫,動(dòng)畫將從動(dòng)畫最后結(jié)束的地方開始。

層級關(guān)系時(shí)間

每個(gè)動(dòng)畫和圖層在時(shí)間上都有它的層級概念,相對于它的父親來測量。對圖層調(diào)整時(shí)間將會影響到它本身和字圖層的動(dòng)畫,但不會影響到父圖層。
對CALayer或者CAGroupAnimation調(diào)整duration和repeatCount/repeatDuration屬性并不會影響到子動(dòng)畫。但是beginTime,timeOffset和speed屬性將會影響到子動(dòng)畫。

全局時(shí)間和本地時(shí)間

Core Animation有一個(gè)全局時(shí)間的概念,也就是所謂的馬赫時(shí)間。馬赫時(shí)間在設(shè)備上所有進(jìn)程都是全局的,但是在不同設(shè)備上并不是全局的。可以使用如下方法訪問馬赫時(shí)間:
CFTimeInterval time = CACurrentMediaTime();
這個(gè)函數(shù)返回的惡值其實(shí)無關(guān)緊要(它返回了設(shè)備自從上次啟動(dòng)后的秒數(shù)),它真實(shí)的作用在于對動(dòng)畫時(shí)間測量提供一個(gè)相對值。
注意:當(dāng)設(shè)備休眠的時(shí)候馬赫時(shí)間會暫停,也就是所有CAAnimations(基于馬赫時(shí)間)同樣會暫停。
每個(gè)CALayer和CAAnimation實(shí)例都有自己本地時(shí)間的概念,是根據(jù)父圖層/動(dòng)畫層級關(guān)系中的beginTime,timeOffset和speed屬性計(jì)算。CALayer提供了如下方法轉(zhuǎn)換不同圖層之間的本地時(shí)間:

- (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(nullable CALayer *)l;
- (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(nullable CALayer *)l;
暫停、倒回和快進(jìn)

給圖層添加一個(gè)CAAnimation實(shí)際上是給動(dòng)畫對象做一個(gè)不可改變的拷貝,所以對原始動(dòng)畫對象屬性的修改對真實(shí)的動(dòng)畫并沒有作用。相反,使用-animationForKey:來檢索圖層正在進(jìn)行的動(dòng)畫可以返回正確的動(dòng)畫對象,但是修改它會拋出異常。
一個(gè)簡單的方法是利用CAMediaTiming來暫停圖層本身。如果把圖層的speed設(shè)置為0,它會暫停任何添加到圖層上的動(dòng)畫,類似的,設(shè)置speed大于1.0將會快進(jìn),設(shè)置成一個(gè)負(fù)值將會倒回動(dòng)畫。

手動(dòng)動(dòng)畫

timeOffset一個(gè)很有用的功能在于它可以讓你手動(dòng)控制動(dòng)畫進(jìn)程,通過設(shè)置speed為0,可以禁用動(dòng)畫的自動(dòng)播放,讓后使用timeOffset來來回回顯示動(dòng)畫序列。

緩沖

CAMediaTimingFunction

使用緩沖方程式的方法是設(shè)置CAAnimation的timingFunction屬性,是CAMediaTimingFunction類的一個(gè)對象。如果行抗改變隱式動(dòng)畫的基石函數(shù),同樣也可以使用CATransaction的+setAnimationTimingfunction:方法。
最簡單的創(chuàng)建CAMediaTimingFunction的方式是調(diào)用+ (instancetype)functionWithName:(NSString *)name;方法,這里傳入如下幾個(gè)常量之一:

kCAMediaTimingFunctionLinear(默認(rèn))勻速
kCAMediaTimingFunctionEaseIn 慢到快
kCAMediaTimingFunctionEaseOut 快到慢
kCAMediaTimingFunctionEaseInEaseOut 慢快慢
kCAMediaTimingFunctionDefault 和kCAMediaTimingFunctionEaseInEaseOut類似,只是加速和減速的過程都稍微有些慢。

CAMediaTimingFunction使用了一個(gè)叫做三次貝塞爾曲線的函數(shù),它只可以產(chǎn)出指定緩沖函數(shù)的子集。一個(gè)三次貝塞爾曲線通過四個(gè)點(diǎn)來定義,第一個(gè)和最后一個(gè)點(diǎn)代表了曲線的起點(diǎn)和終點(diǎn),剩下中間兩個(gè)點(diǎn)叫做控制點(diǎn),因?yàn)樗鼈兛刂屏饲€的形狀,貝塞爾曲線控制點(diǎn)其實(shí)是位于曲線之外的點(diǎn),也就是說曲線并不一定要穿過它們??梢园阉鼈兿胂蟪晌?jīng)過它們的磁鐵。

CAKeyframeAnimation有個(gè)timingFunctions屬性是CAMediaTimingFunction對象數(shù)組??梢杂糜趯?shí)現(xiàn)復(fù)雜動(dòng)畫

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容