一扫沼、前言
Core Animation
主要依賴于CALayer
的對象layer來進行一切與動畫有關的操作。在view中,layer包含了view的頁面信息充甚,幾何信息(寬高以政,圓角等)還有一些視覺屬性霸褒。layer無法單獨顯示內容伴找,一個layer僅僅只是管理了一個bitmap的狀態(tài)信息,而bitmap可以作為用戶定義的一個view或者多個view混合形成的image的返回結果废菱。layer主要是管理數據技矮,因此在app中使用layer,可以將它當中和model一樣的對象使用殊轴。
通過CALayer可以實現以下功能:
移動衰倦、縮放、旋轉旁理、變形樊零、圓角、透明度孽文、陰影驻襟,帶顏色的邊框、不規(guī)則圖形實現芋哭、透明遮罩沉衣、多級非線性動畫。
二减牺、CALayer
CALayer和UIView有些相似豌习,是一些被層級關系樹管理著的矩形塊,同時也可以添加subLayer拔疚,唯一的區(qū)別是CALayer不處理交互事件肥隆。
2.1CALayer tree
Core Animation 使用三種類型的layer tree對象來實現動畫:
- model layer tree(模型層樹)
Objects in the model layer tree (or simply “l(fā)ayer tree”) are the ones your app interacts with the most. The objects in this tree are the model objects that store the target values for any animations. Whenever you change the property of a layer, you use one of these objects.
模型層樹是于app管理最密切的稚失,這個層樹里面的model對象存儲了所有與動畫有關所有值信息栋艳,改變layer的值的時候就是改變這些對象的值。
- presentation tree(表示層樹)
Objects in the presentation tree contain the in-flight values for any running animations. Whereas the layer tree objects contain the target values for an animation, the objects in the presentation tree reflect the current values as they appear onscreen. You should never modify the objects in this tree. Instead, you use these objects to read current animation values, perhaps to create a new animation starting at those values.
表示層樹的對象包含了任何正在進行的動畫的值墩虹,是當前值在屏幕上的映射嘱巾。在使用動畫時,最好不要更改這些對象诫钓,而是使用這些對象讀取當前動畫相關值旬昭,在這些值的基礎上創(chuàng)建一個新的動畫。
- refer tree(渲染層樹)
Objects in the render tree perform the actual animations and are private to Core Animation.
渲染樹是動畫的真實執(zhí)行者菌湃,但是它是私有的问拘,開發(fā)者不能調用。
執(zhí)行動畫時,app主要與layer tree對象通信,偶爾會獲取通過presentationLayer
屬性獲取presentation tree中與之響應的對象骤坐。通過presentationLayer
,開發(fā)者可以在動畫過程中讀取到presentation layer的屬性值绪杏。
當改變layer的值的時候,layer-tree的值會馬上改變纽绍,通過render-tree渲染蕾久,presentation-tree以動畫的形式展現layer的某個屬性值的漸變過程。
Important: You should access objects in the presentation tree only while an animation is in flight. While an animation is in progress, the presentation tree contains the layer values as they appear onscreen at that instant. This behavior differs from the layer tree, which always reflects the last value set by your code and is equivalent to the final state of the animation.
如果在動畫中的任意時刻拌夏,查看 layer 的 opacity
值僧著,你是得不到與屏幕內容對應的透明度的。取而代之障簿,你需要查看 presentationLayer
的opacity
以獲得正確的結果盹愚。
2.2CALayer的屬性
- layer中動畫很少使用frame,用的是bounds和position
- 設置透明度不是alpha站故,而是opacity
anchorPoint:
Geometry related manipulations of a layer occur relative to that layer’s anchor point皆怕,The impact of the anchor point is most noticeable when manipulating the position or transform properties of the layer.
layer的幾何操作都與錨點有關,當操作position和transform屬性的時候最為明顯西篓。
表示的是該點相對于x愈腾,y的比例,默認值為(0.5污淋,0.5)顶滩。
改變錨點相當于改變了參照點,圖形沿著錨點改變的反方向移動寸爆。
嘗試用一個黑點表示錨點礁鲁,改變錨點的位置,看看圖形的移動情況:
int i = 1;
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"bbb");
UIView *v = self.subviews[0];
v.layer.cornerRadius = 4;
v.layer.masksToBounds = YES;
CGPoint p;
switch (i) {
case 1:
p = CGPointMake(0,0);
break;
case 2:
p = CGPointMake(0.5,0.5);
break;
case 3:
p = CGPointMake(1,1);
break;
default:
break;
}
i++;
if (i > 3) {
i = 1;
}
self.layer.anchorPoint = p;
[UIView animateWithDuration:0.3 animations:^{
//紅色view的寬高分別為100赁豆,50
v.center = CGPointMake(p.x * 100, p.x * 50);
}];
NSLog(@"%@",NSStringFromCGPoint(p));
NSLog(@"%@",NSStringFromCGPoint(self.layer.position));
}
其他幾個屬性的改變演示:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
UITouch *touch = [touches anyObject];
CGPoint p = [touch locationInView:self];
CGFloat raidus = 25;
CGSize size = CGSizeMake(2*raidus, 2*raidus);
CALayer *layer = [[CALayer alloc] init];
//中心點位置
layer.position = p;
//layer大小
layer.bounds = CGRectMake(0, 0, size.width,size.height);
//圓角
layer.cornerRadius = raidus;
//背景顏色仅醇,
layer.backgroundColor = [UIColor colorWithRed:50/255 green:200/255 blue:120/255 alpha:1].CGColor;
//陰影顏色
layer.shadowColor = [UIColor grayColor].CGColor;
//陰影偏移量
layer.shadowOffset = CGSizeMake(5, 5);
//陰影透明度
layer.shadowOpacity = 0.8;
[self.layer addSublayer:layer];
}
2.3 layer與view的關系
雖然可以在layer上不斷添加子layer,但是layer不能取代view魔种,layer只是讓動畫和繪制view的內容更高效析二。
Layers do not handle events, draw content, participate in the responder chain, or do many other things.
layer不會響應事件,也不會參與事件的傳遞节预,因此view是必不可少的叶摄,否則就不能響應各類的事件了()。
NOTE:可以通過hitTest:
方法判斷是否點擊了layer
下面代碼利用presentedLayer
和hitTest:
實現點擊屏幕任意位置安拟,layer會移動到該處蛤吓,點擊layer隨機改變layer背景:
@interface ViewController ()
@property (strong, nonatomic) CALayer *layer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIImage *image = [UIImage imageNamed:@"icon1"];
CALayer *layer = [CALayer layer];
layer.position = CGPointMake(50, 50);
layer.bounds = CGRectMake(0, 0, 50, 50);
layer.contents = (__bridge id)(image.CGImage);
layer.contentsGravity = kCAGravityResizeAspect;
layer.contentsScale = [UIScreen mainScreen].scale;
[self.view.layer addSublayer:layer];
self.point = self.leftBottomView.layer.position;
self.layer = layer;
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
CGPoint p = [[touches anyObject] locationInView:self.view];
if ([self.layer.presentationLayer hitTest:p]) {
self.layer.backgroundColor = [UIColor colorWithRed:arc4random()/INT_MAX green:arc4random()/INT_MAX blue:arc4random()/INT_MAX alpha:1].CGColor;
}else{
[CATransaction begin];
[CATransaction setAnimationDuration:4];
self.layer.position = p;
[CATransaction commit];
}
}
@end
三、基于CALayer的繪圖模型
layer在app中不具有實際的繪圖能力糠赦,它只是獲取了app的頁面并將它緩存到一個被稱作 后備緩存器 的bitmap中会傲。用戶改變了layer的屬性時只是改變了layer的狀態(tài)信息锅棕,當與動畫結合時,Core Animation
將layer的bitmap傳遞給graphics hardware淌山,graphics hardware會將新的變化以動畫的形式表現出來裸燎,
在view層會調用drawRect:方法從新繪制新的視圖,但是這個方法是用CPU在主線程重新繪制了視圖泼疑,會導致內存消耗過大德绿。Core Animation
通過在任何可以的情況下調度bitmap的緩存來達到同樣或者相似的效果。
3.1 CALayer conent屬性
CALayer有一個與你所要展現的效果的bitmap相結合的contents
屬性王浴,
@property(nullable, strong) id contents;
有三種方式創(chuàng)建content:
1.當layer的content基本不變時可以直接把image對象直接賦值給content
2.當layer的content可能周期改變或者通過外部提供時脆炎,可以把layer的delegate賦值給外部對象梅猿,讓delegate來繪制content
3.當你想創(chuàng)建一個sublayer或者改變layer的繪制行為的時候氓辣,通過重寫sublayer的繪制方法來給layer的content賦值。
-
直接賦值image給content
layer只是管理bitmap圖片的一個容器袱蚓,所有可以直接把image(必須是CGImageRef)直接賦值給content钞啸。layer不會將你的image復制,而是直接使用你提供的image喇潘。在多處使用同一張圖片的時候可以節(jié)省內存体斩。
The image you assign to a layer must be a CGImageRef type.
這里要注意的是,contents
雖然是id類型颖低,但是如果你賦值的不是CGImageRef
類型的值絮吵,會得到空白的圖層。更頭疼的是UIImage
有一個CGImage
屬性忱屑,它返回的是一個CGImageRef
類型值蹬敲,但是你
aLayer.contents = [UIImage imageWithName(@"pic")].CGImage
的時候發(fā)現,編譯器報錯了莺戒,因為CGImageRef并不是一個真正的Cocoa對象伴嗡,而是一個Core Foundation類型,要通過bridged關鍵之轉換(編譯器會提示你這樣做):
aLayer.contents = (__bride id)[UIImage imageWithName(@"pic")].CGImage
示例代碼如下:
-(void)viewDidLoad {
[super viewDidLoad];
UIImage *img = [UIImage imageNamed:@"icon1"];
self.view.layer.contents = (__bridge id)(img.CGImage);
}
得到的效果
contentGravity
可以看出本來圓形的icon被拉長了从铲,UIImageView顯示圖片的時候也遇到過這種情況瘪校,解決方法是設置合適的contentMode值。layer中控制這中屬性的叫contentsGravity名段,它是一個NSString類型:
kCAGravityCenter
kCAGravityTop
kCAGravityBottom
kCAGravityLeft
kCAGravityRight
kCAGravityTopLeft
kCAGravityTopRight
kCAGravityBottomLeft
kCAGravityBottomRight
kCAGravityResize
kCAGravityResizeAspect
kCAGravityResizeAspectFill
contentsScale
contentsScale定義了圖片的像素尺寸和試圖大小的比例阱扬,默認值是1.0(相當于自定義@2x、@3x圖片伸辟,例如麻惶,如果要以每個點一個像素繪制圖片,就自娩,設置值為1.0用踩,如果要以每個點2個像素繪制圖片渠退,設置值為2.0)。
NOTE:記得手動設置contentsScale屬性的值脐彩,否則圖片在retina屏幕上顯示就不正確了碎乃。
layer.contentsScale = [UIScreen mainScreen].bounds;
** contentRect**
contentRect允許開發(fā)者在圖層的邊框里顯示圖片的一個子區(qū)域,它使用了單位坐標來計算惠奸,默認的值為(0梅誓,0,1佛南,1)梗掰,如果知道一個小一點的矩形,圖片就會被裁剪嗅回。
利用這一點可以用來做圖片拼合及穗。先將需要拼合的圖片打包整合到一張大圖上一次性載入,相比多次載入不同的圖片绵载,可以優(yōu)化內存使用埂陆,縮短載入時間等。
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *leftTopView;
@property (weak, nonatomic) IBOutlet UIView *rightTop;
@property (weak, nonatomic) IBOutlet UIView *leftBottomView;
@property (weak, nonatomic) IBOutlet UIView *rightBottomView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIImage *image = [UIImage imageNamed:@"icon1"];
[self addSpriteImage:image withContentRect:CGRectMake(0, 0, 0.5, 0.5) toLayer:self.leftTopView.layer];
[self addSpriteImage:image withContentRect:CGRectMake(0.5, 0, 0.5, 0.5) toLayer:self.rightTop.layer];
[self addSpriteImage:image withContentRect:CGRectMake(0, 0.5, 0.5, 0.5) toLayer:self.leftBottomView.layer];
[self addSpriteImage:image withContentRect:CGRectMake(0.5, 0.5, 0.5, 0.5) toLayer:self.rightBottomView.layer];
}
- (void)addSpriteImage:(UIImage *)image withContentRect:(CGRect)rect toLayer:(CALayer *)layer //set image
{
layer.contents = (__bridge id)image.CGImage;
//scale contents to fit
layer.contentsGravity = kCAGravityResizeAspect;
//set contentsRect
layer.contentsRect = rect;
}
contentsCenter
contentsCenter是CGRect類型的值娃豹,它與contentGravity配合使用焚虱,表示的是可被拉伸的區(qū)域,也就是說懂版,在這個區(qū)域內的才會被contentGravity(kCAGravityResize
鹃栽、kCAGravityResizeAspect、
kCAGravityResizeAspectFill才有效)影響到躯畴。
UIImage *image = [UIImage imageNamed:@"icon1"];
CALayer *layer = self.view.layer;
layer.contents = (__bridge id)(image.CGImage);
layer.contentsGravity = kCAGravityResizeAspect;
layer.contentsScale = [UIScreen mainScreen].scale;
layer.contentsCenter = CGRectMake(0.45, 0.45, 0.1, 0.1);
-
通過layer的代理賦值image
如果layer的content屬性會動態(tài)改變時民鼓,需要用到CALayerDelegate的代理方法在需要的時候給content賦值。layer有一個可選的代理屬性私股,實現了CALayerDelegate協議摹察,它是一個非正式協議,你只需要調用想調用的方法倡鲸,剩下的CALayer會幫你實現供嚎,代理方法:
//當已經有image對象時可以直接調用這個代理方法給content賦值
-(void)displayLayer:(CALayer *)layer;
//如果想通過自定義顯示內容的話用這個方法
-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)cox;
NOTE:兩個代理方法調用一個即可,如果兩個方法都實現了峭状,系統只會調用displayLayer:方法克滴。
apple官方文檔給的實例代碼如下:
- (void)displayLayer:(CALayer *)theLayer {
// Check the value of some state property
if (self.displayYesImage) {
// Display the Yes image
theLayer.contents = [someHelperObject loadStateYesImage];
}else {
// Display the No image
theLayer.contents = [someHelperObject loadStateNoImage];
}
}
必須是新建的layer的delegate方法,而且必須調用layer(注意是layer不是view)的setNeedsDisplay方法优床,否則兩個代理方法都不會實現劝赔。
-(void)viewDidLoad {
[super viewDidLoad];
CALayer *layer = [[CALayer alloc] init];
layer.position = CGPointMake(100, 200);
layer.bounds = CGRectMake(0, 0, 100, 100);
layer.delegate = self;
[self.view.layer addSublayer:layer];
//必須手動調用setNeedsDisplay方法,否則代理方法無法實現
[layer setNeedsDisplay];
}
-(void)drawLayer:(CALayer *)theLayer inContext:(CGContextRef)theContext {
CGMutablePathRef thePath = CGPathCreateMutable();
CGPathMoveToPoint(thePath,NULL,15.0f,15.f);
CGPathAddCurveToPoint(thePath,
NULL,
15.f,250.0f,
295.0f,250.0f,
295.0f,15.0f);
CGContextBeginPath(theContext);
CGContextAddPath(theContext, thePath);
CGContextSetLineWidth(theContext, 5);
CGContextStrokePath(theContext);
// Release the path
CFRelease(thePath);
}