參考文檔: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);,則效果如下:

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

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

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);
}

- 當(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所在的位置。

當(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的寬高不再一致了。

錨點(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)。示例如下:

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);

如果是矩形或者圓之類的,用CGPath會相當(dāng)簡單明了,但是如果是更加復(fù)雜的一些圖形,UIBezierPath類會更適合。
圖層蒙板
CALayer有一個(gè)屬性叫做mask,這個(gè)屬性本身就是一個(gè)CALayer類型。它類似于一個(gè)字圖層,相對于父圖層(即擁有該屬性的圖層)布局。不同于那些繪制在父圖層中的字圖層,mask圖層定義了父圖層的部分可見區(qū)域。
mask圖層的color屬性是無關(guān)緊要的,真正重要的是圖層的輪廓,mask圖層實(shí)心的部分會被保留下來,其他的則會被拋棄。

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

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;
};

混合變換
CGAffineTransformConcat(CGAffineTransform t1,
CGAffineTransform t2)
3D變換


透視投影
在真實(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對象的視圖中心。

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];

圓角
創(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];

如果想依照圖形來裁剪視圖內(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;

行距和字距
由于繪制的實(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);

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];

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

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)畫