簡介ios
圖層樹git
寄宿圖github
圖層幾何學算法
視覺效果數據庫
變換編程
專有圖層api
隱式動畫數組
顯示動畫緩存
圖層時間安全
UIKit API
UIKit是一組Objective-C API,爲線條圖形、Quartz圖像和顏色操做提供Objective-C 封裝,並提供2D繪製、圖像處理及用戶接口級別的動畫。
UIKit包括UIBezierPath(繪製線、角度、橢圓及其它圖形)、UIImage(顯示圖像)、UIColor(顏色操做)、UIFont和UIScreen(提供字體和屏幕信息)等類以及在位圖圖形環境、PDF圖形環境上進行繪製和 操做的功能等, 也提供對標準視圖的支持,也提供對打印功能的支持。
UIKit中UIView類自己在繪製時自動建立一個圖形環境(對應Core Graphics層的CGContext類型)做爲當前的圖形繪製環境。在繪製時能夠調用UIGraphicsGetCurrentContext 函數得到當前的圖形環境。
Core Animation
Core Animation是一套Objective-C API,實現了一個高性能的複合引擎,並提供一個簡單易用的編程接口,給用戶UI添加平滑運動和動態反饋能力。
Core Animation 是 UIKit實現動畫和變換的基礎,也負責視圖的複合功能。使用Core Animation能夠實現定製動畫和細粒度的動畫控制,建立複雜的、支持動畫和變換的layered 2D視圖。
Core Animation不屬於繪製系統,但它是以硬件複合和操做顯示內容的基礎設施。這個基礎設施的核心是layer對象,用來管理和操做顯示內容。在ios 中每個視圖都對應Core Animation的一個層對象,與視圖同樣,層之間也組織爲層關係樹。一個層捕獲視圖內容爲一個被圖像硬件容易操做的位圖。在多數應用中層做爲管理視圖的方式使用,但也能夠建立獨立的層到一個層關係樹中來顯示視圖不夠支持的顯示內容。
Core Graphics 與Quartz 2D API
Core Graphics是一套C-based API, 支持向量圖形,線、形狀、圖案、路徑、剃度、位圖圖像和pdf 內容的繪製。
Quartz 2D 是Core Graphics中的2D 繪製呈現引擎。Quartz是資源和設備無關的,提供路徑繪製,anti-aliased(抗鋸齒處理)呈現,剃度填充圖案,圖像,透明繪製和透明層、遮蔽和陰影、顏色管理,座標轉換,字體、offscreen呈現、pdf文檔建立、顯示和分析等功能。
Quartz 2D可以與全部的圖形和動畫技術(如Core Animation, OpenGL ES, 和 UIKit 等)一塊兒使用。
Quartz採用paint模式進行繪製。
Quartz 中使用的圖形環境也由一個類CGContext表示。
Quartz 中能夠把一個圖形環境做爲一個繪製目標。當使用Quartz 進行繪製時,全部設備特定的特性被包含在你使用的特定類型的圖形環境中,所以經過給相同的圖像操做函數提供不一樣的圖像環境你就可以畫相同的圖像到不一樣的設備上,所以作到了圖像繪製的設備無關性。
Core Animation
Core Animation是iOS與OS X平臺上負責圖形渲染與動畫的基礎設施。Core Animation能夠動畫視圖和其餘的可視元素。Core Animation爲你完成了實現動畫所需的大部分繪幀工做。你只需在配置少許的動畫參數(如開始點位置和結束點位置)就可啓動Core Animation。Core Animation將大部分實際的繪圖任務交給了圖形硬件處理,圖形硬件會加速圖形渲染的速度。這種自動化的圖形加速讓動畫具備更高的幀率且更加平滑,但這並不會增長CPU的負擔而致使影響你應用的運行速度。
CoreAnimation是一個複合引擎,它的職責就是儘量快的組合屏幕上不一樣的可視內容,這個內容是被分解成獨立的圖層,存儲在一個叫作圖層樹的體系之中。這個樹造成了UIKit以及在iOS應用程序當中你所能在屏幕上看見的一切的基礎。
※圖層和視圖
一個視圖就是在屏幕上顯示的一個矩形塊(好比圖片,文字或者視頻),它可以攔截相似於鼠標點擊或者觸摸手勢等用戶輸入。視圖在層級關係中能夠互相嵌套, 一個視圖能夠管理它全部子視圖的位置。在iOS當中,全部的視圖都從一個叫作UIView的基類派生而來,UIView能夠處理觸摸事件,能夠支持基於CoreGraphics繪圖,能夠作仿射變換(例如旋轉或者縮放),或者簡單的相似於滑動或者漸變的動畫。
(一種典型的iOS屏幕(左邊)和造成視圖的層級關係(右邊))
※CALayer
CALayer類在概念上和UIView相似,一樣也是一些被層級關係樹管理的矩形塊,一樣也能夠包含一些內容(像圖片,文本或者背景色),管理子圖層的位置。它們有一些方法和屬性用來作動畫和變換。和UIView最大的不一樣是CALayer不處理用戶的交互,CALayer並不清楚具體的響應鏈(iOS 11經過視圖層級關係用來傳送觸摸事件的機制),因而它並不可以響應事件,可是它提供了一些方法來判斷是否一個觸點在圖層的範圍指內(圖層幾何學)。
※平行的層級關係
每個UIView都有一個CALayer實例的圖層屬性,也就是所謂的backing layer,視圖的職責就是建立並管理這個圖層,以確保當子視圖在層級關係中添加或者被移除的時候,他們關聯的圖層也能一樣對應在層級關係樹當中有相同的操做。實際上這些背後關聯的圖層纔是真正用來在屏幕上顯示和作動畫,UIView僅僅是對它的一個封裝,提供了一些iOS相似於處理觸摸的具體功能,以及CoreAnimation底層方法的高級接口。
iOS基於UIVie和CALayer提供兩個平行的層級關係作職責分離,這樣也能避免不少重複代碼。在iOS和Mac OS兩個平臺上,事件和用戶交互有不少地方的不一樣,基於多點觸控的用戶界面和基於鼠標鍵盤有着本質的區別,這就是爲何iOS有UIKit何UIView,可是Mac OS有AppKit何NSView的緣由。他們功能上很類似,可是實現上有着顯著的區別。
繪圖,佈局和動畫,相比之下就是相似Mac筆記本和桌面系列同樣應用於iPhone和iPad觸屏的改了。吧這種功能的邏輯分開並應用到獨立的CoreAnimation框架,蘋果就可以在iOS和mac OS之間共享代碼,使得對蘋果本身的OS開發團隊和第三方開發者去開發兩個平臺的應用更加便捷。
實際上,這裏並非兩個層級關係,而是四個,每個都扮演了不一樣的角色,除了圖層級和圖層樹以外,還存在呈現樹和渲染樹。見「隱式動畫」和「性能調優」分別討論。
※圖層的能力
若是你略微想在底層作一些改變,或者使用一些蘋果沒有在UIView上實現的接口功能,這時除了介入CoreAnimation底層以外別無選擇,下面有些UIView沒有暴露出來的CALayer功能:陰影,圓角,帶顏色的邊框;3D變換;非矩形範圍;透明遮罩;多級非線性動畫
※使用圖層
一個視圖只有一個相關聯的圖層(自動建立),同時它也能夠支持添加無數多個子圖層,使用圖層關聯的視圖而不是CALayer的好處在於,你能使用全部CALayer底層特性的同時,也能夠使用UIView的高級API(好比自動排版,佈局和事件處理)。下面狀況可能更須要使用CALayer而不是UIView:開發同時能夠在MacOS上運行的誇平臺應用;使用多種CALayer的子類,而且不想建立額外的UIView去包封裝它們全部;作一些對性能特別挑剔的工做,好比對UIView一些可忽略不計的操做都會引發顯著的不一樣(儘管這種狀況下,你可能會想直接使用OpenGL來繪圖)
//示例
// // LayerTreeViewController.m // CoreAnimationLearn // 圖層的樹狀結構 // Created by Vie on 2017/6/21. // Copyright © 2017年 Vie. All rights reserved. // #import "LayerTreeViewController.h" @interface LayerTreeViewController () @property (nonatomic, strong) UIView *whiteBtmView; @end @implementation LayerTreeViewController #pragma mark lazy loading -(UIView *)whiteBtmView{ if (!_whiteBtmView) { _whiteBtmView=[[UIView alloc] init]; [_whiteBtmView setBackgroundColor:RGB_ALPHA_COLOR(255, 255, 255, 1)]; } return _whiteBtmView; } #pragma mark 視圖控制器回調 - (void)viewDidLoad { [super viewDidLoad]; [self.view setBackgroundColor:RGB_ALPHA_COLOR(230, 230, 230, 1)]; UIBarButtonItem *leftItem=[[UIBarButtonItem alloc] initWithTitle:@"<" style:UIBarButtonItemStylePlain target:self action:@selector(backAction:)]; self.navigationItem.leftBarButtonItem=leftItem; //讓約束從導航欄底部開始算起 self.edgesForExtendedLayout = UIRectEdgeNone; self.extendedLayoutIncludesOpaqueBars = NO; self.modalPresentationCapturesStatusBarAppearance = NO; //白色視圖 [self.view addSubview:self.whiteBtmView]; [self.whiteBtmView mas_makeConstraints:^(MASConstraintMaker *make) { make.width.mas_equalTo(200); make.height.mas_equalTo(200); make.center.mas_equalTo(self.view); }]; //藍色layer CALayer *blueLayer=[CALayer layer]; blueLayer.frame=CGRectMake(50.0f, 50.0f, 100.0f, 100.0f); blueLayer.backgroundColor=[UIColor blueColor].CGColor; [self.whiteBtmView.layer addSublayer:blueLayer]; } #pragma mark Action -(void)backAction:(id)sender{ [self.navigationController popViewControllerAnimated:YES]; } #pragma mark 其餘方法 @end
運行效果
※contents屬性
CALayer有一個屬性叫作contents,這個屬性的類型被定義爲id,但實際給contents賦的不是CGImageRef獲得的圖層將是空白的。它之因此被定義爲id類型是由於在Mac OS系統上,這個屬性對CGImage和NSImage類型的值都起做用。UIimage有一個CGImage屬性,它返回一個「CGImageRef」,但這裏並非一個真正的Cocoa對象,而是一個CoreFoundation類型,能夠經過bridged關鍵字轉換(若是沒有使用ARC就不須要__bridge這部分)。若是要給圖層的寄宿圖層賦值能夠按照下面這個方法
layer.contents = (__bridge id)image.CGImage;
※contentsGravity
CALayer與contentMode對應的屬性叫作contentsGravity,它是一個NSString類型,不像對應UIKit部分,那裏面是枚舉。和contentMode同樣,contentsGravity的目的是爲了解決內容在圖層的邊界中怎麼對齊。contentsGravity可選的常量值有一下一些:kCAGravityCenter;kCAGravityTop;kCAGravityBottom;kCAGravityLeft;kCAGravityRight;kCAGravityTopLeft;kCAGravityTopRight;kCAGravityBottomLeft; kCAGravityBottomRight;kCAGravityResize;kCAGravityResizeAspect;kCAGravityResizeAspectFil。下面等比拉伸以適應圖層邊界。
self.layerView.layer.contentsGravity = kCAGravityResizeAspect;
※contentsScale
contentsScale屬性定義寄宿圖的像素尺寸和視圖大小的比例,默認狀況下它是一個值爲1.0的浮點數(若是contentsScale設置爲1.0,將會以每一個點2個像素繪製圖片,這就是Retina屏幕)。它並非總會對屏幕上的寄宿圖有影響,由於contents因爲設置了contentsGravity屬性,它已經被拉伸以適應圖層的邊界。UIView有一個相似功能可是很是少用到contentsScaleFactor屬性。下面設置圖層contentsScale屬性:
layer.contentsScale = [UIScreen mainScreen].scale;
※maskToBounds
UIView有一個clipsToBounds的屬性能夠用來決定是否顯示超出邊界的內容,CALayer對應的屬性叫作maskToBounds。
※contentsRect
CALayer的contentsRect屬性容許咱們在圖層邊框裏顯示寄宿圖的一個子域,和bouns,frame不一樣,它使用了單位座標,指定在0到1直接,是一個相對值(而像素和點是絕對值)。因此它們是相對於寄宿圖的尺寸的。默認的contentsRect是{0, 0, 1, 1}
一個自定義的contentsRect和顯示的內容
contentsRect最有趣的用處之一就是它可以使用image sprites(圖片拼合),一般多張圖片能夠拼合後打包整合到一張大圖上一次性載入。相比屢次載入不一樣圖片,這樣作可以帶了不少方面的好處:內存使用,載入時間,渲染性能等等。
※contentsCenter
contentsCenter實際上是一個CGRect,它定義了圖層中的可拉伸區域和一個固定的邊框。改變contentsCenter的值並不會影響到技術圖的顯示,除非這個圖層的大小改變了,你纔看的到效果。默認狀況下,contentsCenter是{0,0,1,1},這覺得這layer的大小改變了,那麼寄宿圖將會根據contentsGravity均勻地拉伸開。可是若是增長原點的值並減少尺寸,將會在周圍創造一個邊框。
contentsCenter設置爲{0.25, 0.25, 0.5, 0.5}的效果。
※Custome Drawing
給contents賦CGImage的值不是惟一的設置寄宿圖的方法。咱們也能夠直接用CoreGraphics直接繪製寄宿圖,CALayer有一個可選的delegate屬性,實現了CALayerDelegate協議
//示例
// // BoardingFigureViewController.m // CoreAnimationLearn // 寄宿圖 // Created by Vie on 2017/6/22. // Copyright © 2017年 Vie. All rights reserved. // #import "BoardingFigureViewController.h" @interface BoardingFigureViewController ()<CALayerDelegate> @property (nonatomic, strong) UIView *whiteBtmView; @property (nonatomic, strong) UIView *whiteOtheBtmView; @property (nonatomic, strong) UIView *contentsCenterView; @end @implementation BoardingFigureViewController #pragma mark lazy loading -(UIView *)whiteBtmView{ if (!_whiteBtmView) { _whiteBtmView=[[UIView alloc] init]; [_whiteBtmView setBackgroundColor:RGB_ALPHA_COLOR(255, 255, 255, 1)]; } return _whiteBtmView; } -(UIView *)whiteOtheBtmView{ if (!_whiteOtheBtmView) { _whiteOtheBtmView=[[UIView alloc] init]; [_whiteOtheBtmView setBackgroundColor:RGB_ALPHA_COLOR(255, 255, 255, 1)]; } return _whiteOtheBtmView; } -(UIView *)contentsCenterView{ if (!_contentsCenterView) { _contentsCenterView=[[UIView alloc] init]; [_contentsCenterView setBackgroundColor:RGB_ALPHA_COLOR(255, 255, 255, 1)]; } return _contentsCenterView; } #pragma mark 視圖控制器回調 - (void)viewDidLoad { [super viewDidLoad]; [self.view setBackgroundColor:RGB_ALPHA_COLOR(230, 230, 230, 1)]; UIBarButtonItem *leftItem=[[UIBarButtonItem alloc] initWithTitle:@"<" style:UIBarButtonItemStylePlain target:self action:@selector(backAction:)]; self.navigationItem.leftBarButtonItem=leftItem; //讓約束從導航欄底部開始算起 self.edgesForExtendedLayout = UIRectEdgeNone; self.extendedLayoutIncludesOpaqueBars = NO; self.modalPresentationCapturesStatusBarAppearance = NO; //白色視圖 [self.view addSubview:self.whiteBtmView]; [self.whiteBtmView mas_makeConstraints:^(MASConstraintMaker *make) { make.width.mas_equalTo(100); make.height.mas_equalTo(50); make.center.mas_equalTo(self.view); }]; self.whiteBtmView.layer.contents=(__bridge id)[UIImage imageNamed:@"1.jpg"].CGImage; // self.whiteBtmView.contentMode = UIViewContentModeScaleAspectFit; self.whiteBtmView.layer.contentsGravity = kCAGravityResizeAspect; // self.whiteBtmView.layer.contentsGravity = kCAGravityCenter; self.whiteBtmView.layer.contentsScale=[UIScreen mainScreen].scale; //不顯示超出邊界的內容 // self.whiteBtmView.layer.masksToBounds=YES; self.whiteBtmView.clipsToBounds=YES; //顯示寄宿圖的一個子域。contentsRect不是按點來計算的,它使用了單位座標,單位座標指定在0到1之間 self.whiteBtmView.layer.contentsRect=CGRectMake(0, 0, 1, 0.5); //另外一個白色視圖 [self.view addSubview:self.whiteOtheBtmView]; [self.whiteOtheBtmView mas_makeConstraints:^(MASConstraintMaker *make) { make.width.and.height.mas_equalTo(self.whiteBtmView); make.top.mas_equalTo(self.whiteBtmView.mas_bottom); make.left.mas_equalTo(self.whiteBtmView); }]; self.whiteOtheBtmView.layer.contents=(__bridge id)[UIImage imageNamed:@"1.jpg"].CGImage; self.whiteOtheBtmView.layer.contentsGravity = kCAGravityResizeAspect; self.whiteOtheBtmView.layer.contentsScale=[UIScreen mainScreen].scale; //顯示寄宿圖的一個子域。contentsRect不是按點來計算的,它使用了單位座標,單位座標指定在0到1之間 self.whiteOtheBtmView.layer.contentsRect=CGRectMake(0, 0.5, 1, 0.5); //contentsCenterView [self.view addSubview:self.contentsCenterView]; [self.contentsCenterView mas_makeConstraints:^(MASConstraintMaker *make) { make.height.mas_equalTo(100); make.width.mas_equalTo(100); make.top.mas_equalTo(self.whiteOtheBtmView.mas_bottom); make.left.mas_equalTo(self.whiteBtmView); }]; self.contentsCenterView.layer.contents=(__bridge id)[UIImage imageNamed:@"5.png"].CGImage; self.contentsCenterView.layer.contentsGravity = kCAGravityResizeAspect; self.contentsCenterView.layer.contentsCenter= CGRectMake(0.25, 0.25, 0.5, 0.5); //藍色layer CALayer *blueLayer=[CALayer layer]; blueLayer.frame=CGRectMake(50.0f, 50.0f, 100.0f, 100.0f); blueLayer.backgroundColor=[UIColor blueColor].CGColor; [self.contentsCenterView.layer addSublayer:blueLayer]; blueLayer.delegate=self; [blueLayer display]; } #pragma mark CALayerDelegate -(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{ CGContextSetLineWidth(ctx, 2.0f); CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor); CGContextStrokeEllipseInRect(ctx, layer.bounds); } #pragma mark Action -(void)backAction:(id)sender{ [self.navigationController popViewControllerAnimated:YES]; } @end
運行效果
※佈局
UIView有三個比較重要的佈局屬性frame,bounds,center;CALayer對應地叫作frame,bounds和position。
frame表明了圖層外部座標(也就是在父圖層上佔據的空間),bounds是內部座標({0,0}一般是圖層的左上角),center和position都表明了相對於父圖層anchorPoint所在的位置,如今能夠把它想象成圖層的中心點就行。
下面顯示了這些屬性是如何相互依賴的,UIView和CALayer的座標系的座標系
視圖的frame,bounds和center屬性僅僅是存取方法,當操縱視圖的frame,其實是在改變位於視圖下方CALayer的frame,不可以獨立於圖層以外改變視圖的frame。對於視圖或者圖層來講,frame並非一個很是清晰的屬性,它實際上是一個虛擬屬性,是根據bounds,position和transform計算而來,因此當其中任何一個值發生改變,frame都會變化,相反改變frame的值一樣會影響到他們當中的值。
當對圖層作變換的時候,好比旋轉或者縮放,frame實際上表明瞭覆蓋在圖層旋轉以後整個軸對齊的矩形區域,也就是說frame的寬高可能和bounds的寬高再也不一致了;下面是旋轉一個視圖或者圖層以後的frame屬性
※錨點
前面說視圖的center屬性和圖層的position屬性都指定了anchorPoint相對於父圖層的位置。圖層的anchorPoint經過position來控制它的fram的位置。能夠認爲anchorPoint是用來移動圖層的把柄。默認來講anchorPoint位於圖層的中點,因此圖層將會以這個點爲中心放置。anchorPoint屬性並無被UIView接口暴露出來,這也是視圖的position屬性被叫作「center」的緣由。可是圖層的anchorPoint能夠被移動,好比你能夠把它置於frame的左上角,因而圖層的內容將會向右下角的position方向移動,而不是居中了。anchorPoint用代爲座標來描述,也就是圖層的相對座標,圖層左上角是{0,0},右下角是{1,1},所以默認座標是{0.5,0.5},anchorPoint能夠經過制定x和y值小於0或者大於1,使它放置在圖層範圍以外。下圖改變了anchorPoint,position屬性保持固定的值並無發生改變,可是fram卻移動了。
※座標系
和視圖同樣,圖層在圖層樹當中也是相對於父圖層按層級關係放置,一個圖層的position依賴於它父圖層的bounds,若是父圖層發生了移動,它的全部子圖層也會跟着移動。這樣對於放置圖層會更加方便,由於你能夠經過移動根視圖來將它們的子圖層做爲一個總體來移動,可是有時候你須要知道一個圖層的絕對位置,或者是相對於另外一個圖層的位置,而不是它當前父圖層的位置。CALayer給不一樣座標系之間的圖層轉換提供了一些工具類方法:(這些方法能夠把定義在一個圖層座標系下的點或者矩形轉換成另外一個圖層座標系下的點或者矩形)
- (CGPoint)convertPoint:(CGPoint)point fromLayer:(CALayer *)layer; - (CGPoint)convertPoint:(CGPoint)point toLayer:(CALayer *)layer; - (CGRect)convertRect:(CGRect)rect fromLayer:(CALayer *)layer; - (CGRect)convertRect:(CGRect)rect toLayer:(CALayer *)layer;
※翻轉的幾何結構
在iOS上一個圖層的position位於父圖層的左上角,在Mac OS上一般是位於左下角。CoreAnimation能夠經過geometryFlipped屬性來視頻這兩種狀況,它決定了一個圖層的座標是否相對於父圖層垂直翻轉,是一個BOOL類型。在iOS上經過設置它爲YES意味着它的子圖層將會被垂直翻轉,也就是將會沿着底部排版而不是一般的頂部(它的全部子圖層也同理,除非把它們的gemetryFlipped屬性也設爲YES)。
※Z座標軸
和UIView嚴格的二維座標系不一樣,CALayer存在於一個三維空間當中。除了position和anchorPoint屬性以外,CALayer還有另外兩個屬性,zPosition和anchorPointZ,兩者都是在Z軸上描述圖層位置的浮點類型。zPosition最實用的功能就是改變圖層顯示順序了。一般圖層是根據它們子圖層的sublayers出現的順序來繪製的,這就是所謂的畫家算法--就像一個畫家在牆上做畫--後被繪製上的圖層將會遮蓋住以前的圖層,可是經過增長圖層的zPosition,就能夠把圖層向相機(這裏相機相對於用戶是視角,和iPhone的內置相機沒有任何關係)方向前置,因而它就在全部小於它zPosition圖層值的的前面了。通常給zPosition提升一個像素就可讓視圖前置,固然0.1或者0.0001也可以作到,可是最好不要這樣,由於浮點類型四捨五入的計算可能會形成一些不便的麻煩。
※Hit Testing
CALayer並不關心任何響應鏈事件,因此不能直接處理觸摸事件或者手勢,可是它又一系列方法幫你處理事件:
-containsPoint:接受一個在本圖層座標系下的CGPoint,若是這個點在圖層frame範圍內就返回YES。
-hitTest:方法一樣接受一個CGPoint類型參數,而不是BOOL類型,它返回圖層自己,或者包含這個座標點的葉子節點圖層。
※自動佈局
在Mac OS平臺,CALayer有一個叫作layoutManager的屬性能夠經過CALayoutManager協議和CAConstraintLayoutManager類來實現自動排版機制。可是因爲某些緣由,這在iOS上並不適用。最好使用視圖而不是單獨的圖層而不是單獨圖層來構建應用程序的另外一個重要緣由之一。
//示例
// // LayerGeometryViewController.m // CoreAnimationLearn // 圖層幾何學 // Created by Vie on 2017/6/22. // Copyright © 2017年 Vie. All rights reserved. // #import "LayerGeometryViewController.h" @interface LayerGeometryViewController () @property (nonatomic, strong) UIView *whiteView; @property (nonatomic, strong) UIView *greenView; @property (nonatomic, strong) UIView *redView; @property (nonatomic, strong) CALayer *blueLayer; @end @implementation LayerGeometryViewController #pragma mark lazy loading -(UIView *)whiteView{ if (!_whiteView) { _whiteView=[[UIView alloc] init]; [_whiteView setBackgroundColor:RGB_ALPHA_COLOR(255, 255, 255, 1)]; } return _whiteView; } -(UIView *)greenView{ if (!_greenView) { _greenView=[[UIView alloc] init]; [_greenView setBackgroundColor:RGB_ALPHA_COLOR(29, 164, 23, 1)]; } return _greenView; } -(UIView *)redView{ if (!_redView) { _redView=[[UIView alloc] init]; [_redView setBackgroundColor:RGB_ALPHA_COLOR(215, 0, 18, 1)]; } return _redView; } -(CALayer *)blueLayer{ if (!_blueLayer) { _blueLayer=[CALayer layer]; _blueLayer.backgroundColor=[UIColor blueColor].CGColor; } return _blueLayer; } #pragma mark 視圖控制器回調 - (void)viewDidLoad { [super viewDidLoad]; [self.view setBackgroundColor:RGB_ALPHA_COLOR(230, 230, 230, 1)]; UIBarButtonItem *leftItem=[[UIBarButtonItem alloc] initWithTitle:@"<" style:UIBarButtonItemStylePlain target:self action:@selector(backAction:)]; self.navigationItem.leftBarButtonItem=leftItem; //讓約束或者佈局從導航欄底部開始算起(導航欄底部爲Y軸0) self.edgesForExtendedLayout = UIRectEdgeNone; self.extendedLayoutIncludesOpaqueBars = NO; self.modalPresentationCapturesStatusBarAppearance = NO; //白色視圖 [self.view addSubview:self.whiteView]; self.whiteView.frame=CGRectMake(0, 0, 100, 100); self.whiteView.layer.anchorPoint=CGPointMake(0, 0); //藍色layer self.blueLayer.frame=CGRectMake(25.0f, 25.0f, 50.0f, 50.0f); [self.whiteView.layer addSublayer:self.blueLayer]; //綠色視圖 [self.view addSubview:self.greenView]; [self.greenView mas_makeConstraints:^(MASConstraintMaker *make) { make.width.height.mas_equalTo(100); make.top.mas_equalTo(100); make.left.mas_equalTo(100); }]; //紅色視圖 [self.view addSubview:self.redView]; [self.redView mas_makeConstraints:^(MASConstraintMaker *make) { make.width.height.mas_equalTo(self.greenView.mas_height); make.top.mas_equalTo(150); make.left.mas_equalTo(150); }]; //移動綠色視圖z座標使其靠近屏幕(zPosition默認0) self.greenView.layer.zPosition=1.0f; } #pragma mark Action -(void)backAction:(id)sender{ [self.navigationController popViewControllerAnimated:YES]; } //hitTest判斷點擊視圖 -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ CUSTOM_LOG(@"進入touches事件") //得到觸摸的相對位置 CGPoint touchPoint=[[touches anyObject] locationInView:self.view]; //得到點擊的圖層自己,或者包含這個座標點的葉子節點圖層 CALayer *touchLayer=[self.whiteView.layer hitTest:touchPoint]; if (touchLayer==self.blueLayer) { [[[UIAlertView alloc] initWithTitle:@"Inside Blue Layer" message:nil delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show]; }else if (touchLayer==self.whiteView.layer){ [[[UIAlertView alloc] initWithTitle:@"Inside White Layer" message:nil delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show]; } } ////containsPoint判斷點擊視圖 //-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ // CUSTOM_LOG(@"進入touches事件") // //得到觸摸的相對位置 // CGPoint touchPoint=[[touches anyObject] locationInView:self.view]; // //轉換指出白層的座標 // touchPoint = [self.whiteView.layer convertPoint:touchPoint fromLayer:self.view.layer]; // //返回接收方是否包含一個指定的點。 // if ([self.whiteView.layer containsPoint:touchPoint]) { // //轉換指出blueLayer的座標 // touchPoint = [self.blueLayer convertPoint:touchPoint fromLayer:self.whiteView.layer]; // if ([self.blueLayer containsPoint:touchPoint]) { // [[[UIAlertView alloc] initWithTitle:@"Inside Blue Layer" // message:nil // delegate:nil // cancelButtonTitle:@"OK" // otherButtonTitles:nil] show]; // } else { // [[[UIAlertView alloc] initWithTitle:@"Inside White Layer" // message:nil // delegate:nil // cancelButtonTitle:@"OK" // otherButtonTitles:nil] show]; // } // } // //} @end
運行效果
※圓角
圓角矩形是iOS的一個標誌性審美特性,CALayer有一個叫作conrnerRadius的屬性控制着圖層角的曲率,它是一個浮點數默認爲0(爲0的時候就是直角),默認狀況下這個曲率值隻影響背景顏色而不影響背景圖片或是子圖層。不過,若是吧maskToBounds設置成YES的話,圖層裏面的全部東西都會被截取。
※圖層邊框
CALayer另外兩個很是有用屬性就是borderWidth和borderColor。兩者共同定義了圖層邊的繪製樣式。這條線(也被稱做stroke)沿着圖層的bounds繪製,同時也包含圖層的角。
borderWidth是以點爲單位的定義邊框粗細的浮點數,默認爲0,borderColor定義了邊框的顏色,默認爲黑色。
borderColor是CGColorRef類型,而不是UIColor。因此它不是Cocoa的內置對象。
※陰影
iOS的另外一個常見特性就是陰影。陰影每每能夠達到圖層深度暗示的效果。也可以用來強調正在顯示的圖層和優先級(好比說一個在其餘視圖以前的彈出框),不過有時候他們只是單純的裝飾目的。
shadowOpactiy(控制陰影透明度)屬性一個大於默認值(也就是0)的值(值必須在0.0和1.0之間的浮點數),陰影就能夠顯示在任意圖層之下。
shadowColor(控制陰影顏色)默認是黑色,它的類型是CGColorRef。
shadowOffset(控制陰影的方向和距離)它是一個CGSize的值,寬度控制着陰影橫向的位移,高度控制着縱向位移。shadowOffset的默認值是{0,-3}表示相對於Y軸有3個點的向上位移。
shadowRadius(控制陰影的模糊度),當它的值是0的時候,陰影就和視圖同樣有一個很是肯定的邊界線。當值愈來愈大的時候邊界線看上去就會愈來愈模糊和天然。蘋果自家的應用設計更偏向於天然的陰影,因此一個非零值再合適不過了
※陰影裁剪
圖層的陰影繼承自內容的外形,而不是根據邊界和角半徑來肯定。爲了計算出陰影的形狀,CoreAnimation會將寄宿圖(包括子視圖)考慮在內,而後經過這些完美搭配圖層形狀而建立一個陰影
※shadowPath屬性
shadowPath是一個CGPathRef類型,能夠經過這個屬性單獨於圖層形狀以外指定陰影的形狀。
※圖層蒙版
CALayer有一個屬性叫作mask,就像是一個餅乾切割機,mask圖層實心的部分會被保留下來,其餘則會被拋棄,若是mask圖層比父圖層要小,只有在mask圖層裏面的內容纔是它關心的,除此之外的一切都會被隱藏起來。CALayer蒙版圖層不侷限與靜態圖,任何有圖層構成的均可以做爲msk屬性,這意味着你的蒙版能夠經過代碼甚至是動畫實時生成。下面是把圖片和蒙版圖層做用在一塊兒的效果:
//示例
//create mask layer CALayer *maskLayer = [CALayer layer]; maskLayer.frame = self.imageView.bounds; UIImage *maskImage = [UIImage imageNamed:@"Cone.png"]; maskLayer.contents = (__bridge id)maskImage.CGImage; //apply mask to image layer self.imageView.layer.mask = maskLayer;
※拉伸過濾
當視圖顯示一個圖片的時候,都應該正確的顯示這個圖片(意既:以正確的比例和正確的1:1像素顯示在屏幕上)。緣由以下:可以更好的顯示最好的畫質,像素既沒有被壓縮,也沒有被拉伸;能更好的使用內存,由於這就是全部你要存儲的東西;最好的性能表現,CPU不須要爲此額外的計算。
不過有時候,顯示一個非真實大小的圖片確實是咱們須要的效果。好比說一個頭像或是圖片的縮略圖,再好比說一個能夠被拖拽和伸縮的大圖。當圖片須要顯示不一樣大小的時候,有一種叫作拉伸過濾的算法就起到做用了。它做用於原圖的像素上並根據須要生成新的像素顯示在屏幕上。
minification(縮小圖片)和magnification(放大圖片)默認過濾器都是kCAFiliterLinear(雙線性濾波算法)經過對多個像素取樣最終生成新的值,獲得一個平滑的表現不錯的拉伸圖。可是當放大倍數比較大的時候圖片就模糊不清了。
kCAFilterTrilinear(三線性濾波算法)和kCAFiliterLinear很是類似,大部分狀況下兩者都看不出來有什麼差異。可是三線性濾波算法存儲了多個大小狀況下的圖片(也叫多重貼圖),並三維取樣,同事結合大圖和小圖的存儲進而獲得最後的結果。這個方法的好處在於算法可以從一系列已經接近於最終大小的圖片中獲得想要的結果,也就是說不要對不少像素同步取樣。這不只提升了性能,也避免了小几率因舍入錯誤引發的取樣失靈的問題。下面對於大圖來講,雙線性濾波和三線性濾波表現的更出色:
kCAFilterNearest(最近過濾)是一種比較武斷的方法,就是取最近的單像素點而無論其餘的顏色。這樣作很是快,也不會使圖片模糊,可是最明顯的效果就是會使壓縮圖片更糟,圖片放大以後也顯得塊狀或是馬賽克嚴重。下面對於沒有斜線的小圖來講,最近過濾算法要好不少:
總的來講,線性過濾保留了形狀,最近過濾則保留了像素的差別。
※組透明
UIView有一個叫作alpha的屬性來肯定視圖的透明度。CALayer有一個等同的屬性叫作opacity,這兩個屬性都是影響子層級的。當你顯示一個50%透明度的圖層時,圖層的每一個像素都會通常顯示本身的顏色,另外一半顯示圖層下面的顏色,這是正常的透明度表現。可是若是圖層包含一個一樣的50%透明的子圖層時,這時所看到視圖50%來自子視圖,25%來自圖層自己的顏色,另外的25%則來自背景色。
通常狀況下,設置了一個圖層的透明度,會但願它包含的整個圖層樹像一個總體同樣的透明效果,能夠經過設置CALayer的shouldRasterize屬性來實現組透明的效果,若是它被設置爲YES,在應用透明度自前,圖層及其子圖層都會被整合成一個總體的圖片,這樣就沒有透明度混合的問題了。爲了啓用shouldRasterize屬性,設置了圖層的rasterizationScale屬性,默認狀況下,全部圖層拉伸都是1.0,須要確保設置了rasterizationScale屬性去匹配屏幕,以防止出現Retina屏幕像素化的問題。
//實例
//ClockView.m
// // ClockView.m // CoreAnimationLearn // // Created by Vie on 2017/6/26. // Copyright © 2017年 Vie. All rights reserved. // #import "ClockView.h" @interface ClockView() @property (nonatomic, strong) UIView *time1View; @property (nonatomic, strong) UIView *time2View; @property (nonatomic, strong) UIView *time3View; @property (nonatomic, strong) UIView *time4View; @property (nonatomic, strong) UIView *time5View; @property (nonatomic, strong) UIView *time6View; @property (nonatomic, weak) NSTimer *timer; @end @implementation ClockView #pragma mark lazy loading -(UIView *)time1View{ if (!_time1View) { _time1View=[[UIView alloc] init]; [_time1View setBackgroundColor:RGB_ALPHA_COLOR(255, 255, 255, 1)]; _time1View.layer.contents=(__bridge id)([UIImage imageNamed:@"7.jpg"].CGImage); _time1View.layer.magnificationFilter = kCAFilterNearest;//最近鄰插值過濾器 } return _time1View; } -(UIView *)time2View{ if (!_time2View) { _time2View=[[UIView alloc] init]; [_time2View setBackgroundColor:RGB_ALPHA_COLOR(255, 255, 255, 1)]; _time2View.layer.contents=(__bridge id)([UIImage imageNamed:@"7.jpg"].CGImage); _time2View.layer.magnificationFilter = kCAFilterNearest;//最近鄰插值過濾器 } return _time2View; } -(UIView *)time3View{ if (!_time3View) { _time3View=[[UIView alloc] init]; [_time3View setBackgroundColor:RGB_ALPHA_COLOR(255, 255, 255, 1)]; _time3View.layer.contents=(__bridge id)([UIImage imageNamed:@"7.jpg"].CGImage); _time3View.layer.magnificationFilter = kCAFilterNearest;//最近鄰插值過濾器 } return _time3View; } -(UIView *)time4View{ if (!_time4View) { _time4View=[[UIView alloc] init]; [_time4View setBackgroundColor:RGB_ALPHA_COLOR(255, 255, 255, 1)]; _time4View.layer.contents=(__bridge id)([UIImage imageNamed:@"7.jpg"].CGImage); _time4View.layer.magnificationFilter = kCAFilterNearest;//最近鄰插值過濾器 } return _time4View; } -(UIView *)time5View{ if (!_time5View) { _time5View=[[UIView alloc] init]; [_time5View setBackgroundColor:RGB_ALPHA_COLOR(255, 255, 255, 1)]; _time5View.layer.contents=(__bridge id)([UIImage imageNamed:@"7.jpg"].CGImage); _time5View.layer.magnificationFilter = kCAFilterNearest;//最近鄰插值過濾器 } return _time5View; } -(UIView *)time6View{ if (!_time6View) { _time6View=[[UIView alloc] init]; [_time6View setBackgroundColor:RGB_ALPHA_COLOR(255, 255, 255, 1)]; _time6View.layer.contents=(__bridge id)([UIImage imageNamed:@"7.jpg"].CGImage); _time6View.layer.magnificationFilter = kCAFilterNearest;//最近鄰插值過濾器 } return _time6View; } #pragma mark 頁面回調 -(instancetype)init{ self=[super init]; if (self) { } return self; } #pragma mark 頁面加載 -(void)drawRect:(CGRect)rect{ //時間視圖 [self addSubview:self.time1View]; [self.time1View mas_makeConstraints:^(MASConstraintMaker *make) { make.width.equalTo(self.mas_width).multipliedBy(0.167); make.height.equalTo(self.time1View.mas_width).multipliedBy(1.312); make.bottom.mas_equalTo(0); make.left.mas_equalTo(0); }]; [self addSubview:self.time2View]; [self.time2View mas_makeConstraints:^(MASConstraintMaker *make) { make.width.height.equalTo(self.time1View); make.bottom.mas_equalTo(0); make.left.equalTo(self.time1View.mas_right); }]; [self addSubview:self.time3View]; [self.time3View mas_makeConstraints:^(MASConstraintMaker *make) { make.width.height.equalTo(self.time1View); make.bottom.mas_equalTo(0); make.left.mas_equalTo(self.time2View.mas_right); }]; [self addSubview:self.time4View]; [self.time4View mas_makeConstraints:^(MASConstraintMaker *make) { make.width.height.equalTo(self.time1View); make.bottom.mas_equalTo(0); make.left.equalTo(self.time3View.mas_right); }]; [self addSubview:self.time5View]; [self.time5View mas_makeConstraints:^(MASConstraintMaker *make) { make.width.height.equalTo(self.time1View); make.bottom.mas_equalTo(0); make.left.equalTo(self.time4View.mas_right); }]; [self addSubview:self.time6View]; [self.time6View mas_makeConstraints:^(MASConstraintMaker *make) { make.width.height.equalTo(self.time1View); make.bottom.mas_equalTo(0); make.left.equalTo(self.time5View.mas_right); }]; self.timer=[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES]; [self timerAction]; } #pragma mark Action -(void)timerAction{ NSDate *dateNow=[NSDate date]; NSString *string= [NSString stringWithFormat:@"進入定時器,時間:%@",dateNow]; CUSTOM_LOG(string); //將時間轉換爲小時,分鐘和秒 NSCalendar *calendar=[NSCalendar currentCalendar]; NSUInteger units =NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay|NSCalendarUnitHour|NSCalendarUnitMinute|NSCalendarUnitSecond; NSDateComponents *components=[calendar components:units fromDate:dateNow]; //設置時 [self setDigit:components.hour/10 forView:self.time1View]; [self setDigit:components.hour%10 forView:self.time2View]; //設置分 [self setDigit:components.minute/10 forView:self.time3View]; [self setDigit:components.minute%10 forView:self.time4View]; //設置秒 [self setDigit:components.second/10 forView:self.time5View]; [self setDigit:components.second%10 forView:self.time6View]; } #pragma mark 其餘方法 -(void)setDigit:(NSInteger)digit forView:(UIView *)view{ view.layer.contentsRect=CGRectMake(digit*0.1, 0, 0.1, 1.0f); } @end
//圖片
//VisualEffectViewController.m
// // VisualEffectViewController.m // CoreAnimationLearn // 視覺效果 // Created by Vie on 2017/6/23. // Copyright © 2017年 Vie. All rights reserved. // #import "VisualEffectViewController.h" #import "ClockView.h" @interface VisualEffectViewController () @property (nonatomic, strong) UIView *whiteView; @property (nonatomic, strong) UIView *redView; @property (nonatomic, strong) UIView *whiteOtherView; @property (nonatomic, strong) UIView *redOtherView; @property (nonatomic, strong) UIView *clockView; @property (nonatomic, strong) UIButton *btn; @end @implementation VisualEffectViewController #pragma mark lazy loading -(UIView *)whiteView{ if (!_whiteView) { _whiteView=[[UIView alloc] init]; [_whiteView setBackgroundColor:RGB_ALPHA_COLOR(255, 255, 255, 1)]; } return _whiteView; } -(UIView *)redView{ if (!_redView) { _redView=[[UIView alloc] init]; [_redView setBackgroundColor:RGB_ALPHA_COLOR(215, 0, 18, 1)]; } return _redView; } -(UIView *)whiteOtherView{ if (!_whiteOtherView) { _whiteOtherView=[[UIView alloc] init]; [_whiteOtherView setBackgroundColor:RGB_ALPHA_COLOR(255, 255, 255, 1)]; } return _whiteOtherView; } -(UIView *)redOtherView{ if (!_redOtherView) { _redOtherView=[[UIView alloc] init]; [_redOtherView setBackgroundColor:RGB_ALPHA_COLOR(234, 65, 134, 1)]; } return _redOtherView; } -(UIView *)clockView{ if (!_clockView) { _clockView=[[ClockView alloc] init]; } return _clockView; } -(UIButton *)btn{ if (!_btn) { _btn=[[UIButton alloc] init]; [_btn setBackgroundColor:RGB_ALPHA_COLOR(255, 255, 255, 1)]; _btn.layer.cornerRadius = 10; } return _btn; } #pragma mark 視圖控制器回調 - (void)viewDidLoad { [super viewDidLoad]; [self.view setBackgroundColor:RGB_ALPHA_COLOR(230, 230, 230, 1)]; UIBarButtonItem *leftItem=[[UIBarButtonItem alloc] initWithTitle:@"<" style:UIBarButtonItemStylePlain target:self action:@selector(backAction:)]; self.navigationItem.leftBarButtonItem=leftItem; //讓約束從導航欄底部開始算起 self.edgesForExtendedLayout = UIRectEdgeNone; self.extendedLayoutIncludesOpaqueBars = NO; self.modalPresentationCapturesStatusBarAppearance = NO; [super viewDidLoad]; //第一個白色背景視圖 [self.view addSubview:self.whiteView]; [self.whiteView mas_makeConstraints:^(MASConstraintMaker *make) { make.width.height.mas_equalTo(150); make.centerX.mas_equalTo(self.view.mas_centerX); make.top.mas_equalTo(20); }]; //第一個紅色視圖 [self.whiteView addSubview:self.redView]; [self.redView mas_makeConstraints:^(MASConstraintMaker *make) { make.width.height.mas_equalTo(self.whiteView).multipliedBy(0.5); make.top.left.mas_equalTo(-25); }]; //第二個白色背景視圖 [self.view addSubview:self.whiteOtherView]; [self.whiteOtherView mas_makeConstraints:^(MASConstraintMaker *make) { make.width.height.mas_equalTo(150); make.centerX.mas_equalTo(self.view.mas_centerX); make.top.mas_equalTo(225); }]; //第二個紅色視圖 [self.whiteOtherView addSubview:self.redOtherView]; [self.redOtherView mas_makeConstraints:^(MASConstraintMaker *make) { make.width.height.mas_equalTo(self.whiteOtherView).multipliedBy(0.5); make.top.left.mas_equalTo(-25); }]; //設置圓角半徑對咱們的層 self.whiteView.layer.cornerRadius=20.0f; self.whiteOtherView.layer.cornerRadius=20.0f; //啓用裁剪邊框 self.whiteView.layer.masksToBounds=YES; //設置邊框大小 self.whiteView.layer.borderWidth=5.0f; self.whiteOtherView.layer.borderWidth=5.0f; //設置陰影(和圖層邊框不一樣,圖層的陰影繼承自內容的外形,而不是根據邊界和角半徑來肯定。爲了計算出陰影的形狀,Core Animation會將寄宿圖(包括子視圖,若是有的話)考慮在內,而後經過這些來完美搭配圖層形狀從而建立一個陰影) self.whiteView.layer.shadowOpacity = 0.5f;//陰影透明度0~1之間 self.whiteView.layer.shadowOffset=CGSizeMake(0, 5);//陰影方向和距離 self.whiteView.layer.shadowRadius=5.0f;//陰影模糊度 self.whiteOtherView.layer.shadowOpacity = 0.5f;//陰影透明度0~1之間 self.whiteOtherView.layer.shadowOffset=CGSizeMake(0, 5);//陰影方向和距離 self.whiteOtherView.layer.shadowRadius=5.0f;//陰影模糊度 // self.whiteView.layer.shadowRadius=5.0f;//陰影模糊度 // CGMutablePathRef path=CGPathCreateMutable(); // CGPathAddEllipseInRect(path, NULL, self.whiteOtherView.bounds); // self.whiteOtherView.layer.shadowPath=path; // CGPathRelease(path); //按鈕btn [self.view addSubview:self.btn]; [self.btn mas_makeConstraints:^(MASConstraintMaker *make) { make.width.mas_equalTo(100); make.height.mas_equalTo(40); make.centerX.mas_equalTo(self.view); make.top.equalTo(self.whiteOtherView.mas_bottom); }]; UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(20, 0, 60, 40)]; label.text = @"Hello"; label.textAlignment = NSTextAlignmentCenter; [self.btn addSubview:label]; self.btn.alpha=0.5; //時間視圖 [self.view addSubview:self.clockView]; [self.clockView mas_makeConstraints:^(MASConstraintMaker *make) { make.width.mas_equalTo(self.view.mas_width); make.height.mas_equalTo(self.clockView.mas_width).multipliedBy(0.1313); make.left.mas_equalTo(0); make.bottom.mas_equalTo(0); } ]; //使用shouldRasterize屬性解決組透明問題 self.whiteOtherView.layer.shouldRasterize = YES; self.whiteOtherView.layer.rasterizationScale = [UIScreen mainScreen].scale; } #pragma mark Action -(void)backAction:(id)sender{ [self.navigationController popViewControllerAnimated:YES]; } #pragma mark 其餘方法 @end
運行效果
※仿射變換
UIView的transform屬性是一個CGAffineTransform類型,用於在二維空間作旋轉,縮放和平移。CGAffineTransform是一個能夠和二維空間向量(例如CGPoint)作乘法的3x2的矩陣。用CGPoint的每一列和CGAffineTransform矩陣的每一行對應元素相乘再求和,就造成了一個新的CGPoint類型的結果。要解釋一下圖中顯示的灰色元素,爲了能讓矩陣作乘法,左邊矩陣的列數必定要和右邊矩陣的行數個數相同,因此要給矩陣填充一些標誌值,使得既可讓矩陣作乘法,又不改變運算結果,而且不必存儲這些添加的值,由於它們的值不會發生變化,可是要用來作運算。所以,一般會用3×3(而不是2×3)的矩陣來作二維變換,你可能會見到3行2列格式的矩陣,這是所謂的以列爲主的格式。下面是矩陣表示圖:
CGAffineTransform中「仿射」的意思是不管變換矩陣用什麼值,圖層中平行的兩條線在變換以後任然保持平行,下面是展現圖:
※建立一個CGAffineTransform
下面幾個函數都建立了一個CGAffineTransform實例:
CGAffineTransformMakeRotation(CGFloat angle) //旋轉變換 CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)//縮放變換 CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)//平移變換
UIView能夠經過設置transform屬性作變換,實際上它只是封裝了內部圖層的變換,CALayer一樣也有一個transform屬性,但它的類型是CATransform。CALayer對應於UIView的transform屬性叫作affineTransform。
//對圖層旋轉45度
//旋轉圖層45度,弧度經常使用數學常量pi的倍數表示,一個pi表明180度,四分之一的pi就是45度 CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4); self.layerView.layer.affineTransform = transform;
※混合變換
CoreGraphics提供了一系列的函數能夠在一個變換的基礎上作更深層次的變換,好比作一個既要縮放又要旋轉的變換,例以下面幾個函數
CGAffineTransformRotate(CGAffineTransform t, CGFloat angle)
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy)
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)
初始生成一個什麼都不作的變換CGAffineTransform類型的空值,矩陣論中稱做單位矩陣,CoreGraphics一樣也提供了一個方便的常量:
CGAffineTransformIdentity
若是須要混合兩個已經存在的變換矩陣,就能夠使用下面方法,在兩個變換矩陣的基礎上建立一個新的變換:
CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);
//下面函數建立一個複雜的變換,變換的順序會影響最終的結果,也就是說旋轉以後的平移和平移以後的旋轉結果可能不一樣。
//圖片向右邊發生了平移,但並無指定距離那麼遠(200像素),另外它還有點向下發生了平移。緣由在於當你按順序作了變換,上一個變換的結果將會影響以後的變換,因此200像素的向右平移一樣也被旋轉了30度,縮小了50%,因此它其實是斜向移動了100像素。 CGAffineTransform transform=CGAffineTransformIdentity;//建立一個新的變換 transform=CGAffineTransformScale(transform, 0.5, 0.5);//縮小50% transform=CGAffineTransformRotate(transform, M_PI/180.0*30.0);//順時針旋轉30° transform=CGAffineTransformTranslate(transform, 200, 0);//平移200個位置 self.imgView.layer.affineTransform=transform;//轉換用在圖層上
※剪切變換
斜切變換是仿射變換的第四種類型,較於平移,旋轉和縮放並不經常使用(這也是CoreGraphics沒有提供相應函數的緣由),用「傾斜」描述更加恰當
//實現一個斜切變換
//斜切變換 CGAffineTransform transform=CGAffineTransformIdentity; transform.c=-1; transform.b=0; self.imgView.layer.affineTransform=transform;
※3D變換
transform屬性(CATransform3D類型),可讓圖層在3D空間內移動或者旋轉,CATransform3D是一個能夠在3緯空間內作變換的4x4的矩陣,下面是矩陣表示圖:
CoreAnimation提供了一系列的方法建立和組合CATransform3D類型的矩陣,和CoreGraphics的函數相似,3D的仿射變換多了一個z參數。
CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z)
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz)
CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)
X軸以右爲正方形,Y軸如下爲正方形,Z軸和這兩個軸分別垂直,指向視角外爲正方向,繞Z軸的旋轉等同於以前二維空間的仿射旋轉,可是繞X軸和Y軸的旋轉就突破了屏幕的二維空間,而且在用戶視角看來發生了傾斜。下面是X,Y,Z軸,以及圍繞它們旋轉的方向:
//繞Y軸旋轉圖層
//沿Y軸旋轉45度,看起來圖層並無被旋轉,只是看起來更窄,是由於咱們在用一個斜向的視角看它,而不是透視 CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0); self.layerView.layer.transform = transform;
※透視投影
在真實世界中,當物體遠離咱們的時候,因爲視角的緣由看起來會變小,理論上說遠離咱們的視圖的邊要比靠近視角的邊更短,但實際上並無發生,而咱們當前的視角是等距離的,也就是在3D變換中任然保持平行,和以前的仿射變換相似。爲了作一些修正,須要引入投影變換(又稱做z變換)來對除了旋轉以外的變換矩陣作一些修改,經過CATransform3D修改矩陣值,達到透視效果經過矩陣中一個很簡單的元素來控制:m34。用於按比例縮放X和Y的值來計算到底要視角多遠。m34的默認值是0,能夠經過設置M34爲-1.0/d來應用透視效果,d表明了想象中視角相機和屏幕之間的距離,以像素爲單位,那應該如何計算這個距離呢?實際上並不須要,大概估算一個就行了(由於視角相機實際上並不存在,因此能夠根據屏幕上的顯示效果自由決定它的放置的位置。一般500-1000就已經很好了,可是對於特定的圖層有時候更小或者更大的值會看起來更舒服,減小距離的值會增長透視效果,因此一個很是微小的值會讓它看起來更加失真,然而一個很是大的值會讓它基本失去透視效果)。
//3d變換;CATransform3D的m34元素,用來作透視m34的默認值是0,咱們能夠經過設置m34爲-1.0 / d來應用透視效果 CATransform3D transform=CATransform3DIdentity; transform.m34=-1.0/500.0; transform=CATransform3DRotate(transform, M_PI_4, 0, 1, 0);//Y軸旋轉45° self.imgView.layer.transform=transform;//變換應用到圖層
※滅點
當在透視角度繪圖的時候,遠離相機視角的物體將會變小變遠,當遠離到一個極限距離,它們就可能縮成了一個點,因而全部的物體最後都匯聚消失在同一個點,這個點一般是視圖的中心,Core Animation定義了這個點位於變換圖層的anchorPoint。這就是說,當圖層發生變換時,這個點永遠位於圖層變換以前anchorPoint的位置。當改變一個圖層的position,你也改變了它的滅點,作3D變換的時候要時刻記住這一點,當你視圖經過調整m34來讓它更加有3D效果,應該首先把它放置於屏幕中央,而後經過平移來把它移動到指定位置(而不是直接改變它的position),這樣全部的3D圖層都共享一個滅點。下圖建立擬真效果的透視:
※sublayerTransform屬性
若是有多個視圖或者圖層,每一個都作3D變換,那就須要分別設置相同的m34只,而且確保在變換以前都在屏幕中央共享一個position,有更好的方法:CALayer有一個屬性叫作sublayerTransform,它是CATransform3D類型,但和對一個圖層的變換不一樣,它影響到全部的子圖層。表示能夠一次性對包含這些圖層的容器作變換,全部的子圖層都自動繼承了這個變換方法。
//子層一塊兒變換 CATransform3D perspective = CATransform3DIdentity; perspective.m34 = -1.0 / 500.0; perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0); perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0); self.view.layer.sublayerTransform = perspective;
※背面
圖層是雙面繪製的,反面顯示的是正面的一個鏡像圖片,但這並非一個很好的特性,由於若是圖層包含文本或者其餘控件,那用戶看到這些內容的鏡像圖片固然會感到困惑,另外也有可能形成資源的浪費(想象用這些圖層造成一個不透明的固態立方體,既然永遠都看不見這些圖層的背面,那爲何浪費GPU來繪製它們呢?)。
CALayer有一個叫作doubleSided的屬性來控制圖層背面是否要被繪製。這是一個BOOL類型,默認爲YES,若是設置爲NO,那麼當圖層正面從相機視角消失的時候,它將不會被繪製。
//圖層是雙面繪製的,反面顯示的是正面的一個鏡像圖片。但這並非一個很好的特性,由於若是圖層包含文本或者其餘控件,那用戶看到這些內容的鏡像圖片固然會感到困惑。另外也有可能形成資源的浪費:想象用這些圖層造成一個不透明的固態立方體,既然永遠都看不見這些圖層的背面,那爲何浪費GPU來繪製它們呢? //doubleSided的屬性來控制圖層的背面是否要被繪製 self.imgView.layer.doubleSided=NO;
※扁平化圖層
對包含已經作過變換的圖層的圖層作反方向的變換:二維圖層會恢復原狀,3D變換不會恢復原狀(
)。※固體對象
能夠用3D空間建立一個固態的3D對象(其實是一個技術上所謂的空洞對象,但它以固態呈現),這裏用六個獨立的視圖來構建一個立方體的各個面。
※光亮和陰影
CoreAnimation能夠用3D顯示圖層,可是它對光線並無概念。若是須要動態的建立光線效果,能夠根據每一個視圖的方向應用不一樣的alpha值作出半透明的陰影圖層,但爲了計算陰影圖層的不透明度,你須要獲得每一個面的正太向量(垂直於表面的向量),而後根據一個想象的光源計算出兩個向量叉乘結果。叉乘表明了光源和圖層之間的角度,從而決定了它有多大程度上的光亮。
這裏用GLKit框架來作向量的計算,每一個面的CATransform3D都被裝換成GLMatrix4,而後經過GLMatrix4GetMatrix3函數得出一個3x3的旋轉矩陣,這個旋轉矩陣指定了圖層的方向,而後能夠用它來獲得正太向量的值。
※點擊事件
這裏點擊第三個表面頂部看見的按鈕,什麼都沒有發生,由於點擊事件的處理有視圖在父視圖中的順序決定的,並非3D空間中Z軸順序,因此這裏按照視圖/圖層順序來講,4,5,6在3的前面這就和普通的2D佈局在按鈕上覆蓋物體同樣。你也許認爲把doubleSided設置成NO能夠解決這個問題,由於它再也不渲染視圖後面的內容,但實際上並不起做用。由於背對相機而隱藏的視圖仍然會響應點擊事件(這和經過設置hidden屬性或者設置alpha爲0而隱藏的視圖不一樣,那兩種方式將不會響應事件)。因此即便禁止了雙面渲染仍然不能解決這個問題(雖然因爲性能問題,仍是須要把它設置成NO)。這裏有幾種正確的方案:把除了表面3的其餘視圖userInteractionEnabled屬性都設置成NO來禁止事件傳遞。或者簡單經過代碼把視圖3覆蓋在視圖6上。不管怎樣均可以點擊按鈕了
//示例
// // TransformViewController.m // CoreAnimationLearn // 變換 // Created by Vie on 2017/6/26. // Copyright © 2017年 Vie. All rights reserved. // #import "TransformViewController.h" #import <GLKit/GLKit.h> #define LIGHT_DIRECTION 0, 1, -0.5 #define AMBIENT_LIGHT 0.5 @interface TransformViewController () @property (nonatomic, strong) UIImageView *imgView; @property (nonatomic, strong) UIView *cube1View; @property (nonatomic, strong) UIView *cube2View; @property (nonatomic, strong) UIView *cube3View; @property (nonatomic, strong) UIView *cube4View; @property (nonatomic, strong) UIView *cube5View; @property (nonatomic, strong) UIView *cube6View; @end @implementation TransformViewController #pragma mark lazy loading -(UIImageView *)imgView{ if (!_imgView) { _imgView=[[UIImageView alloc] init]; _imgView.image=[UIImage imageNamed:@"3.jpg"]; } return _imgView; } -(UIView *)cube1View{ if (!_cube1View) { _cube1View=[[UIView alloc] init]; _cube1View.layer.contents=(__bridge id)([UIImage imageNamed:@"7.jpg"].CGImage); _cube1View.layer.magnificationFilter = kCAFilterNearest;//最近鄰插值過濾器 _cube1View.layer.contentsRect=CGRectMake(1*0.1, 0, 0.1, 1.0f); } return _cube1View; } -(UIView *)cube2View{ if (!_cube2View) { _cube2View=[[UIView alloc] init]; _cube2View.layer.contents=(__bridge id)([UIImage imageNamed:@"7.jpg"].CGImage); _cube2View.layer.magnificationFilter = kCAFilterNearest;//最近鄰插值過濾器 _cube2View.layer.contentsRect=CGRectMake(2*0.1, 0, 0.1, 1.0f); } return _cube2View; } -(UIView *)cube3View{ if (!_cube3View) { _cube3View=[[UIView alloc] init]; _cube3View.layer.contents=(__bridge id)([UIImage imageNamed:@"7.jpg"].CGImage); _cube3View.layer.magnificationFilter = kCAFilterNearest;//最近鄰插值過濾器 _cube3View.layer.contentsRect=CGRectMake(3*0.1, 0, 0.1, 1.0f); } return _cube3View; } -(UIView *)cube4View{ if (!_cube4View) { _cube4View=[[UIView alloc] init]; _cube4View.layer.contents=(__bridge id)([UIImage imageNamed:@"7.jpg"].CGImage); _cube4View.layer.magnificationFilter = kCAFilterNearest;//最近鄰插值過濾器 _cube4View.layer.contentsRect=CGRectMake(4*0.1, 0, 0.1, 1.0f); } return _cube4View; } -(UIView *)cube5View{ if (!_cube5View) { _cube5View=[[UIView alloc] init]; _cube5View.layer.contents=(__bridge id)([UIImage imageNamed:@"7.jpg"].CGImage); _cube5View.layer.magnificationFilter = kCAFilterNearest;//最近鄰插值過濾器 _cube5View.layer.contentsRect=CGRectMake(5*0.1, 0, 0.1, 1.0f); } return _cube5View; } -(UIView *)cube6View{ if (!_cube6View) { _cube6View=[[UIView alloc] init]; _cube6View.layer.contents=(__bridge id)([UIImage imageNamed:@"7.jpg"].CGImage); _cube6View.layer.magnificationFilter = kCAFilterNearest;//最近鄰插值過濾器 _cube6View.layer.contentsRect=CGRectMake(6*0.1, 0, 0.1, 1.0f); } return _cube6View; } #pragma mark 視圖控制器回調 - (void)viewDidLoad { [super viewDidLoad]; [self.view setBackgroundColor:RGB_ALPHA_COLOR(230, 230, 230, 1)]; UIBarButtonItem *leftItem=[[UIBarButtonItem alloc] initWithTitle:@"<" style:UIBarButtonItemStylePlain target:self action:@selector(backAction:)]; self.navigationItem.leftBarButtonItem=leftItem; //讓約束從導航欄底部開始算起 self.edgesForExtendedLayout = UIRectEdgeNone; self.extendedLayoutIncludesOpaqueBars = NO; self.modalPresentationCapturesStatusBarAppearance = NO; //圖片視圖 [self.view addSubview:self.imgView]; [self.imgView mas_makeConstraints:^(MASConstraintMaker *make) { make.width.height.mas_equalTo(100); make.centerX.equalTo(self.view); make.top.mas_equalTo(20); }]; // //將視圖旋轉45度 // self.imgView.layer.affineTransform=CGAffineTransformMakeRotation(M_PI_4); // //圖片向右邊發生了平移,但並無指定距離那麼遠(200像素),另外它還有點向下發生了平移。緣由在於當你按順序作了變換,上一個變換的結果將會影響以後的變換,因此200像素的向右平移一樣也被旋轉了30度,縮小了50%,因此它其實是斜向移動了100像素。 // CGAffineTransform transform=CGAffineTransformIdentity;//建立一個新的變換 // transform=CGAffineTransformScale(transform, 0.5, 0.5);//縮小50% // transform=CGAffineTransformRotate(transform, M_PI/180.0*30.0);//順時針旋轉30° // transform=CGAffineTransformTranslate(transform, 200, 0);//平移200個位置 // self.imgView.layer.affineTransform=transform;//轉換用在圖層上 // //斜切變換 // CGAffineTransform transform=CGAffineTransformIdentity; // transform.c=-1; // transform.b=0; // self.imgView.layer.affineTransform=transform; //3d變換;CATransform3D的m34元素,用來作透視m34的默認值是0,咱們能夠經過設置m34爲-1.0 / d來應用透視效果 CATransform3D transform=CATransform3DIdentity; transform.m34=-1.0/500.0; transform=CATransform3DRotate(transform, M_PI_4, 0, 1, 0);//Y軸旋轉45° self.imgView.layer.transform=transform;//變換應用到圖層 //圖層是雙面繪製的,反面顯示的是正面的一個鏡像圖片。但這並非一個很好的特性,由於若是圖層包含文本或者其餘控件,那用戶看到這些內容的鏡像圖片固然會感到困惑。另外也有可能形成資源的浪費:想象用這些圖層造成一個不透明的固態立方體,既然永遠都看不見這些圖層的背面,那爲何浪費GPU來繪製它們呢? //doubleSided的屬性來控制圖層的背面是否要被繪製 self.imgView.layer.doubleSided=NO; [self loadCubeView]; } -(void)loadCubeView{ //先添加視圖 [self.view addSubview:self.cube1View]; [self.view addSubview:self.cube2View]; [self.view addSubview:self.cube3View]; [self.view addSubview:self.cube4View]; [self.view addSubview:self.cube5View]; [self.view addSubview:self.cube6View]; [self.cube1View mas_makeConstraints:^(MASConstraintMaker *make) { make.width.height.mas_equalTo(200); make.center.equalTo(self.view); }]; [self.cube2View mas_makeConstraints:^(MASConstraintMaker *make) { make.width.height.mas_equalTo(200); make.center.equalTo(self.view); }]; [self.cube3View mas_makeConstraints:^(MASConstraintMaker *make) { make.width.height.mas_equalTo(200); make.center.equalTo(self.view); }]; [self.cube4View mas_makeConstraints:^(MASConstraintMaker *make) { make.width.height.mas_equalTo(200); make.center.equalTo(self.view); }]; [self.cube5View mas_makeConstraints:^(MASConstraintMaker *make) { make.width.height.mas_equalTo(200); make.center.equalTo(self.view); }]; [self.cube6View mas_makeConstraints:^(MASConstraintMaker *make) { make.width.height.mas_equalTo(200); make.center.equalTo(self.view); }]; //仿射變換 CATransform3D transform = CATransform3DMakeTranslation(0, 0, 100); self.cube1View.layer.transform = transform; transform=CATransform3DMakeTranslation(100, 0, 0); transform=CATransform3DRotate(transform, M_PI_2, 0, 1, 0); self.cube2View.layer.transform=transform; transform=CATransform3DMakeTranslation(0 , -100, 0); transform=CATransform3DRotate(transform, M_PI_2, 1, 0, 0); self.cube3View.layer.transform=transform; transform=CATransform3DMakeTranslation(0 , 100, 0); transform=CATransform3DRotate(transform, -M_PI_2, 1, 0, 0); self.cube4View.layer.transform=transform; transform=CATransform3DMakeTranslation(-100 , 0, 0); transform=CATransform3DRotate(transform, -M_PI_2, 0, 1, 0); self.cube5View.layer.transform=transform; transform=CATransform3DMakeTranslation(0 , 0, -100); transform=CATransform3DRotate(transform, M_PI, 0, 1, 0); self.cube6View.layer.transform=transform; //子層一塊兒變換 CATransform3D perspective = CATransform3DIdentity; perspective.m34 = -1.0 / 500.0; perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0); perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0); self.view.layer.sublayerTransform = perspective; } -(void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:animated]; //告知頁面佈局馬上更新,避免下面方法中獲取的bounds爲0 [self.view layoutIfNeeded]; [self applyLightingToCubeFace:self.cube1View.layer]; [self applyLightingToCubeFace:self.cube2View.layer]; [self applyLightingToCubeFace:self.cube3View.layer]; [self applyLightingToCubeFace:self.cube4View.layer]; [self applyLightingToCubeFace:self.cube5View.layer]; [self applyLightingToCubeFace:self.cube6View.layer]; } -(void)viewDidAppear:(BOOL)animated{ [super viewDidAppear:animated]; //對於視圖或者圖層來講,frame並非一個很是清晰的屬性,它實際上是一個虛擬屬性,是根據bounds,position和transform計算而來,因此當其中任何一個值發生改變,frame都會變化。相反,改變frame的值一樣會影響到他們當中的值,記住當對圖層作變換的時候,好比旋轉或者縮放,frame實際上表明瞭覆蓋在圖層旋轉以後的整個軸對齊的矩形區域,也就是說frame的寬高可能和bounds的寬高再也不一致了 NSString *logString=[NSString stringWithFormat:@"frameHeight:%f|boundsHeight:%f",self.imgView.frame.size.height,self.imgView.bounds.size.height]; CUSTOM_LOG(logString); } #pragma mark Action -(void)backAction:(id)sender{ [self.navigationController popViewControllerAnimated:YES]; } #pragma mark 其餘方法 //給立方體每一個面添加光照陰影效果 -(void)applyLightingToCubeFace:(CALayer *)face{ //添加照明層 CALayer *layer=[CALayer layer]; layer.frame=face.bounds; [face addSublayer:layer]; //圖層變換轉換爲矩陣 //GLKMatrix4和CATransform3D內存結構一致,但座標類型有長度區別,因此理論上應該作一次float到CGFloat的轉換 CATransform3D transform=face.transform; GLKMatrix4 matrix4=*(GLKMatrix4 *)&transform; GLKMatrix3 matrix3=GLKMatrix4GetMatrix3(matrix4); //得到圖層標準 GLKVector3 normal=GLKVector3Make(0, 0, 1); normal=GLKMatrix3MultiplyVector3(matrix3, normal); normal=GLKVector3Normalize(normal); //獲得點積與光的方向 GLKVector3 light=GLKVector3Normalize(GLKVector3Make(LIGHT_DIRECTION)); float dotProduct=GLKVector3DotProduct(light, normal); //設置照明層不透明度 CGFloat shadow=1+dotProduct-AMBIENT_LIGHT; UIColor *color=[UIColor colorWithWhite:0 alpha:shadow]; layer.backgroundColor=color.CGColor; } @end
運行效果
※CAShapeLayer
CAShapeLayer是一個經過矢量圖形而不是bitmap來繪製的圖層子類。指定諸如顏色和線寬等屬性,用CGPath來定義想要繪製的圖形,最後CAShapeLayer就會自動渲染出來了。也能夠用CoreGraphics直接向原始的CALayer的內容中繪製一個路徑,相比之下使用CAShapeLayer有幾個有點:渲染快速,CAShapeLayer使用了硬件加速,繪製同一圖形會比用CoreGraphics快不少;高效使用內存,一個CAShapeLayer不須要像普通CALayer同樣建立一個寄宿圖形,因此不管有多大,都不會佔用太多內存;不會被圖層邊界剪裁掉,一個CAShapeLayer能夠在邊界以外繪製,圖層路徑不會像在使用CoreGraphics的普通CALayer同樣被剪裁掉;不會出現像素化,給CAShapeLayer作3D變換時,它不像一個有寄宿圖的普通圖層同樣變得像素化。
※建立一個CGPath
CAShapeLayer能夠用來繪製全部可以經過CGPath來表示的形狀。下面使用UIBeizerPath幫助類建立圖層路徑,這樣不用考慮人工釋放CGPath。
//示例
//用CAShapeLayer渲染一個簡單的火柴人 -(void)createMatchPeople{ //使用UIBezierPath建立圖層路徑 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 CAShapeLayer *shapeLayer=[CAShapeLayer layer]; shapeLayer.strokeColor=[UIColor redColor].CGColor; shapeLayer.fillColor=[UIColor clearColor].CGColor; shapeLayer.lineWidth=5; shapeLayer.lineJoin=kCALineJoinRound; shapeLayer.lineCap=kCALineCapRound; shapeLayer.path=path.CGPath; [self.view.layer addSublayer:shapeLayer]; }
運行效果
※圓角
使用UIBezierPath單獨制定每一個角,繪製圓角矩形
//示例
//建立三個圓角一個直角的矩形 -(void)createRectangular{ //使用UIBezierPath建立圖層路徑 CGRect rect=CGRectMake(50, 50, 100, 100); CGSize radii=CGSizeMake(20, 20); UIRectCorner corners=UIRectCornerTopRight|UIRectCornerBottomRight|UIRectCornerBottomLeft; UIBezierPath *path=[UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:radii]; //建立CAShapeLayer CAShapeLayer *shapeLayer=[CAShapeLayer layer]; shapeLayer.strokeColor=[UIColor redColor].CGColor; shapeLayer.fillColor=[UIColor clearColor].CGColor; shapeLayer.lineWidth=5; shapeLayer.lineJoin=kCALineJoinRound; shapeLayer.lineCap=kCALineCapRound; shapeLayer.path=path.CGPath; [self.view.layer addSublayer:shapeLayer]; }
運行效果
※CATextLayer
CALayer的子類CATextLayer,它以圖層的形式包含了UILabel幾乎全部的繪製特性,而且額外提供了一些新的特性,CATextLayer(使用Core Text)也要比UILabel(經過WebKit實現繪製)渲染得快得多,
※富文本
CATextLayer支持屬性化字符串,使用NSAttributedString,NSTextArributeName實例來設置字符串屬性
//示例
//用CATextLayer來實現一個UILable -(void)createUILabel{ //建立CATextLayer CATextLayer *textLayer=[CATextLayer layer]; textLayer.frame=self.view.bounds; //設置文本屬性 textLayer.foregroundColor=[UIColor blackColor].CGColor; textLayer.alignmentMode=kCAAlignmentJustified; textLayer.wrapped=YES; //設置字體 UIFont *font=[UIFont systemFontOfSize:15]; // CFStringRef fontName=(__bridge CFStringRef)font.fontName; // CGFontRef fontRef=CGFontCreateWithFontName(fontName); // textLayer.font=fontRef; // textLayer.fontSize=font.pointSize; // CGFontRelease(fontRef); //設置文本 NSString *text=@"Lorem ipsum dolor sit amet, consectetur adipiscing \ elit. Quisque massa arcu, eleifend vel varius in, facilisis pulvinar \ leo. Nunc quis nunc at mauris pharetra condimentum ut ac neque. Nunc elementum, libero ut porttitor dictum, diam odio congue lacus, vel \ fringilla sapien diam at purus. Etiam suscipit pretium nunc sit amet \ lobortis"; //以Retina的質量來顯示文字,避免文本像素化 textLayer.contentsScale=[UIScreen mainScreen].scale; //設置富文本屬性字符串 NSMutableAttributedString *string=nil; string=[[NSMutableAttributedString alloc] initWithString:text]; //CTFont CFStringRef fontName=(__bridge CFStringRef)font.fontName; CGFloat fontSize=font.pointSize; CTFontRef fontRef=CTFontCreateWithName(fontName, fontSize, NULL); //set text attributes NSDictionary *attributes=@{(__bridge id)kCTForegroundColorAttributeName:(__bridge id)[UIColor orangeColor].CGColor, (__bridge id)kCTFontAttributeName:(__bridge id)fontRef }; [string setAttributes:attributes range:NSMakeRange(0, [text length])]; attributes=@{ (__bridge id)kCTForegroundColorAttributeName:(__bridge id)[UIColor redColor].CGColor, (__bridge id)kCTUnderlineStyleAttributeName:@(kCTUnderlineStyleSingle), (__bridge id)kCTFontAttributeName:(__bridge id)fontRef }; [string setAttributes:attributes range:NSMakeRange(6, 5)]; CFRelease(fontRef); textLayer.string=string; [self.view.layer addSublayer:textLayer]; }
運行效果
※行距和字距
因爲繪製的實現機制不一樣(Core Text和WebKit),用CATextLayer渲染和用UILabel渲染出的文本行距和子距也不是不盡相同的。兩者的差別程度(由使用的字體和字符決定)總的來講挺小的。
※UILabel的替代品
CATextLayer比UILabel有着更好的性能表現,能夠繼承UILabel,用CATextLayer做爲宿主圖層的UILabel子類重寫+layerClass方法,這樣能夠隨着視圖自動調整大小,並且也沒有冗餘的寄宿圖了,把CATextLayer做爲宿主圖層的另外一個好處就是視圖自動設置了contentsScale屬性。下面是CATextLayer的UILabel子類(能夠在一些線上的開源項目中找到):
//LayerLabel.m文件
// // LayerLabel.m // CoreAnimationLearn // 用CATextLayer做爲宿主圖層的UILabel子類,這樣就能夠隨着視圖自動調整大小並且也沒有冗餘的寄宿圖啦。 // Created by Vie on 2017/7/5. // Copyright © 2017年 Vie. All rights reserved. // #import "LayerLabel.h" @interface LayerLabel() @property (nonatomic, strong) CATextLayer *textLayer;//文本圖層 @end @implementation LayerLabel /** @author Vie,2017-07-05 重寫方法,使得在建立的時候返回一個不一樣的圖層子類 @return 圖層類 */ +(Class)layerClass{ //這使得咱們的標籤建立一個CATextLaye,而不是常規CALayer支持層 return [CATextLayer class]; } #pragma mark lazy loading -(CATextLayer *)textLayer{ if (!_textLayer) { _textLayer=(CATextLayer *)self.layer; } return _textLayer; } #pragma mark 視圖加載 -(instancetype)initWithFrame:(CGRect)frame{ self=[super initWithFrame:frame]; if (self) { //圖層設置 self.textLayer.alignmentMode=kCAAlignmentLeft; self.textLayer.wrapped=YES; [self.layer display]; } return self; } #pragma mark 事件處理 /** @author Vie,2017-07-05 @param text 接收text並修改textLayer圖層文本 */ -(void)setText:(NSString *)text{ super.text=text; self.textLayer.string=text; } /** @author Vie,2017-07-05 @param textColor 接收textColor並修改textLayer圖層文本顏色 */ -(void)setTextColor:(UIColor *)textColor{ super.textColor=textColor; self.textLayer.foregroundColor=textColor.CGColor; } /** @author Vie,2017-07-05 @param font 接收font並修改textLayer圖層文本font */ -(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); } @end
//使用
//建立自定義的LayerLabel視圖 -(void)createLayerLabel{ LayerLabel *label=[[LayerLabel alloc] initWithFrame:self.view.bounds]; label.text=@"Lorem ipsum dolor sit amet, consectetur adipiscing \ elit. Quisque massa arcu, eleifend vel varius in, facilisis pulvinar \ leo. Nunc quis nunc at mauris pharetra condimentum ut ac neque. Nunc elementum, libero ut porttitor dictum, diam odio congue lacus, vel \ fringilla sapien diam at purus. Etiam suscipit pretium nunc sit amet \ lobortis"; label.textColor=[UIColor orangeColor]; label.font=[UIFont systemFontOfSize:18.0f]; [self.view addSubview:label]; }
運行效果
※CATransformLayer
CATransformLayer不一樣於普通的CALayer,由於它不能顯示它本身的內容,只有當存在了一個能做用於子視圖的變換它才真正存在。CATransformLayer並不平面化它的子圖層,因此它可以用於構造一個層級的3D結構
//實例
//CATransformLayer -(void)createTransformLayer{ //set up the perspective transform CATransform3D pt = CATransform3DIdentity; pt.m34 = -1.0 / 500.0; self.view.layer.sublayerTransform = pt; //set up the transform for cube 1 and add it CATransform3D c1t = CATransform3DIdentity; c1t = CATransform3DTranslate(c1t, -100, 0, 0); CALayer *cube1 = [self cubeWithTransform:c1t]; [self.view.layer addSublayer:cube1]; //set up the transform for cube 2 and add it 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.view.layer addSublayer:cube2]; } -(CALayer *)cubeWithTransform:(CATransform3D)transform{ //建立多維數據集的圖層 CATransformLayer *cube=[CATransformLayer layer]; //1 CATransform3D ct=CATransform3DMakeTranslation(0, 0, 50); [cube addSublayer:[self faceWithTransform:ct]]; //2 ct=CATransform3DMakeTranslation(50, 0, 0); ct=CATransform3DRotate(ct, M_PI_2, 0, 1, 0); [cube addSublayer:[self faceWithTransform:ct]]; //3 ct=CATransform3DMakeTranslation(0, -50, 0); ct=CATransform3DRotate(ct, M_PI_2, 1, 0, 0); [cube addSublayer:[self faceWithTransform:ct]]; //4 ct=CATransform3DMakeTranslation(0, 50, 0); ct=CATransform3DRotate(ct, -M_PI_2, 1, 0, 0); [cube addSublayer:[self faceWithTransform:ct]]; //5 ct=CATransform3DMakeTranslation(-50, 0, 0); ct=CATransform3DRotate(ct, -M_PI_2, 0, 1, 0); [cube addSublayer:[self faceWithTransform:ct]]; //6 ct=CATransform3DMakeTranslation(0, 0, -50); ct=CATransform3DRotate(ct, M_PI, 0, 1, 0); [cube addSublayer:[self faceWithTransform:ct]]; cube.position = CGPointMake(self.view.frame.size.width / 3.0, self.view.frame.size.height / 3.0); //apply the transform and return cube.transform = transform; return cube; }
運行效果
※CAGradientLayer
CAGrdientLayer是用來生成兩種或更多顏色平滑漸變的。用CoreGraphics賦值一個CAGradientLayer並將內容繪製到一個普通圖層的寄宿圖也是有可能的,可是CAGradientLayer的真正好處在於繪製使用了硬件加速。
※基礎漸變
將從一個簡單的紅變藍的對角線漸變開始。這些漸變色彩放在一個數組中,並賦給colors屬性。這個數組成員接受CGColorRef類型的值(並非從NSObject派生而來),因此要用經過bridge轉換以確保編譯正常。CAGradientLayer也有startPoint和endPoint屬性,它們決定了漸變的方向。這兩個參數是以單位座標系進行的定義,因此左上角座標是{0, 0},右下角座標是{1, 1}。
※多重漸變
colors屬性能夠包含不少顏色,默認狀況下,這些顏色在空間上均勻的被渲染,可是能夠用locations屬性來調整空間,locations屬性是一個浮點數值的數組(以NSNumber包裝)。這些浮點數定義了colors屬性中每一個不一樣顏色的爲準,也是以單位座標系進行標定。0.0表明漸變的開始,1.0表明結束。locations數組並非強制要求的,可是給它賦值了就必定要確保locations的數組大小和colors數組大小必定要相同,不然將會獲得一個空白的漸變。
//示例
//CAGradientLayer顏色漸變 -(void)createRedBlueGradient{ CAGradientLayer *gradientLayer=[CAGradientLayer layer]; gradientLayer.frame=self.view.bounds; //設置顏色 gradientLayer.colors=@[(__bridge id)[UIColor redColor].CGColor,(__bridge id)[UIColor orangeColor].CGColor,(__bridge id)[UIColor blueColor].CGColor]; gradientLayer.startPoint=CGPointMake(0, 0); //默認狀況下,這些顏色在空間上均勻地被渲染,可是咱們能夠用locations屬性來調整空間。 gradientLayer.locations=@[@0.0,@0.1,@0.7]; gradientLayer.endPoint=CGPointMake(1, 1); [self.view.layer addSublayer:gradientLayer]; }
運行效果
※CAReplicatorLayer
CAReplicationLayer的目的是爲了高效生成許多類似的圖層。它會繪製一個或多個圖層的子圖層,並在每一個複製體上應用不一樣的變換
※重複圖層(Repeating Layers)
在屏幕的中間建立了一個小白色方塊圖層,而後用CAReplicatorLayer生成十個圖層組成一個圓圈。instanceCount屬性指定了圖層須要重複多少次。instanceTransform指定了一個CATransform3D變換(這種狀況下,下一圖層的位移和旋轉將會移動到圓圈的下一個點)。變換是逐步增長的,每一個實例都是相對於前一實例佈局。這就是爲何這些複製體最終不會出如今同一位置上。能夠用instanceBlueOffset和instanceGreenOffset屬性實現顏色變化,經過逐步減小藍色和綠色通道,組件將圖層顏色轉換成了紅色
//示例
//CAReplicatorLayer重複圖層;建立了一個小白色方塊圖層,而後用CAReplicatorLayer生成十個圖層組成一個圓圈 -(void)createReplicatorLayer{ CAReplicatorLayer *replicator=[CAReplicatorLayer layer]; replicator.frame=self.view.bounds; [self.view.layer addSublayer:replicator]; //建立副本數量,包括原圖層 replicator.instanceCount=10; CATransform3D transform=CATransform3DIdentity; transform=CATransform3DTranslate(transform, 0, -50, 0); transform=CATransform3DRotate(transform, M_PI/5, 0, 0, 1); transform=CATransform3DTranslate(transform, 0, 50, 0); replicator.instanceTransform=transform; //爲每一個實例應用顏色轉變 replicator.instanceBlueOffset = -0.1; replicator.instanceGreenOffset = -0.1; //建立一個內部子層,並將其複製因子 CALayer *layer = [CALayer layer]; layer.frame = CGRectMake(125.0f, 50.0f, 50.0f, 50.0f); layer.backgroundColor = [UIColor whiteColor].CGColor; [replicator addSublayer:layer]; }
運行效果
※反射
使用CAReplicatorLaye並應用一個負比例變換於一個圖層圖層,就能夠建立指定視圖(或整個視圖層次)內容的鏡像圖片,這樣就建立了一個實時的反射效果。指定一個繼承於UIVie的ReflectionView,它會自動產生內容的反射效果。
開源代碼ReflectionView完成了一個自適應的漸變淡出效果(用CAGradientLayer和圖層蒙板實現)
//示例
//ReflectionView.m文件
// // ReflectionView.m // CoreAnimationLearn // 用CAReplicatorLayer自動繪製反射 // Created by Vie on 2017/7/6. // Copyright © 2017年 Vie. All rights reserved. // #import "ReflectionView.h" @implementation ReflectionView +(Class)layerClass{ return [CAReplicatorLayer class]; } #pragma mark lazy loading #pragma mark 視圖加載 -(instancetype)initWithFrame:(CGRect)frame{ self=[super initWithFrame:frame]; if (self) { [self setUp]; } return self; } -(void)setUp{ //配置Reflection CAReplicatorLayer *layer=(CAReplicatorLayer *)self.layer; layer.instanceCount=2; //低於原始反射實例和垂直翻轉 CATransform3D transform=CATransform3DIdentity; CGFloat verticalOffset=self.bounds.size.height*2; transform=CATransform3DTranslate(transform, 0, verticalOffset, 0); transform=CATransform3DScale(transform, 1, -1, 0); layer.instanceTransform=transform; //減小反射層的透明值 layer.instanceAlphaOffset=-0.7; } @end
運行效果
※CAScrollLayer
CAScrollLayer有一個-scrollToPoint:方法,它自動適應bounds的原點以便圖層出如今滑動的地方。CAScrollLayer並無等同於UIScrollView中contentSize的屬性,因此當CAScrollLayer滑動的時候徹底沒有一個全局的可滑動區域的概念,也沒法自適應它的邊界原點至你指定的值。它子因此不能自適應邊界大小是由於它不須要,內容徹底能夠超過邊界來實現滑動。那麼CAScrollLayer的意義到底何在,由於能夠簡單地用一個普通的CALayer而後手動適應邊界原點,其實UIScrollView並無用CAScrollLayer,是簡單的經過直接操做圖層邊界來實現滑動。
※CATiedLayer
當你須要繪製一個很大的圖片像是一個高像素的照片或者地球表面的詳細地圖。因爲iOS應用一般運行在內存受限的設備上,因此讀取整個圖片到內存中是不明智的。載入大圖可能會至關的慢。能高效繪製在iOS上的圖片也有一個大小限制,全部顯示在屏幕上的圖片最終都會被轉化爲OpenGL紋理,同時OpenGL有一個最大的紋理尺寸(一般是2048x2048,或4096x4096,這個取決於設備型號)。
CATiledLayer爲載入大圖形成的性能問題提供了一個解決方案:將大圖分解成小片而後將他們單獨按需載入。
※小片裁剪
用CATiledLayer將圖片裁剪成許多小一些的圖片,可是不要在運行時讀入整個圖片並裁剪,那CATiledLayer的因此性能有點就損失殆盡了。
※Retina小圖
上面裁剪的小圖並非以Retina的分辨率顯示的,爲了以屏幕的原生分辨率來渲染CATiledLayer,須要設置圖層的contentsScale來匹配UIScreen的scale屬性,增大了contentsScale就自動有了默認的小圖尺寸(
layer.contentsScale = [UIScreen mainScreen].scale;
※CAEmitterLayer
CAEmitterLayer是一個高性能的粒子引擎,被用來建立實時粒子動畫如:煙霧,火,雨等等這些效果。它看上線像是許多CAEmitterCell的容器,這些CAEmitterCell定義了一個粒子效果。爲不一樣的粒子效果定義一個或多個CAEmitterCell做爲模板,同時CAEmitterLayer負責基於這些模板實例化一個粒子流。一個CAEmitterCell相似於一個CALayer:它有一個contents屬性能夠定義爲一個CGImage,另外還有一些能夠設置屬性控制着表現和行爲。
//示例
//用CAEmitterLayer建立爆炸效果 -(void)createCAEmitterLayerView{ //建立Emitter層 CAEmitterLayer *emitter=[CAEmitterLayer layer]; emitter.frame=self.view.bounds; [self.view.layer addSublayer:emitter]; //配置Emitter emitter.renderMode=kCAEmitterLayerAdditive; emitter.emitterPosition=CGPointMake(emitter.frame.size.width/2.0, emitter.frame.size.height/2.0); //建立一個粒子模板 CAEmitterCell *cell=[[CAEmitterCell alloc] init]; cell.contents=(__bridge id)[UIImage imageNamed:@"8.png"].CGImage; cell.birthRate=88;//動畫每秒生產對象的數量 cell.lifetime=5.0;//動畫單個細胞的生命週期 cell.color=[UIColor orangeColor].CGColor;//指定了一個能夠混合圖片內容顏色的混合色 cell.alphaSpeed=-0.4;//這裏表示粒子的透明度每過1秒就減小0.4 cell.velocity=39;//粒子模板動畫的初始速度 cell.velocityRange=50;//粒子模板動畫的速度能夠變化的量。 cell.emissionRange=M_PI*2.0;//粒子某一屬性的變化範圍,這裏值是2π覺得着粒子能夠從360度任意位置反射出來 cell.contentsScale=[UIScreen mainScreen].scale; emitter.emitterCells=@[cell]; }
運行效果
※CAEAGLLayer
在iOS 5中,蘋果引入了一個新的框架叫作GLKit,它去掉了一些設置OpenGL的複雜性,提供了一個叫作CLKView的UIView的子類,處理大部分的設置和繪製工做。前提是各類各樣的OpenGL繪圖緩衝的底層可配置項任然須要你用CAEAGLLayer完成,它是CALayer的一個子類,用了顯示任意的OpenGL圖形。
//示例
//用CAEAGLLayer繪製一個三角形 @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; //用CAEAGLLayer繪製一個三角形 -(void)createCAEAGLLayerView{ //set up context self.glContext = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES2]; [EAGLContext setCurrentContext:self.glContext]; //set up layer self.glLayer = [CAEAGLLayer layer]; self.glLayer.frame = self.view.bounds; [self.view.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)setUpBuffers{ //設置幀緩衝 glGenFramebuffers(1, &_framebuffer); glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer); //設置彩色渲染緩衝區 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); //檢查成功 if (glCheckFramebufferStatus(GL_FRAMEBUFFER)!=GL_FRAMEBUFFER_COMPLETE) { NSLog(@"Failed to make complete framebuffer object: %i", glCheckFramebufferStatus(GL_FRAMEBUFFER)); } } -(void)tearDownBuffers{ if (_framebuffer) { glDeleteFramebuffers(1, &_framebuffer); _framebuffer=0; } if (_colorRenderbuffer) { 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]; }
運行效果
※AVPlayerLayer
AVPlayerLayer是由AVFoundation框架來提供的,它和CoreAnimation緊密地結合在一塊兒,提供了一個CALayer的子類來顯示自定義的內容類型。AVPlayerLayer是用來在iOS上播放視頻的。它是高級接口例如MPMoviePlayer的底層實現,提供了顯示視頻的底層控制。
//示例
//AVPlayerLayer給視頻增長變換,邊框和圓角 -(void)createAVPlayerLayerView{ NSString *path=[[NSBundle mainBundle] pathForResource:@"DotA2官方宣傳片" ofType:@"mp4"]; NSURL *videoURL=[NSURL fileURLWithPath:path]; AVPlayer *player=[AVPlayer playerWithURL:videoURL]; AVPlayerLayer *layer=[AVPlayerLayer playerLayerWithPlayer:player]; layer.videoGravity=AVLayerVideoGravityResizeAspect; layer.frame=CGRectMake(-80, 80, 480, 320); [self.view.layer addSublayer:layer]; //轉換層 CATransform3D transform=CATransform3DIdentity; transform.m34=-1.0/500.0; transform=CATransform3DRotate(transform, M_PI_4, 1, 1, 0); layer.transform=transform; //添加圓角和邊框 layer.masksToBounds=YES; layer.cornerRadius=20.0; layer.borderColor=[UIColor redColor].CGColor; layer.borderWidth=5.0; [player play]; }
運行效果
※事務
CoreAnimation基於一個假設:屏幕上任何東西均可以(或者可能)作動畫。因此並不須要在CoreAnimation中手動打開動畫,可是你須要明確的關閉它,不然它會一直存在。
當你改變CALayer一個可作動畫的屬性時,這個改變並不會馬上在屏幕上體現出來。相反,該屬性會從先前的值平滑過渡到新的值,而不是跳變。這一切都是默認的行爲,你不須要作額外的操做。這就是所謂的隱式動畫。之因此叫作隱式是由於咱們並無指定任何動畫的類型。僅僅改變了一個屬性,而後CoreAnimation來決定如何而且什麼時候去作動畫。
CoreAnimation經過事務用來包含一系列屬性動畫集合的機制,任何用指定事務去改變能夠作動畫的圖層屬性都不會馬上發送變化,而是當事務一旦提交的時候開始用一個動畫過渡到新值。事務是經過CATransaction類來作管理,這個類的設計有些奇怪,不像你從它的命名預期的那樣去管理一個簡單的事務,二手管理了一疊你不能訪問的事務。CATransaction沒有屬性或者實例方法,而且也不能用+alloc和-init方法建立它。而是用類方法+begin和+commit分別來入棧或者出棧。
任何能夠作動畫的圖層屬性都會被添加到棧頂的事務,你能夠經過+setAnimationDuration:方法設置當前事務的動畫時間,或者經過+animationDuration方法來獲取時長值(默認0.25秒)。CoreAnimation在每一個run loop週期中自動開始一次新的事務(run loop是iOS負責收集用戶輸入,處理未完成的定時器或者網絡事件,最終從新繪製屏幕的東西),即便你不顯式的使用[CATransaction begin]開始一次事務,在一個特定run loop循環中的任何屬性的變化都會被收集起來,而後作一次0。25秒的動畫。明白這些以後,咱們就能夠輕鬆修改變色動畫的時間了。咱們固然能夠用當前事務的+setAnimationDuration:方法來修改動畫時間,但在這裏咱們首先起一個新的事務,因而修改時間就不會有別的反作用。由於修改當前事務的時間可能會致使同一時刻別的動畫(如屏幕旋轉),因此最好仍是在調整動畫以前壓入一個新的事務。
※完成塊
基於UIView的block的動畫容許在動畫結束的時候提供一個完成的動做。CATransaction接口提供的+setCompletionBlock:方法也有一樣的功能。
※圖層的行爲
Core Animation一般對CALayer的全部屬性(可動畫的屬性)作動畫,可是UIView把它關聯的圖層的這個特性關閉了。
咱們把改變屬性時CALayer自動應用的動畫稱做行爲,當CALayer的屬性被修改時候,它會調用-actionForKey:方法,傳遞屬性的名稱。剩下的操做都在CALayer的頭文件中有詳細的說明,實質上是以下幾步:
- 圖層首先檢測它是否有委託,而且是否實現CALayerDelegate協議指定的-actionForLayer:forKey方法。若是有,直接調用並返回結果。
- 若是沒有委託,或者委託沒有實現-actionForLayer:forKey方法,圖層接着檢查包含屬性名稱對應行爲映射的actions字典。
- 若是actions字典沒有包含對應的屬性,那麼圖層接着在它的style字典接着搜索屬性名。
- 最後,若是在style裏面也找不到對應的行爲,那麼圖層將會直接調用定義了每一個屬性的標準行爲的-defaultActionForKey:方法。
因此一輪完整的搜索結束以後,-actionForKey:要麼返回空(這種狀況下將不會有動畫發生),要麼是CAAction協議對應的對象,最後CALayer拿這個結果去對先前和當前的值作動畫。因而這就解釋了UIKit是如何禁用隱式動畫的:每一個UIView對它關聯的圖層都扮演了一個委託,而且提供了-actionForLayer:forKey的實現方法。當不在一個動畫塊的實現中,UIView對全部圖層行爲返回nil,可是在動畫block範圍以內,它就返回了一個非空值。當屬性在動畫塊以外發生改變,UIView直接經過返回nil來禁用隱式動畫。但若是在動畫塊範圍以內,根據動畫具體類型返回相應的屬性。
固然返回nil並非禁用隱式動畫惟一的辦法,CATransaction有個方法叫作+setDisableActions:,能夠用來對全部屬性打開或者關閉隱式動畫。若是在清單7.2的[CATransaction begin]以後添加下面的代碼,一樣也會阻止動畫的發生:
[CATransaction setDisableActions:YES];
總結一下,咱們知道了以下幾點
- UIView關聯的圖層禁用了隱式動畫,對這種圖層作動畫的惟一辦法就是使用UIView的動畫函數(而不是依賴CATransaction),或者繼承UIView,並覆蓋-actionForLayer:forKey:方法,或者直接建立一個顯式動畫(具體細節見第八章)。
- 對於單獨存在的圖層,咱們能夠經過實現圖層的-actionForLayer:forKey:委託方法,或者提供一個actions字典來控制隱式動畫。
※呈現與模型
CALayer的屬性行爲其實很不正常,由於改變一個圖層的屬性並無馬上生效,而是經過一段時間漸變動新。改變一個圖層的屬性,屬性值的確是馬上更新的(若是讀取它的數據,會發現它的值在你設置它的那一刻就已經生效了),可是屏幕上並無立刻發生改變。這是由於設置的屬性並無直接調整圖層的外觀,相反,他只是定義了圖層動畫結束以後將要變化的外觀。
當設置CALayer的屬性,其實是在定義當前事務結束以後圖層如何顯示的模型。Core Animation扮演了一個控制器的角色,而且負責根據圖層行爲和事務設置去不斷更新視圖的這些屬性在屏幕上的狀態。是一個典型的微型MVC模式。CALayer是一個鏈接用戶界面(就是MVC中的view)虛構的類,可是在界面自己這個場景下,CALayer的行爲更像是存儲了視圖如何顯示和動畫的數據模型。實際上,在蘋果本身的文檔中,圖層樹一般都是值的圖層樹模型。
在iOS中,屏幕每秒鐘重繪60次。若是動畫時長比60分之一秒要長,Core Animation就須要在設置一次新值和新值生效之間,對屏幕上的圖層進行從新組織。這意味着CALayer除了「真實」值(就是你設置的值)以外,必需要知道當前顯示在屏幕上的屬性值的記錄。
每一個圖層屬性的顯示值都被存儲在一個叫作呈現圖層的獨立圖層當中,他能夠經過-presentationLayer方法來訪問。這個呈現圖層其實是模型圖層的複製,可是它的屬性值表明了在任何指定時刻當前外觀效果。換句話說,你能夠經過呈現圖層的值來獲取當前屏幕上真正顯示出來的值。
前面提到除了圖層樹,另外還有呈現樹。呈現樹經過圖層樹中全部圖層的呈現圖層所造成。注意呈現圖層僅僅當圖層首次被提交(就是首次第一次在屏幕上顯示)的時候建立,因此在那以前調用-presentationLayer將會返回nil。
你可能注意到有一個叫作–modelLayer的方法。在呈現圖層上調用–modelLayer將會返回它正在呈現所依賴的CALayer。一般在一個圖層上調用-modelLayer會返回–self(實際上咱們已經建立的原始圖層就是一種數據模型)。
//示例
// // ImplicitAnimationViewController.m // CoreAnimationLearn // 隱式動畫 // Created by Vie on 2017/7/18. // Copyright © 2017年 Vie. All rights reserved. // #import "ImplicitAnimationViewController.h" @interface ImplicitAnimationViewController () @property (nonatomic, strong) CALayer *colorLayer; @end @implementation ImplicitAnimationViewController - (void)viewDidLoad { [super viewDidLoad]; [self.view setBackgroundColor:RGB_ALPHA_COLOR(230, 230, 230, 1)]; UIBarButtonItem *leftItem=[[UIBarButtonItem alloc] initWithTitle:@"<" style:UIBarButtonItemStylePlain target:self action:@selector(backAction:)]; self.navigationItem.leftBarButtonItem=leftItem; //讓約束從導航欄底部開始算起 self.edgesForExtendedLayout = UIRectEdgeNone; self.extendedLayoutIncludesOpaqueBars = NO; self.modalPresentationCapturesStatusBarAppearance = NO; [self createColorLayerView]; } //隨機改變圖層顏色 -(void)createColorLayerView{ self.colorLayer=[CALayer layer]; self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f); self.colorLayer.backgroundColor = [UIColor blueColor].CGColor; //添加一個自定義的動做,默認是漸變 CATransition *transition = [CATransition animation]; transition.type = kCATransitionPush; transition.subtype = kCATransitionFromTop; self.colorLayer.actions = @{@"backgroundColor": transition}; [self.view.layer addSublayer:self.colorLayer]; } //點擊動畫 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { //得到觸點 CGPoint point = [[touches anyObject] locationInView:self.view]; //檢查咱們是否點擊了移動圖層 if ([self.colorLayer hitTest:point]) { //begin a new transaction [CATransaction begin]; //set the animation duration to 1 second [CATransaction setAnimationDuration:1.0]; //在完成時添加自旋動畫 [CATransaction setCompletionBlock:^{ //旋轉90度 //注意旋轉動畫要比顏色漸變快得多,這是由於完成塊是在顏色漸變的事務提交併出棧以後才被執行,因而,用默認的事務作變換,默認的時間也就變成了0.25秒。 CGAffineTransform transform = self.colorLayer.affineTransform; transform = CGAffineTransformRotate(transform, M_PI_2); self.colorLayer.affineTransform = transform; }]; //randomize the layer background color 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.0].CGColor; //commit the transaction [CATransaction commit]; } else { //不然(慢慢地)將圖層移動到新位置 [CATransaction begin]; [CATransaction setAnimationDuration:1.0]; self.colorLayer.position = point; [CATransaction commit]; } } #pragma mark Action -(void)backAction:(id)sender{ [self.navigationController popViewControllerAnimated:YES]; } #pragma mark lazy loading @end
運行效果
※屬性動畫
屬性動畫做用於圖層的某個單一屬性,並指定了它的一個目標值,或者一連串將要作動畫的值。屬性動畫分爲兩種:基礎和關鍵幀。
※基礎動畫
動畫其實就是一段時間內發生的改變,最簡單的形式就是從一個值改變到另外一個值,這也是CABasicAnimation最主要的功能。CABasicAnimation是CAPropertyAnimation的一個子類,而CAPropertyAnimation的父類是CAAnimation,CAAnimation同時也是Core Animation全部動畫類型的抽象基類。
做爲一個抽象類,CAAnimation自己並無作多少工做,它提供了一個計時函數(見「緩衝」),一個委託(用於反饋動畫狀態)以及一個removedOnCompletion,用於標識動畫是否該在結束後自動釋放(默認YES,爲了防止內存泄露)。CAAnimation同時實現了一些協議,包括CAAction(容許CAAnimation的子類能夠提供圖層行爲),以及CAMediaTiming(「圖層時間」將會詳細解釋)。
CAPropertyAnimation經過指定動畫的keyPath做用於一個單一屬性,CAAnimation一般應用於一個指定的CALayer,因而這裏指的也就是一個圖層的keyPath了。實際上它是一個關鍵路徑(一些用點表示法能夠在層級關係中指向任意嵌套的對象),而不只僅是一個屬性的名稱,由於這意味着動畫不只能夠做用於圖層自己的屬性,並且還包含了它的子成員的屬性,甚至是一些虛擬的屬性(後面會詳細解釋)。
CABasicAnimation繼承於CAPropertyAnimation,並添加了以下屬性:
id fromValue (表明了動畫開始以前屬性的值)
id toValue (表明了動畫結束以後的值)
id byValue (表明了動畫執行過程當中改變的值)
fromValue,toValue和byValue屬性能夠用不少種方式來組合,但爲了防止衝突,不能一次性同時指定這三個值。例如,若是指定了fromValue等於2,toValue等於4,byValue等於3,那麼Core Animation就不知道結果究竟是4(toValue)仍是5(fromValue + byValue)了。總的說來,就是隻須要指定toValue或者byValue,剩下的值均可以經過上下文自動計算出來。
※CAAnimationDelegate
CAAnimationDelegate協議的delegate能夠知道一個顯式動畫在什麼時候結束,能夠再CAAnimation頭文件或者蘋果開發者文檔中找到相關函數,下面用animationDidStop:finished:方法在動畫結束以後來更新圖層的backgroundColor(當更新屬性的時候,咱們須要設置一個新的事務,而且禁用圖層行爲。不然動畫會發生兩次,一個是由於顯式的CABasicAnimation,另外一次是由於隱式動畫)。對CAAnimation而言,使用委託模式而不是一個完成塊會帶來一個問題,就是當你有多個動畫的時候,沒法在在回調方法中區分。在一個視圖控制器中建立動畫的時候,一般會用控制器自己做爲一個委託,可是全部的動畫都會調用同一個回調方法,因此你就須要判斷究竟是那個圖層的調用。
//示例
//隨機改變圖層顏色 -(void)createColorLayerView{ self.colorLayer=[CALayer layer]; self.colorLayer.frame=CGRectMake(50.0f, 50.0f, 100.0f, 100.0f); self.colorLayer.backgroundColor=[UIColor blueColor].CGColor; [self.view.layer addSublayer:self.colorLayer]; [NSTimer scheduledTimerWithTimeInterval:3.0f target:self selector:@selector(changeColor) userInfo:nil repeats:YES]; } -(void)changeColor{ //建立顏色隨機數 CGFloat red=arc4random() % 255/255.0; CGFloat green=arc4random()%255/255.0; CGFloat blue=arc4random()%255/255.0; UIColor *color = [UIColor colorWithRed:red green:green blue:blue alpha:1.0]; //建立基礎動畫 CABasicAnimation *animation=[CABasicAnimation animation]; animation.keyPath=@"backgroundColor"; animation.toValue=(__bridge id)color.CGColor; animation.duration = 2; [self applyBasicAnimation:animation toLayer:self.colorLayer]; } //這裏已經正在進行一段動畫,咱們須要從呈現圖層那裏去得到fromValue,而不是模型圖層。另外,因爲這裏的圖層並非UIView關聯的圖層,咱們須要用CATransaction來禁用隱式動畫行爲,不然默認的圖層行爲會干擾咱們的顯式動畫(實際上,顯式動畫一般會覆蓋隱式動畫,但在文章中並無提到,因此爲了安全最好這麼作)。 - (void)applyBasicAnimation:(CABasicAnimation *)animation toLayer:(CALayer *)layer { //設置from值(若是可用的話,使用表示層) animation.fromValue = [(layer.presentationLayer ? layer.presentationLayer : layer) valueForKeyPath:animation.keyPath]; //update the property in advance //note: this approach will only work if toValue != nil [CATransaction begin]; //須要用CATransaction來禁用隱式動畫行爲,不然默認的圖層行爲會干擾咱們的顯式動畫(實際上,顯式動畫一般會覆蓋隱式動畫,但在文章中並無提到,因此爲了安全最好這麼作)。 [CATransaction setDisableActions:YES]; [layer setValue:animation.toValue forKeyPath:animation.keyPath]; [CATransaction commit]; animation.delegate = self; //apply animation to layer [layer addAnimation:animation forKey:nil]; } //動畫完成以後修改背景圖顏色 - (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag { [CATransaction begin]; //須要用CATransaction來禁用隱式動畫行爲,不然默認的圖層行爲會干擾咱們的顯式動畫(實際上,顯式動畫一般會覆蓋隱式動畫,但在文章中並無提到,因此爲了安全最好這麼作)。 [CATransaction setDisableActions:YES]; self.colorLayer.backgroundColor=[UIColor orangeColor].CGColor; [CATransaction commit]; }
運行效果
※關鍵幀動畫
CABasicAnimation揭示了大多數隱式動畫背後依賴的機制,這的確頗有趣,可是顯式地給圖層添加CABasicAnimation相較於隱式動畫而言,只能說費力不討好。
CAKeyframeAnimation是另外一種UIKit沒有暴露出來但功能強大的類。和CABasicAnimation相似,CAKeyframeAnimation一樣是CAPropertyAnimation的一個子類,它依然做用於單一的一個屬性,可是和CABasicAnimation不同的是,它不限制於設置一個起始和結束的值,而是能夠根據一連串隨意的值來作動畫。關鍵幀起源於傳動動畫,意思是指主導的動畫在顯著改變發生時重繪當前幀(也就是關鍵幀),每幀之間剩下的繪製(能夠經過關鍵幀推算出)將由熟練的藝術家來完成。CAKeyframeAnimation也是一樣的道理:你提供了顯著的幀,而後Core Animation在每幀之間進行插值。
CAKeyframeAnimation並不能自動把當前值做爲第一幀(就像CABasicAnimation那樣把fromValue設爲nil)。動畫會在開始的時候忽然跳轉到第一幀的值,而後在動畫結束的時候忽然恢復到原始的值。因此爲了動畫的平滑特性,咱們須要開始和結束的關鍵幀來匹配當前屬性的值。
※虛擬屬性
若是想要對一個物體作旋轉的動畫,那就須要做用於transform屬性,由於CALayer沒有顯式提供角度或者方向之類的屬性,爲了旋轉圖層,咱們能夠對transform.rotation關鍵路徑應用動畫,而不是transform自己。transform.rotation屬性有一個奇怪的問題是它其實並不存在。這是由於CATransform3D並非一個對象,它其實是一個結構體,也沒有符合KVC相關屬性,transform.rotation其實是一個CALayer用於處理動畫變換的虛擬屬性。
※動畫組
CAAnimationGroup能夠把這些動畫組合在一塊兒。CAAnimationGroup是另外一個繼承於CAAnimation的子類,它添加了一個animations數組的屬性,用來組合別的動畫。
//示例
//沿着一個貝塞爾曲線對圖層作動畫 -(void)createBezierPathAnimation{ //建立路徑 UIBezierPath *bezierPath=[[UIBezierPath alloc] init]; [bezierPath moveToPoint:CGPointMake(0, 150)]; [bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)]; //用CAShapeLayer畫路徑 CAShapeLayer *pathLayer=[CAShapeLayer layer]; pathLayer.path=bezierPath.CGPath; pathLayer.fillColor=[UIColor clearColor].CGColor; pathLayer.strokeColor=[UIColor redColor].CGColor; pathLayer.lineWidth=3.0f; [self.view.layer addSublayer:pathLayer]; //圖片層 CALayer *shipLayer=[CALayer layer]; shipLayer.frame=CGRectMake(0, 0, 64, 64); shipLayer.position=CGPointMake(0, 150); shipLayer.contents=(__bridge id)[UIImage imageNamed:@"3.jpg"].CGImage; [self.view.layer addSublayer:shipLayer]; //建立關鍵幀動畫 CAKeyframeAnimation *animation=[CAKeyframeAnimation animation]; animation.keyPath=@"position"; animation.path=bezierPath.CGPath; //rotationMode的屬性。設置它爲常量kCAAnimationRotateAuto,圖層將會根據曲線的切線自動旋轉 animation.rotationMode=kCAAnimationRotateAuto; //動畫船旋轉 CABasicAnimation *animationBasic = [CABasicAnimation animation]; animationBasic.keyPath = @"transform.rotation"; animationBasic.byValue = @(M_PI * 2); //動畫組 CAAnimationGroup *groupAnimation = [CAAnimationGroup animation]; groupAnimation.animations = @[animation, animationBasic]; groupAnimation.repeatDuration=INFINITY; groupAnimation.duration = 4.0; [shipLayer addAnimation:groupAnimation forKey:nil]; }
運行效果
※過渡
有時候對於iOS應用程序來講,但願能經過屬性動畫來對比較難作動畫的佈局進行一些改變。好比交換一段文本和圖片,或者用一段網格視圖來替換,等等。屬性動畫只對圖層的可動畫屬性起做用,因此若是要改變一個不能動畫的屬性(好比圖片),或者從層級關係中添加或者移除圖層,屬性動畫將不起做用。
因而就有了過渡的概念。過渡並不像屬性動畫那樣平滑地在兩個值之間作動畫,而是影響到整個圖層的變化。過渡動畫首先展現以前的圖層外觀,而後經過一個交換過渡到新的外觀。爲了建立一個過渡動畫,咱們將使用CATransition,一樣是另外一個CAAnimation的子類,和別的子類不一樣,CATransition有一個type和subtype來標識變換效果。
type屬性是一個NSString類型,能夠被設置成以下類型:
kCATransitionFade 平滑的淡入淡出效果
kCATransitionMoveIn 從頂部滑動進入,但不像推送動畫那樣把老圖層推走
kCATransitionPush 把原始的圖層滑動出去來顯示新的外觀,而不是把新的圖層滑動進入。
kCATransitionReveal 它建立了一個新的圖層,從邊緣的一側滑動進來,把舊圖層從另外一側推出去的效果
subtype來控制它們的方向,提供了以下四種類型:
kCATransitionFromRight
kCATransitionFromLeft
kCATransitionFromTop
kCATransitionFromBottom
※隱式過渡
CATransition的確是默認的行爲。可是對於視圖關聯的圖層,或者是其餘隱式動畫的行爲,這個特性依然是被禁用的,可是對於你本身建立的圖層,這意味着對圖層contents圖片作的改動都會自動附上淡入淡出的動畫。
※對圖層樹的動畫
CATransition並不做用於指定的圖層屬性,這就是說你能夠在即便不能準確得知改變了什麼的狀況下對圖層作動畫,例如,在不知道UITableView哪一行被添加或者刪除的狀況下,直接就能夠平滑地刷新它,或者在不知道UIViewController內部的視圖層級的狀況下對兩個不一樣的實例作過渡動畫。這些例子和咱們以前的狀況徹底不一樣,由於它們不只涉及到圖層的屬性,並且是整個圖層樹的改變--在這種動畫的過程當中手動在層級關係中添加或者移除圖層。要確保CATransition添加到的圖層在過渡動畫發生時不會在樹狀結構中被移除,不然CATransition將會和圖層一塊兒被移除。通常來講,只須要將動畫添加到被影響圖層的superlayer。
※自定義動畫
經過UIView +transitionFromView:toView:duration:options:completion:和+transitionWithView:duration:options:animations:方法提供了Core Animation的過渡特性。可是這裏的可用的過渡選項和CATransition的type屬性提供的常量徹底不一樣。UIView過渡方法中options參數能夠由以下常量指定:
UIViewAnimationOptionTransitionFlipFromLeft
UIViewAnimationOptionTransitionFlipFromRight
UIViewAnimationOptionTransitionCurlUp
UIViewAnimationOptionTransitionCurlDown
UIViewAnimationOptionTransitionCrossDissolve
UIViewAnimationOptionTransitionFlipFromTop
UIViewAnimationOptionTransitionFlipFromBottom
除了UIViewAnimationOptionTransitionCrossDissolve以外,剩下的值和CATransition類型徹底不要緊。
CALayer有一個-renderInContext:方法,能夠經過把它繪製到Core Graphics的上下文中捕獲當前內容的圖片,而後在另外的視圖中顯示出來。
※在動畫過程當中取消動畫
能夠用-addAnimation:forKey:方法中的key參數來在添加動畫以後檢索一個動畫,使用以下方法:
- (CAAnimation *)animationForKey:(NSString *)key;
爲了終止一個指定的動畫,你能夠用以下方法把它從圖層移除掉:
- (void)removeAnimationForKey:(NSString *)key;
或者移除全部動畫:
- (void)removeAllAnimations;
※CAMediaTiming協議
CAMediaTiming協議定義了在一段動畫內用來控制逝去時間的屬性的集合,CALayer和CAAnimation都實現了這個協議,因此時間能夠被任意基於一個圖層或者一段動畫的類控制。
※持續和重複
duration是一個CFTimeInterval的類型(相似於NSTimeInterval的一種雙精度浮點類型),對將要進行的動畫的一次迭代指定了時間。
repeatCount,表明動畫重複的迭代次數。若是duration是2,repeatCount設爲3.5(三個半迭代),那麼完整的動畫時長將是7秒。
duration和repeatCount默認都是0。但這不意味着動畫時長爲0秒,或者0次,這裏的0僅僅表明了「默認」,也就是0.25秒和1次。
建立重複動畫的另外一種方式是使用repeatDuration屬性,它讓動畫重複一個指定的時間,而不是指定次數。
autoreverses的屬性(BOOL類型)在每次間隔交替循環過程當中自動回放。這對於播放一段連續非循環的動畫頗有用,例如打開一扇門,而後關上它
//示例
//使用autoreverses屬性實現門的搖擺,autoreverses的屬性(BOOL類型)在每次間隔交替循環過程當中自動回放;在這裏咱們把repeatDuration設置爲INFINITY,因而動畫無限循環播放,設置repeatCount爲INFINITY也有一樣的效果。注意repeatCount和repeatDuration可能會相互衝突,因此你只要對其中一個指定非零值。對兩個屬性都設置非0值的行爲沒有被定義。 -(void)createShutDoorAnimation{ //添加圖片當作門 CALayer *doorLayer=[CALayer layer]; doorLayer.frame=CGRectMake(0, 0, 128, 256); doorLayer.position=CGPointMake(150-64, 150); doorLayer.anchorPoint=CGPointMake(0, 0.5); doorLayer.contents=(__bridge id)[UIImage imageNamed:@"3.jpg"].CGImage; [self.view.layer addSublayer:doorLayer]; //應用角度變換 CATransform3D perspective=CATransform3DIdentity; perspective.m34=-1.0/500.0; self.view.layer.sublayerTransform=perspective; //應用門搖擺動畫 CABasicAnimation *animation=[CABasicAnimation animation]; animation.keyPath=@"transform.rotation.y"; animation.toValue=@(-M_PI_2); animation.duration=2.0; animation.repeatDuration=INFINITY; // animation.repeatCount=INFINITY; animation.autoreverses=YES; [doorLayer addAnimation:animation forKey:nil]; }
運行效果
※相對時間
每次討論到Core Animation,時間都是相對的,每一個動畫都有它本身描述的時間,能夠獨立地加速,延時或者偏移。
beginTime指定了動畫開始以前的的延遲時間。這裏的延遲從動畫添加到可見圖層的那一刻開始測量,默認是0(就是說動畫會馬上執行)。
speed是一個時間的倍數,默認1.0,減小它會減慢圖層/動畫的時間,增長它會加快速度。若是2.0的速度,那麼對於一個duration爲1的動畫,實際上在0.5秒的時候就已經完成了。
timeOffset和beginTime相似,可是和增長beginTime致使的延遲動畫不一樣,增長timeOffset只是讓動畫快進到某一點,例如,對於一個持續1秒的動畫來講,設置timeOffset爲0.5意味着動畫將從一半的地方開始。
和beginTime不一樣的是,timeOffset並不受speed的影響。因此若是你把speed設爲2.0,把timeOffset設置爲0.5,那麼你的動畫將從動畫中間的地方開始,雖然動畫實際上被縮短到了0.5秒。然而即便使用了timeOffset讓動畫從結束的地方開始,它仍然播放了一個完整的時長,這個動畫僅僅是循環了一圈,而後從頭開始播放。
//示例
//測試timeOffset和speed屬性;speed是一個時間的倍數,默認1.0,減小它會減慢圖層/動畫的時間,增長它會加快速度。若是2.0的速度,那麼對於一個duration爲1的動畫,實際上在0.5秒的時候就已經完成了。增長timeOffset只是讓動畫快進到某一點,例如,對於一個持續1秒的動畫來講,設置timeOffset爲0.5意味着動畫將從一半的地方開始。 -(void)createSpeedAnimation{ //建立路徑 UIBezierPath *bezierPath=[[UIBezierPath alloc] init]; [bezierPath moveToPoint:CGPointMake(0, 150)]; [bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)]; //draw the path using a CAShapeLayer CAShapeLayer *pathLayer=[CAShapeLayer layer]; pathLayer.path=bezierPath.CGPath; pathLayer.fillColor=[UIColor clearColor].CGColor; pathLayer.strokeColor=[UIColor redColor].CGColor; pathLayer.lineWidth=3.0f; [self.view.layer addSublayer:pathLayer]; //圖片層 CALayer *shipLayer=[CALayer layer]; shipLayer.frame=CGRectMake(0, 0, 64, 64); shipLayer.position=CGPointMake(0, 150); shipLayer.contents=(__bridge id)[UIImage imageNamed:@"3.jpg"].CGImage; [self.view.layer addSublayer:shipLayer]; //建立關鍵幀動畫 CAKeyframeAnimation *animation=[CAKeyframeAnimation animation]; animation.keyPath=@"position"; animation.duration=8.0; animation.repeatCount=INFINITY; animation.path=bezierPath.CGPath; animation.rotationMode=kCAAnimationRotateAuto; animation.speed=2; animation.timeOffset=3; [shipLayer addAnimation:animation forKey:nil]; }
運行效果
※fillMode
對於beginTime非0的一段動畫來講,會出現一個當動畫添加到圖層上但什麼也沒發生的狀態。相似的,removeOnCompletion被設置爲NO的動畫將會在動畫結束的時候仍然保持以前的狀態。這就產生了一個問題,當動畫開始以前和動畫結束以後,被設置動畫的屬性將會是什麼值呢?
一種多是屬性和動畫沒被添加以前保持一致,也就是在模型圖層定義的值(見第七章「隱式動畫」,模型圖層和呈現圖層的解釋)。
另外一種多是保持動畫開始以前那一幀,或者動畫結束以後的那一幀。這就是所謂的填充,由於動畫開始和結束的值用來填充開始以前和結束以後的時間。
這種行爲就交給開發者了,它能夠被CAMediaTiming的fillMode來控制。fillMode是一個NSString類型,能夠接受以下四種常量:
kCAFillModeForwards
kCAFillModeBackwards
kCAFillModeBoth
kCAFillModeRemoved
默認是kCAFillModeRemoved,當動畫再也不播放的時候就顯示圖層模型指定的值。剩下的三種類型分別爲向前,向後或者既向前又向後去填充動畫狀態,使得動畫在開始前或者結束後仍然保持開始和結束那一刻的值。
這就對避免在動畫結束的時候急速返回提供另外一種方案。可是當用它來解決這個問題的時候,須要把removeOnCompletion設置爲NO,另外須要給動畫添加一個非空的鍵,因而能夠在不須要動畫的時候把它從圖層上移除。
※層級關係時間
每一個動畫和圖層在時間上都有它本身的層級概念,相對於它的父親來測量。對圖層調整時間將會影響到它自己和子圖層的動畫,但不會影響到父圖層。另外一個類似點是全部的動畫都被按照層級組合(使用CAAnimationGroup實例)。
對CALayer或者CAAnimationGroup調整duration和repeatCount/repeatDuration屬性並不會影響到子動畫。可是beginTime,timeOffset和speed屬性將會影響到子動畫。然而在層級關係中,beginTime指定了父圖層開始動畫(或者組合關係中的父動畫)和對象將要開始本身動畫之間的偏移。相似的,調整CALayer和CAGroupAnimation的speed屬性將會對動畫以及子動畫速度應用一個縮放的因子。
※全局時間和本地時間
CoreAnimation有一個全局時間的概念,也就是所謂的馬赫時間(「馬赫」其實是iOS和Mac OS系統內核的命名)。馬赫時間在設備上全部進程都是全局的--可是在不一樣設備上並非全局的--不過這已經足夠對動畫的參考點提供便利了,你能夠使用CACurrentMediaTime函數來訪問馬赫時間:
CFTimeInterval time = CACurrentMediaTime();
這個函數返回的值其實可有可無(它返回了設備自從上次啓動後的秒數,並非你所關心的),它真實的做用在於對動畫的時間測量提供了一個相對值。注意當設備休眠的時候馬赫時間會暫停,也就是全部的CAAnimations(基於馬赫時間)一樣也會暫停。
所以馬赫時間對長時間測量並不有用。好比用CACurrentMediaTime去更新一個實時鬧鐘並不明智。(能夠用[NSDate date]代替,就像第三章例子所示)。
每一個CALayer和CAAnimation實例都有本身本地時間的概念,是根據父圖層/動畫層級關係中的beginTime,timeOffset和speed屬性計算。就和轉換不一樣圖層之間座標關係同樣,CALayer一樣也提供了方法來轉換不一樣圖層之間的本地時間。以下:
- (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l;
- (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;
當用來同步不一樣圖層之間有不一樣的speed,timeOffset和beginTime的動畫,這些方法會頗有用。
※暫停,倒回和快進
設置動畫的speed屬性爲0能夠暫停動畫,但在動畫被添加到圖層以後不太可能再修改它了,因此不能對正在進行的動畫使用這個屬性。給圖層添加一個CAAnimation其實是給動畫對象作了一個不可改變的拷貝,因此對原始動畫對象屬性的改變對真實的動畫並無做用。相反,直接用-animationForKey:來檢索圖層正在進行的動畫能夠返回正確的動畫對象,可是修改它的屬性將會拋出異常。
若是移除圖層正在進行的動畫,圖層將會急速返回動畫以前的狀態。但若是在動畫移除以前拷貝呈現圖層到模型圖層,動畫將會看起來暫停在那裏。可是很差的地方在於以後就不能再恢復動畫了。
一個簡單的方法是能夠利用CAMediaTiming來暫停圖層自己。若是把圖層的speed設置成0,它會暫停任何添加到圖層上的動畫。相似的,設置speed大於1.0將會快進,設置成一個負值將會倒回動畫。
※手動動畫
timeOffset一個頗有用的功能在於它可讓你手動控制動畫進程,經過設置speed爲0,能夠禁用動畫的自動播放,而後來使用timeOffset來來回顯示動畫序列。這能夠使得運用手勢來手動控制動畫變得很簡單。由於在動畫添加到圖層以後不能再作修改了,咱們來經過調整layer的timeOffset達到一樣的效果(清單9.4)。
//例子:仍是以前關門的動畫,修改代碼來用手勢控制動畫。咱們給視圖添加一個UIPanGestureRecognizer,而後用timeOffset左右搖晃。
@property(nonatomic,strong) CALayer *doorLayer; //使用autoreverses屬性實現門的搖擺,autoreverses的屬性(BOOL類型)在每次間隔交替循環過程當中自動回放;在這裏咱們把repeatDuration設置爲INFINITY,因而動畫無限循環播放,設置repeatCount爲INFINITY也有一樣的效果。注意repeatCount和repeatDuration可能會相互衝突,因此你只要對其中一個指定非零值。對兩個屬性都設置非0值的行爲沒有被定義。 -(void)createShutDoorAnimation{ //添加圖片當作門 self.doorLayer=[CALayer layer]; self.doorLayer.frame=CGRectMake(0, 0, 128, 256); self.doorLayer.position=CGPointMake(150-64, 150); self.doorLayer.anchorPoint=CGPointMake(0, 0.5); self.doorLayer.contents=(__bridge id)[UIImage imageNamed:@"3.jpg"].CGImage; [self.view.layer addSublayer:self.doorLayer]; //應用角度變換 CATransform3D perspective=CATransform3DIdentity; perspective.m34=-1.0/500.0; self.view.layer.sublayerTransform=perspective; //應用門搖擺動畫 CABasicAnimation *animation=[CABasicAnimation animation]; animation.keyPath=@"transform.rotation.y"; animation.toValue=@(-M_PI_2); animation.duration=2.0; animation.repeatDuration=INFINITY; // animation.repeatCount=INFINITY; animation.autoreverses=YES; //添加平移手勢識別器來處理滑動 UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] init]; [pan addTarget:self action:@selector(pan:)]; [self.view addGestureRecognizer:pan]; //暫停全部層動畫 self.doorLayer.speed = 0.0; [self.doorLayer addAnimation:animation forKey:nil]; } - (void)pan:(UIPanGestureRecognizer *)pan { //獲得平移手勢的水平份量 CGFloat x = [pan translationInView:self.view].x; //使用一個合理的比例因子,從點轉換爲動畫持續時間 x /= 200.0f; //更新時間偏移和箝位結果 CFTimeInterval timeOffset = self.doorLayer.timeOffset; timeOffset = MIN(0.999, MAX(0.0, timeOffset - x)); self.doorLayer.timeOffset = timeOffset; //重置平移手勢 [pan setTranslation:CGPointZero inView:self.view]; }
運行效果,手平滑控制
※動畫速度
動畫實際上就是一段時間內的變換,這就暗示了變化必定是隨着某個特定的速率進行。速率計算公式:velocity=change/time
這裏的變化能夠指的是一個物體移動的距離,時間指動畫持續的時長,用這樣的一個移動能夠更加形象的描述(好比position和bounds屬性的動畫),但實際上它應用於任意能夠作動畫的屬性(好比color和opacity)。
上面的等式假設了速度在整個動畫過程當中都是恆定不變的,對於這種恆定速度的動畫咱們稱之爲「線性步調」,並且從技術的角度而言這也是實現動畫最簡單的方式,但也是徹底不真實的一種效果。
考慮一個場景,一輛車行駛在必定距離內,它會慢慢地加速到全速,而後當它接近終點的時候,它會慢慢地減速,直到最後停下來。那麼對於一個掉落到地上的物體又會怎樣呢?它會首先停在空中,而後一直加速到落到地面,而後忽然中止(而後因爲積累的動能轉換伴隨着一聲巨響,砰!)。
現實生活中的任何一個物體都會在運動中加速或者減速。那麼咱們如何在動畫中實現這種加速度呢?一種方法是使用物理引擎來對運動物體的摩擦和動量來建模,然而這會使得計算過於複雜。咱們稱這種類型的方程爲緩衝函數,Core Animation內嵌了一系列標準函數提供給咱們使用。
※CAMediaTimingFunction
使用緩衝方程式,首先須要設置CAAnimation的timingFunction屬性,是CAMediaTimingFunction類的一個對象。若是想改變隱式動畫的計時函數,一樣可能夠使用CATransaction的+setAnimationTimingFunction:方法。
建立CAMediaTimingFunction,最簡單的方式是調用+timingFunctionWithName:的構造方法。這裏傳入以下幾個常量之一:
kCAMediaTimingFunctionLinear 建立一個線性的計時函數,一樣也是CAAnimation的timingFunction屬性爲空時候的默認函數
kCAMediaTimingFunctionEaseIn 建立一個慢慢加速而後忽然中止的方法
kCAMediaTimingFunctionEaseOut 以一個全速開始,而後慢慢減速中止
kCAMediaTimingFunctionEaseInEaseOut 建立了一個慢慢加速而後在慢慢減速的過程
kCAMediaTimingFunctionDefault 它和kCAMediaTimingFunctionEaseInEaseOut很相似,可是加速和減速的過程都稍微有些慢。
//示例
@property (nonatomic, strong) CALayer *colorLayer; //緩衝函數的簡單測試 -(void)createBufferEaseInOut{ //建立圖層 self.colorLayer=[CALayer layer]; self.colorLayer.frame=CGRectMake(0, 0, 100, 100); self.colorLayer.position=CGPointMake(self.view.bounds.size.width/2.0, self.view.bounds.size.height/2.0); self.colorLayer.backgroundColor=[UIColor redColor].CGColor; [self.view.layer addSublayer:self.colorLayer]; } #pragma mark Action -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ //簡單的緩衝函數測試 [CATransaction begin]; [CATransaction setAnimationDuration:1.0]; [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]]; self.colorLayer.position=[[touches anyObject] locationInView:self.view]; [CATransaction commit]; }
※UIView的動畫緩衝
UIKit的動畫也一樣支持這些緩衝方法的使用,儘管語法和常量有些不一樣,爲了改變UIView動畫的緩衝選項,給options參數添加以下常量之一:
UIViewAnimationOptionCurveEaseInOut
UIViewAnimationOptionCurveEaseIn
UIViewAnimationOptionCurveEaseOut
UIViewAnimationOptionCurveLinear
它們和CAMediaTimingFunction緊密關聯,UIViewAnimationOptionCurveEaseInOut是默認值(這裏沒有kCAMediaTimingFunctionDefault相對應的值了)。
//示例
@property (nonatomic, strong) UIView *colorView; //UIView的動畫緩衝 -(void)createViewBuffer{ self.colorView = [[UIView alloc] init]; self.colorView.bounds = CGRectMake(0, 0, 100, 100); self.colorView.center = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2); self.colorView.backgroundColor = [UIColor redColor]; [self.view addSubview:self.colorView]; } #pragma mark Action -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ //UIView的動畫緩衝 [UIView animateWithDuration:1.0 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ self.colorView.center = [[touches anyObject] locationInView:self.view]; } completion:NULL]; }
運行效果
※緩衝和關鍵幀動畫
CAKeyframeAnimation有一個NSArray類型的timingFunctions屬性,咱們能夠用它來對每次動畫的步奏指定不一樣的計時函數。可是指定函數的個數必定要等於keyframes數組的元素個數減一,由於它是描述每一幀之間動畫速度的函數。
//示例
//隨機改變圖層顏色,對CAKeyframeAnimation使用CAMediaTimingFunction;kCAMediaTimingFunctionEaseIn,給圖層的顏色變化添加一點脈衝效果,讓它更像現實中的一個彩色燈泡。 -(void)createColorLayerView{ self.colorLayer=[CALayer layer]; self.colorLayer.frame=CGRectMake(50.0f, 50.0f, 100.0f, 100.0f); self.colorLayer.backgroundColor=[UIColor blueColor].CGColor; [self.view.layer addSublayer:self.colorLayer]; [NSTimer scheduledTimerWithTimeInterval:4 target:self selector:@selector(changeColor) userInfo:nil repeats:YES]; } -(void)changeColor{ //建立動畫 CAKeyframeAnimation *animation=[CAKeyframeAnimation animation]; animation.keyPath=@"backgroundColor"; animation.values=@[ (__bridge id)[UIColor blueColor].CGColor, (__bridge id)[UIColor redColor].CGColor, (__bridge id)[UIColor greenColor].CGColor, (__bridge id)[UIColor blueColor].CGColor ]; animation.duration = 4; CAMediaTimingFunction *fn=[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]; animation.timingFunctions=@[fn,fn,fn]; [self.colorLayer addAnimation:animation forKey:nil]; }
運行效果
※自定義緩衝函數
CAMediaTimingFunction一樣有另外一個構造函數,一個有四個浮點參數的+functionWithControlPoints::::(注意這裏奇怪的語法,並無包含具體每一個參數的名稱,這在objective-C中是合法的,可是卻違反了蘋果對方法命名的指導方針,並且看起來是一個奇怪的設計)。使用這個方法,咱們能夠建立一個自定義的緩衝函數。
※三次貝塞爾曲線
CAMediaTimingFunction有一個叫作-getControlPointAtIndex:values:的方法,能夠用來檢索曲線的點,這個方法的設計的確有點奇怪(或許也就只有蘋果能回答爲何不簡單返回一個CGPoint),可是使用它咱們能夠找到標準緩衝函數的點,而後用UIBezierPath和CAShapeLayer來把它畫出來。
※更加複雜的動畫曲線
當一個橡膠球掉落到堅硬的地面的場景,當開始下了落的時候,它會持續加速直到落到地面,而後通過幾回反彈,最後停下來,以下圖;
這種效果無法用一個簡單的三次貝塞爾曲線表示,因而不能用CAMediaTimingFunction來完成。但若是想要實現這樣的效果,能夠用以下幾種方法:
- 用CAKeyframeAnimation建立一個動畫,而後分割成幾個步驟,每一個小步驟使用本身的計時函數(具體下節介紹)。
- 使用定時器逐幀更新實現動畫
※基於關鍵幀的緩衝
爲了使用關鍵幀實現反彈動畫,咱們須要在緩衝曲線中對每個顯著的點建立一個關鍵幀(在這個狀況下,關鍵點也就是每次反彈的峯值),而後應用緩衝函數把每段曲線鏈接起來。同時,咱們也須要經過keyTimes來指定每一個關鍵幀的時間偏移,因爲每次反彈的時間都會減小,因而關鍵幀並不會均勻分佈。
※流程自動化
爲了實現自動化,咱們須要知道如何作以下兩件事情:
* 自動把任意屬性動畫分割成多個關鍵幀
* 用一個數學函數表示彈性動畫,使得能夠對幀作遍歷
爲了解決第一個問題,須要複製Core Animation的插值機制。這是一個傳入起點和終點,而後在這兩個點之間指定時間點產出一個新點的機制。對於簡單的浮點起始值,公式以下(假設時間從0到1):
value = (endValue – startValue) × time + startValue;
那麼若是要插入一個相似於CGPoint,CGColorRef或者CATransform3D這種更加複雜類型的值,咱們能夠簡單地對每一個獨立的元素應用這個方法(也就CGPoint中的x和y值,CGColorRef中的紅,藍,綠,透明值,或者是CATransform3D中獨立矩陣的座標)。咱們一樣須要一些邏輯在插值以前對對象拆解值,而後在插值以後在從新封裝成對象,也就是說須要實時地檢查類型。
一旦咱們能夠用代碼獲取屬性動畫的起始值之間的任意插值,咱們就能夠把動畫分割成許多獨立的關鍵幀,而後產出一個線性的關鍵幀動畫。
注意用到了60 x 動畫時間(秒作單位)做爲關鍵幀的個數,這時由於Core Animation按照每秒60幀去渲染屏幕更新,因此若是每秒生成60個關鍵幀,就能夠保證動畫足夠的平滑(儘管實際上極可能用更少的幀率就能夠達到很好的效果)。
在示例中僅僅引入了對CGPoint類型的插值代碼。可是,從代碼中很清楚能看出如何擴展成支持別的類型。做爲不能識別類型的備選方案,僅僅在前一半返回了fromValue,在後一半返回了toValue。
//示例,使用關鍵幀實現反彈球的動畫
@property (nonatomic, strong) UIImageView *ballView; //使用關鍵幀實現反彈球的動畫 -(void)createBounceBallAnimation{ UIImage *ballImage=[UIImage imageNamed:@"8.png"]; self.ballView=[[UIImageView alloc] initWithImage:ballImage]; self.ballView.layer.contentsScale=[UIScreen mainScreen].scale; [self.view addSubview:self.ballView]; [self animate]; } -(void)animate{ //復位球到屏幕頂部 self.ballView.center=CGPointMake(150, 32); //建立動畫參數 NSValue *fromValue=[NSValue valueWithCGPoint:CGPointMake(150, 32)]; NSValue *toValue=[NSValue valueWithCGPoint:CGPointMake(150, 268)]; CFTimeInterval duration=1.0; //生成關鍵幀 NSInteger numFrames=duration*60; NSMutableArray *frames=[NSMutableArray array]; for (int i=0; i<numFrames; i++) { float time=1/(float)numFrames*i; time=bounceEaseOut(time); [frames addObject:[self interpolateFromValue:fromValue toValue:toValue time:time]]; } //建立關鍵幀動畫 CAKeyframeAnimation *animation=[CAKeyframeAnimation animation]; animation.keyPath=@"position"; animation.duration=1.0; animation.delegate=self; animation.values= frames; self.ballView.layer.position=CGPointMake(150, 268); [self.ballView.layer addAnimation:animation forKey:nil]; } float quadraticEaseInOut(float t) { return (t < 0.5)? (2 * t * t): (-2 * t * t) + (4 * t) - 1; } float bounceEaseOut(float t) { if (t < 4/11.0) { return (121 * t * t)/16.0; } else if (t < 8/11.0) { return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0; } else if (t < 9/10.0) { return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0; } return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0; } float interpolate(float from,float to,float time){ return (to-from)*time+from; } -(id)interpolateFromValue:(id)fromValue toValue:(id)toValue time:(float)time{ if ([fromValue isKindOfClass:[NSValue class]]) { const char *type=[fromValue objCType]; if (strcmp(type, @encode(CGPoint))==0) { CGPoint from=[fromValue CGPointValue]; CGPoint to=[toValue CGPointValue]; CGPoint result=CGPointMake(interpolate(from.x, to.x, time), interpolate(from.y, to.y, time)); return [NSValue valueWithCGPoint:result]; } } return (time<0.5)?fromValue:toValue; } -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ //彈彈球動畫 [self animate]; }
運行效果
※定時幀
動畫看起來是用來顯示一段連續的運動過程,但實際上當在固定位置上展現像素的時候並不能作到這一點。通常來講這種顯示都沒法作到連續的移動,能作的僅僅是足夠快地展現一系列靜態圖片,只是看起來像是作了運動。
以前提到過iOS按照每秒60次刷新屏幕,而後CAAnimation計算出須要展現的新的幀,而後在每次屏幕更新的時候同步繪製上去,CAAnimation最機智的地方在於每次刷新須要展現的時候去計算插值和緩衝。
全部的Core Animation實際上都是按照必定的序列來顯示這些幀,那麼能夠本身作到這些麼?
※NSTimer
用NSTimer來作定時動畫,一秒鐘更新一次,可是若是咱們把頻率調整成一秒鐘更新60次的話,原理是徹底相同的。
用NSTimer來修改」緩衝」章中彈性球的例子。因爲如今在定時器啓動以後連續計算動畫幀,須要在類中添加一些額外的屬性來存儲動畫的fromValue,toValue,duration和當前的timeOffset
NSTimer並非最佳方案,爲了理解這點,咱們須要確切地知道NSTimer是如何工做的。iOS上的每一個線程都管理了一個NSRunloop,字面上看就是經過一個循環來完成一些任務列表。可是對主線程,這些任務包含以下幾項:
當設置一個NSTimer,它會被插入到當前任務列表中,而後直到指定時間過去以後纔會被執行。可是什麼時候啓動定時器並無一個時間上限,並且它只會在列表中上一個任務完成以後開始執行。這一般會致使有幾毫秒的延遲,可是若是上一個任務過了好久才完成就會致使延遲很長一段時間。
屏幕重繪的頻率是一秒鐘六十次,可是和定時器行爲同樣,若是列表中上一個執行了很長時間,它也會延遲。這些延遲都是一個隨機值,因而就不能保證定時器精準地一秒鐘執行六十次。有時候發生在屏幕重繪以後,這就會使得更新屏幕會有個延遲,看起來就是動畫卡殼了。有時候定時器會在屏幕更新的時候執行兩次,因而動畫看起來就跳動了。
能夠經過一些途徑來優化:
※CADisplayLink
CADisplayLink是CoreAnimation提供的另外一個相似於NSTimer的類,它老是在屏幕完成一次更新以前啓動,它的接口設計的和NSTimer很相似,因此它實際上就是一個內置實現的替代,可是和timeInterval以秒爲單位不一樣,CADisplayLink有一個整型的frameInterval屬性,指定了間隔多少幀以後才執行。默認值是1,意味着每次屏幕更新以前都會執行一次。可是若是動畫的代碼執行起來超過了六十分之一秒,你能夠指定frameInterval爲2,就是說動畫每隔一幀執行一次(一秒鐘30幀)或者3,也就是一秒鐘20次,等等。
用CADisplayLink而不是NSTimer,會保證幀率足夠連續,使得動畫看起來更加平滑,但即便CADisplayLink也不能保證每一幀都按計劃執行,一些失去控制的離散的任務或者事件(例如資源緊張的後臺程序)可能會致使動畫偶爾地丟幀。當使用NSTimer的時候,一旦有機會計時器就會開啓,可是CADisplayLink卻不同:若是它丟失了幀,就會直接忽略它們,而後在下一次更新的時候接着運行。
※計算幀的持續時間
不管是使用NSTimer仍是CADisplayLink,仍然須要處理一幀的時間超出了預期的六十分之一秒。因爲不可以計算出一幀真實的持續時間,因此須要手動測量。能夠在每幀開始刷新的時候用CACurrentMediaTime()記錄當前時間,而後和上一幀記錄的時間去比較。
經過比較這些時間,就能夠獲得真實的每幀持續的時間,而後代替硬編碼的六十分之一秒。
*Run Loop模式
注意到當建立CADisplayLink的時候,須要指定一個run loop和run loop mode,對於run loop來講,就使用了主線程的run loop,由於任何用戶界面的更新都須要在主線程執行,可是模式的選擇就並不那麼清楚了,每一個添加到run loop的任務都有一個指定了優先級的模式,爲了保證用戶界面保持平滑,iOS會提供和用戶界面相關任務的優先級,並且當UI很活躍的時候的確會暫停一些別的任務。
一個典型的例子就是當是用UIScrollview滑動的時候,重繪滾動視圖的內容會比別的任務優先級更高,因此標準的NSTimer和網絡請求就不會啓動,一些常見的run loop模式以下:
* NSDefaultRunLoopMode - 標準優先級
* NSRunLoopCommonModes - 高優先級
* UITrackingRunLoopMode - 用於UIScrollView和別的控件的動畫
在例子中,是用了NSDefaultRunLoopMode,可是不能保證動畫平滑的運行,因此就能夠用NSRunLoopCommonModes來替代。可是要當心,由於若是動畫在一個高幀率狀況下運行,你會發現一些別的相似於定時器的任務或者相似於滑動的其餘iOS動畫會暫停,直到動畫結束。
一樣能夠同時對CADisplayLink指定多個run loop模式,因而咱們能夠同時加入NSDefaultRunLoopMode和UITrackingRunLoopMode來保證它不會被滑動打斷,也不會被其餘UIKit控件動畫影響性能,像這樣:
self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode];
※物理模擬
即便使用了基於定時器的動畫來複制「緩存」章關鍵幀的行爲,但仍是會有一些本質上的區別:在關鍵幀的實現中,提早計算了全部幀,可是在新的解決方案中,實際上實在按須要在計算。意義在於能夠根據用戶輸入實時修改動畫的邏輯,或者和別的實時動畫系統例如物理引擎進行整合。
※Chipmunk
基於物理學建立一個真實的重力模擬效果來取代當前基於緩衝的彈性動畫,但即便模擬2D的物理效果就已近極其複雜了,因此就不要嘗試去實現它了,直接用開源的物理引擎庫好了。
將要使用的物理引擎叫作Chipmunk。另外的2D物理引擎也一樣能夠(例如Box2D),可是Chipmunk使用純C寫的,而不是C++,好處在於更容易和Objective-C項目整合。Chipmunk有不少版本,包括一個和Objective-C綁定的「indie」版本。C語言的版本是免費的,因此就用它好了。
//示例
@property (nonatomic, strong) UIImageView *ballView; @property (nonatomic, strong) CADisplayLink *timer; @property (nonatomic, assign) CFTimeInterval duration; @property (nonatomic, assign) CFTimeInterval timeOffset; @property (nonatomic, assign) CFTimeInterval lastStep; @property (nonatomic, strong) id fromValue; @property (nonatomic, strong) id toValue; //經過測量每幀持續時間,來使得動畫更平滑 -(void)createTestDurationPerFrame{ UIImage *ballImage=[UIImage imageNamed:@"8.png"]; self.ballView=[[UIImageView alloc] initWithImage:ballImage]; self.ballView.layer.contentsScale=[UIScreen mainScreen].scale; [self.view addSubview:self.ballView]; [self animate]; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { //replay animation on tap [self animate]; } float interpolateX(float from, float to, float time) { return (to - from) * time + from; } - (id)interpolateFromValue:(id)fromValue toValue:(id)toValue time:(float)time { if ([fromValue isKindOfClass:[NSValue class]]) { //get type const char *type = [(NSValue *)fromValue objCType]; if (strcmp(type, @encode(CGPoint)) == 0) { CGPoint from = [fromValue CGPointValue]; CGPoint to = [toValue CGPointValue]; CGPoint result = CGPointMake(interpolateX(from.x, to.x, time), interpolateX(from.y, to.y, time)); return [NSValue valueWithCGPoint:result]; } } //provide safe default implementation return (time < 0.5)? fromValue: toValue; } float bounceEaseOutX(float t) { if (t < 4/11.0) { return (121 * t * t)/16.0; } else if (t < 8/11.0) { return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0; } else if (t < 9/10.0) { return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0; } return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0; } - (void)animate { //reset ball to top of screen self.ballView.center = CGPointMake(150, 32); //configure the animation self.duration = 1.0; self.timeOffset = 0.0; self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)]; self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)]; //stop the timer if it's already running [self.timer invalidate]; //start the timer self.lastStep = CACurrentMediaTime(); self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)]; //一樣能夠同時對CADisplayLink指定多個run loop模式,因而咱們能夠同時加入NSDefaultRunLoopMode和UITrackingRunLoopMode來保證它不會被滑動打斷,也不會被其餘UIKit控件動畫影響性能,像這樣: [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode]; } - (void)step:(CADisplayLink *)timer { //calculate time delta CFTimeInterval thisStep = CACurrentMediaTime(); CFTimeInterval stepDuration = thisStep - self.lastStep; self.lastStep = thisStep; //update time offset self.timeOffset = MIN(self.timeOffset + stepDuration, self.duration); //get normalized time offset (in range 0 - 1) float time = self.timeOffset / self.duration; //apply easing time = bounceEaseOutX(time); //interpolate position id position = [self interpolateFromValue:self.fromValue toValue:self.toValue time:time]; //move ball view to new position self.ballView.center = [position CGPointValue]; //stop the timer if we've reached the end of the animation if (self.timeOffset >= self.duration) { [self.timer invalidate]; self.timer = nil; } }
運行效果
※CPU VS GPU
關於繪圖和動畫有兩種處理的方式:CPU(中央處理器)和GPU(圖形處理器)。在現代iOS設備中,都有能夠運行不一樣軟件的可編程芯片,可是因爲歷史緣由,能夠說CPU所作的工做都在軟件層面,而GPU在硬件層面。
總的來講,能夠用軟件(使用CPU)作任何事情,可是對於圖像處理,一般用硬件會更快,由於GPU使用圖像對高度並行浮點運算作了優化。因爲某些緣由,想盡量把屏幕渲染的工做交給硬件去處理。問題在於GPU並無無限制處理性能,並且一旦資源用完的話,性能就會開始降低了(即便CPU並無徹底佔用)
大多數動畫性能優化都是關於智能利用GPU和CPU,使得它們都不會超出負荷。因而首先須要知道Core Animation是如何在這兩個處理器之間分配工做的。
※動畫的舞臺
Core Animation處在iOS的核心地位:應用內和應用間都會用到它。一個簡單的動畫可能同步顯示多個app的內容,例如當在iPad上多個程序之間使用手勢切換,會使得多個程序同時顯示在屏幕上。在一個特定的應用中用代碼實現它是沒有意義的,由於在iOS中不可能實現這種效果(App都是被沙箱管理,不能訪問別的視圖)。
動畫和屏幕上組合的圖層實際上被一個單獨的進程管理,而不是你的應用程序。這個進程就是所謂的渲染服務。在iOS5和以前的版本是SpringBoard進程(同時管理着iOS的主屏)。在iOS6以後的版本中叫作BackBoard。
當運行一段動畫時候,這個過程會被四個分離的階段被打破:
* 佈局 - 這是準備你的視圖/圖層的層級關係,以及設置圖層屬性(位置,背景色,邊框等等)的階段。
* 顯示 - 這是圖層的寄宿圖片被繪製的階段。繪製有可能涉及你的-drawRect:和-drawLayer:inContext:方法的調用路徑。
* 準備 - 這是Core Animation準備發送動畫數據到渲染服務的階段。這同時也是Core Animation將要執行一些別的事務例如解碼動畫過程當中將要顯示的圖片的時間點。
* 提交 - 這是最後的階段,Core Animation打包全部圖層和動畫屬性,而後經過IPC(內部處理通訊)發送到渲染服務進行顯示。
可是這些僅僅階段僅僅發生在你的應用程序以內,在動畫在屏幕上顯示以前仍然有更多的工做。一旦打包的圖層和動畫到達渲染服務進程,它們會被反序列化來造成另外一個叫作渲染樹的圖層樹(在「圖層樹」章中提到過)。使用這個樹狀結構,渲染服務對動畫的每一幀作出以下工做:
* 對全部的圖層屬性計算中間值,設置OpenGL幾何形狀(紋理化的三角形)來執行渲染
* 在屏幕上渲染可見的三角形
因此一共有六個階段;最後兩個階段在動畫過程當中不停地重複。前五個階段都在軟件層面處理(經過CPU),只有最後一個被GPU執行。並且,你真正只能控制前兩個階段:佈局和顯示。Core Animation框架在內部處理剩下的事務,你也控制不了它。
這並非個問題,由於在佈局和顯示階段,你能夠決定哪些由CPU執行,哪些交給GPU去作。那麼改如何判斷呢?
※GPU的相關操做
GPU爲一個具體的任務作了優化:它用來採集圖片和形狀(三角形),運行變換,應用紋理和混合而後把它們輸送到屏幕上。現代iOS設備上可編程的GPU在這些操做的執行上又很大的靈活性,可是Core Animation並無暴露出直接的接口。除非你想繞開Core Animation並編寫你本身的OpenGL着色器,從根本上解決硬件加速的問題,那麼剩下的全部都仍是須要在CPU的軟件層面上完成。
寬泛的說,大多數CALayer的屬性都是用GPU來繪製。好比若是你設置圖層背景或者邊框的顏色,那麼這些能夠經過着色的三角板實時繪製出來。若是對一個contents屬性設置一張圖片,而後裁剪它 - 它就會被紋理的三角形繪製出來,而不須要軟件層面作任何繪製。
可是有一些事情會下降(基於GPU)圖層繪製,好比:
※CPU相關的操做
大多數工做在Core Animation的CPU都發生在動畫開始以前。這意味着它不會影響到幀率,因此很好,可是它會延遲動畫開始的時間,讓你的界面看起來會比較遲鈍。
如下CPU的操做都會延遲動畫的開始時間:
當圖層被成功打包,發送到渲染服務器以後,CPU仍然要作以下工做:爲了顯示屏幕上的圖層,Core Animation必須對渲染樹種的每一個可見圖層經過OpenGL循環轉換成紋理三角板。因爲GPU並不知曉Core Animation圖層的任何結構,因此必需要由CPU作這些事情。這裏CPU涉及的工做和圖層個數成正比,因此若是在你的層級關係中有太多的圖層,就會致使CPU每一幀的渲染,即便這些事情不是你的應用程序可控的。
※IO相關操做
還有一項沒涉及的就是IO相關工做。上下文中的IO(輸入/輸出)指的是例如閃存或者網絡接口的硬件訪問。一些動畫可能須要從閃存(甚至是遠程URL)來加載。一個典型的例子就是兩個視圖控制器之間的過渡效果,這就須要從一個nib文件或者是它的內容中懶加載,或者一個旋轉的圖片,可能在內存中尺寸太大,須要動態滾動來加載。
IO比內存訪問更慢,因此若是動畫涉及到IO,就是一個大問題。總的來講,這就須要使用聰敏但尷尬的技術,也就是多線程,緩存和投機加載(提早加載當前不須要的資源,可是以後可能須要用到)。
※測量而不是猜想
因而如今你知道有哪些點可能會影響動畫性能,那該如何修復呢?好吧,其實不須要。有不少種詭計來優化動畫,但若是盲目使用的話,可能會形成更多性能上的問題,而不是修復。
如何正確的測量而不是猜想這點很重要。根據性能相關的知識寫出代碼不一樣於倉促的優化。前者很好,後者實際上就是在浪費時間。
那該如何測量呢?第一步就是確保在真實環境下測試你的程序。
※真機測試,而不是模擬器
當你開始作一些性能方面的工做時,必定要在真機上測試,而不是模擬器。模擬器雖然是加快開發效率的一把利器,但它不能提供準確的真機性能參數。
模擬器運行在你的Mac上,然而Mac上的CPU每每比iOS設備要快。相反,Mac上的GPU和iOS設備的徹底不同,模擬器不得已要在軟件層面(CPU)模擬設備的GPU,這意味着GPU相關的操做在模擬器上運行的更慢,尤爲是使用CAEAGLLayer來寫一些OpenGL的代碼時候。
這就是說在模擬器上的測試出的性能會高度失真。若是動畫在模擬器上運行流暢,可能在真機上十分糟糕。若是在模擬器上運行的很卡,也可能在真機上很平滑。你沒法肯定。
另外一件重要的事情就是性能測試必定要用發佈配置,而不是調試模式。由於當用發佈環境打包的時候,編譯器會引入一系列提升性能的優化,例如去掉調試符號或者移除並從新組織代碼。你也能夠本身作到這些,例如在發佈環境禁用NSLog語句。你只關心發佈性能,那纔是你須要測試的點。
最後,最好在你支持的設備中性能最差的設備上測試:若是基於iOS6開發,這意味着最好在iPhone 3GS或者iPad2上測試。若是可能的話,測試不一樣的設備和iOS版本,由於蘋果在不一樣的iOS版本和設備中作了一些改變,這也可能影響到一些性能。例如iPad3明顯要在動畫渲染上比iPad2慢不少,由於渲染4倍多的像素點(爲了支持視網膜顯示)。
※保持一致的幀率
爲了作到動畫的平滑,你須要以60FPS(幀每秒)的速度運行,以同步屏幕刷新速率。經過基於NSTimer或者CADisplayLink的動畫你能夠下降到30FPS,並且效果還不錯,可是沒辦法經過Core Animation作到這點。若是不保持60FPS的速率,就可能隨機丟幀,影響到體驗。
你能夠在使用的過程當中明顯感到有沒有丟幀,但沒辦法經過肉眼來獲得具體的數據,也無法知道你的作法有沒有真的提升性能。你須要的是一系列精確的數據。
你能夠在程序中用CADisplayLink來測量幀率(「基於定時器的動畫」章中那樣),而後在屏幕上顯示出來,但應用內的FPS顯示並不可以徹底真實測量出Core Animation性能,由於它僅僅測出應用內的幀率。咱們知道不少動畫都在應用以外發生(在渲染服務器進程中處理),但同時應用內FPS計數的確能夠對某些性能問題提供參考,一旦找出一個問題的地方,你就須要獲得更多精確詳細的數據來定位到問題所在。蘋果提供了一個強大的Instruments工具集來幫咱們作到這些。
※Instruments
Instruments是Xcode用來分析程序性能,分析消耗,Instruments的一個很棒的功能在於它能夠建立自定義的工具集(在Xcode->Open Developer Tool->Instruments打開)。除了你初始選擇的工具以外,若是在Instruments中打開右上角+號能夠添加其餘工具分析
這裏說下面幾個工具
Core Animation - 用來調試各類Core Animation性能問題。
OpenGL ES驅動 - 用來調試GPU性能問題。這個工具在編寫Open GL代碼的時候頗有用,但有時也用來處理Core Animation的工做。
※時間分析器
時間分析器工具用了檢測CPU的使用狀況。它能夠告訴咱們程序中的哪一個方法正在消耗大量的CPU時間。使用大量的CPU並不必定是個問題 - 你可能指望動畫路徑對CPU很是依賴,由於動畫每每是iOS設備中最苛刻的任務。可是若是你有性能問題,查看CPU時間對於判斷性能是否是和CPU相關,以及定位到函數都頗有幫助。
時間分析器有一些選項來幫助咱們定位到咱們關心的的方法。能夠使用左側的複選框來打開。其中最有用的是以下幾點:
- 經過線程分離 - 這能夠經過執行的線程進行分組。若是代碼被多線程分離的話,那麼就能夠判斷究竟是哪一個線程形成了問題。
- 隱藏系統庫 - 能夠隱藏全部蘋果的框架代碼,來幫助咱們尋找哪一段代碼形成了性能瓶頸。因爲咱們不能優化框架方法,因此這對定位到咱們能實際修復的代碼頗有用。
- 只顯示Obj-C代碼 - 隱藏除了Objective-C以外的全部代碼。大多數內部的Core Animation代碼都是用C或者C++函數,因此這對咱們集中精力到咱們代碼中顯式調用的方法就頗有用。
※Core Animation
Core Animation工具用來監測Core Animation性能。它給咱們提供了週期性的FPS,而且考慮到了發生在程序以外的動畫。
Core Animation工具也提供了一系列複選框選項來幫助調試渲染瓶頸:
* Color Blended Layers - 這個選項基於渲染程度對屏幕中的混合區域進行綠到紅的高亮(也就是多個半透明圖層的疊加)。因爲重繪的緣由,混合對GPU性能會有影響,同時也是滑動或者動畫幀率降低的罪魁禍首之一。
* ColorHitsGreenandMissesRed - 當使用shouldRasterizep屬性的時候,耗時的圖層繪製會被緩存,而後當作一個簡單的扁平圖片呈現。當緩存再生的時候這個選項就用紅色對柵格化圖層進行了高亮。若是緩存頻繁再生的話,就意味着柵格化可能會有負面的性能影響了(更多關於使用shouldRasterize的細節見第15章「圖層性能」)。
* Color Copied Images - 有時候寄宿圖片的生成意味着Core Animation被強制生成一些圖片,而後發送到渲染服務器,而不是簡單的指向原始指針。這個選項把這些圖片渲染成藍色。複製圖片對內存和CPU使用來講都是一項很是昂貴的操做,因此應該儘量的避免。
* Color Immediately - 一般Core Animation Instruments以每毫秒10次的頻率更新圖層調試顏色。對某些效果來講,這顯然太慢了。這個選項就能夠用來設置每幀都更新(可能會影響到渲染性能,並且會致使幀率測量不許,因此不要一直都設置它)。
* Color Misaligned Images - 這裏會高亮那些被縮放或者拉伸以及沒有正確對齊到像素邊界的圖片(也就是非整型座標)。這些中的大多數一般都會致使圖片的不正常縮放,若是把一張大圖當縮略圖顯示,或者不正確地模糊圖像,那麼這個選項將會幫你識別出問題所在。
* Color Offscreen-Rendered Yellow - 這裏會把那些須要離屏渲染的圖層高亮成黃色。這些圖層極可能須要用shadowPath或者shouldRasterize來優化。
* Color OpenGL Fast Path Blue - 這個選項會對任何直接使用OpenGL繪製的圖層進行高亮。若是僅僅使用UIKit或者Core Animation的API,那麼不會有任何效果。若是使用GLKView或者CAEAGLLayer,那若是不顯示藍色塊的話就意味着你正在強制CPU渲染額外的紋理,而不是繪製到屏幕。
* Flash Updated Regions - 這個選項會對重繪的內容高亮成黃色(也就是任何在軟件層面使用Core Graphics繪製的圖層)。這種繪圖的速度很慢。若是頻繁發生這種狀況的話,這意味着有一個隱藏的bug或者說經過增長緩存或者使用替代方案會有提高性能的空間。
※OpenGL ES驅動
OpenGL ES驅動工具能夠幫你測量GPU的利用率,一樣也是一個很好的來判斷和GPU相關動畫性能的指示器。它一樣也提供了相似Core Animation那樣顯示FPS的工具。
OpenGL ES驅動工具
側欄的右邊是一系列有用的工具。其中和Core Animation性能最相關的是以下幾點:
* Renderer Utilization - 若是這個值超過了~50%,就意味着你的動畫可能對幀率有所限制,極可能由於離屏渲染或者是重繪致使的過分混合。
* Tiler Utilization - 若是這個值超過了~50%,就意味着你的動畫可能限制於幾何結構方面,也就是在屏幕上有太多的圖層佔用了。
※軟件繪圖
術語繪圖一般在Core Animation的上下文中指代軟件繪圖(意即:不禁GPU協助的繪圖)。在iOS中,軟件繪圖一般是由Core Graphics框架完成來完成。可是,在一些必要的狀況下,相比Core Animation和OpenGL,Core Graphics要慢了很多。
軟件繪圖不只效率低,還會消耗可觀的內存。CALayer只須要一些與本身相關的內存:只有它的寄宿圖會消耗必定的內存空間。即便直接賦給contents屬性一張圖片,也不須要增長額外的照片存儲大小。若是相同的一張圖片被多個圖層做爲contents屬性,那麼他們將會共用同一塊內存,而不是複製內存塊。
可是一旦你實現了CALayerDelegate協議中的-drawLayer:inContext:方法或者UIView中的-drawRect:方法(其實就是前者的包裝方法),圖層就建立了一個繪製上下文,這個上下文須要的大小的內存可從這個算式得出:圖層寬*圖層高*4字節,寬高的單位均爲像素。對於一個在Retina iPad上的全屏圖層來講,這個內存量就是 2048*1526*4字節,至關於12MB內存,圖層每次重繪的時候都須要從新抹掉內存而後從新分配。
軟件繪圖的代價昂貴,除非絕對必要,你應該避免重繪你的視圖。提升繪製性能的祕訣就在於儘可能避免去繪製。
※矢量圖形
用Core Graphics來繪圖的一個一般緣由就是隻是用圖片或是圖層效果不能輕易地繪製出矢量圖形。矢量繪圖包含一下這些:
※髒矩形
有時候用CAShapeLayer或者其餘矢量圖形圖層替代Core Graphics並非那麼切實可行。好比繪圖應用:用線條完美地完成了矢量繪製。可是設想一下若是咱們能進一步提升應用的性能,讓它就像一個黑板同樣工做,而後用『粉筆』來繪製線條。模擬粉筆最簡單的方法就是用一個『線刷』圖片而後將它粘貼到用戶手指碰觸的地方,可是這個方法用CAShapeLayer沒辦法實現。
能夠給每一個『線刷』建立一個獨立的圖層,可是實現起來有很大的問題。屏幕上容許同時出現圖層上線數量大約是幾百,那樣很快就會超出的。這種狀況下沒什麼辦法,就用Core Graphics吧(除非你想用OpenGL作一些更復雜的事情)。
※異步繪製
針對這個問題,有一些方法能夠用到:一些狀況下,能夠推測性地提早在另一個線程上繪製內容,而後將由此繪出的圖片直接設置爲圖層的內容。這實現起來可能不是很方便,可是在特定狀況下是可行的。Core Animation提供了一些選擇:CATiledLayer
和drawsAsynchronously
屬性。
※CATiledLayer
※drawsAsynchronously
它與CATiledLayer
使用的異步繪製並不相同。它本身的-drawLayer:inContext:
方法只會在主線程調用,可是CGContext並不等待每一個繪製命令的結束。相反地,它會將命令加入隊列,當方法返回時,在後臺線程逐個執行真正的繪製。
根據蘋果的說法。這個特性在須要頻繁重繪的視圖上效果最好(好比咱們的繪圖應用,或者諸如UITableViewCell
之類的),對那些只繪製一次或不多重繪的圖層內容來講沒什麼太大的幫助。
//示例
//TouchPathDrawingView.h
// // TouchPathDrawingView.h // CoreAnimationLearn // // Created by Vie on 2017/8/7. // Copyright © 2017年 Vie. All rights reserved. // #import <UIKit/UIKit.h> @interface TouchPathDrawingView : UIView @end
//TouchPathDrawingView.m
// // TouchPathDrawingView.m // CoreAnimationLearn // // Created by Vie on 2017/8/7. // Copyright © 2017年 Vie. All rights reserved. // #import "TouchPathDrawingView.h" @interface TouchPathDrawingView() @property (nonatomic, strong) UIBezierPath *path; @end @implementation TouchPathDrawingView //用CAShapeLayer替代Core Graphics,性能就會獲得提升,也避免了創造一個寄宿圖 +(Class)layerClass{ return [CAShapeLayer class]; } -(instancetype)initWithFrame:(CGRect)frame{ if (self=[super initWithFrame:frame]) { self.path=[[UIBezierPath alloc] init]; CAShapeLayer *shapeLayer = (CAShapeLayer *)self.layer; shapeLayer.strokeColor = [UIColor redColor].CGColor; shapeLayer.fillColor = [UIColor clearColor].CGColor; shapeLayer.lineJoin = kCALineJoinRound; shapeLayer.lineCap = kCALineCapRound; shapeLayer.lineWidth = 5; self.path.lineWidth=5; } return self; } -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ CGPoint point=[[touches anyObject] locationInView:self]; [self.path moveToPoint:point]; } -(void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ CGPoint point=[[touches anyObject] locationInView:self]; [self.path addLineToPoint:point]; ((CAShapeLayer *)self.layer).path=self.path.CGPath; } @end
運行效果
※加載和潛伏
繪圖實際消耗的時間一般並非影響性能的因素。圖片消耗很大一部份內存,並且不太可能把須要顯示的圖片都保留在內存中,因此須要在應用運行的時候週期性地加載和卸載圖片。
圖片文件的加載速度同時受到CPU及IO(輸入/輸出)延遲的影響。iOS設備中的閃存已經比傳統硬盤快不少了,但仍然比RAM慢將近200倍左右,這就須要謹慎地管理加載,以免延遲。
只要有可能,就應當設法在程序生命週期中不易察覺的時候加載圖片,例如啓動,或者在屏幕切換的過程當中。按下按鈕和按鈕響應事件之間最大的延遲大概是200ms,遠遠超過動畫幀切換所須要的16ms。你能夠在程序首次啓動的時候加載圖片,可是若是20秒內沒法啓動程序的話,iOS檢測計時器就會終止你的應用(並且若是啓動時間超出2或3秒的話,用戶就會抱怨)。
有些時候,提早加載全部的東西並不明智。好比說包含上千張圖片的圖片傳送帶:用戶但願可以平滑快速翻動圖片,因此就不可能提早預加載全部的圖片;那樣會消耗太多的時間和內存。
有時候圖片也須要從遠程網絡鏈接中下載,這將會比從磁盤加載要消耗更多的時間,甚至可能因爲鏈接問題而加載失敗(在幾秒鐘嘗試以後)。你不能在主線程中加載網絡,並在屏幕凍結期間指望用戶去等待它,因此須要後臺線程。
※線程加載
在「性能調優」章的聯繫人列表例子中,圖片都很是小,因此能夠在主線程同步加載。可是對於大圖來講,這樣作就不太合適了,由於加載會消耗很長時間,形成滑動的不流暢。滑動動畫會在主線程的run loop中更新,它們是在渲染服務進程中運行的,並所以更容易比CAAnimation遭受CPU相關的性能問題。
這裏提高性能惟一的方式就是在另外一個線程中加載圖片。這並不可以下降實際的加載時間(可能狀況會更糟,由於系統可能要消耗CPU時間來處理加載的圖片數據),可是主線程可以有時間作一些別的事情,好比響應用戶輸入,以及滑動動畫。
爲了在後臺線程加載圖片,能夠使用GCD或者NSOperationQueue建立自定義線程,或者使用CATiledLayer。爲了從遠程網絡加載圖片,能夠使用異步的NSURLConnection,可是對本地存儲的圖片,並不十分有效。
※GCD和NSOperationQueue
GCD(Grand Central Dispatch)和NSOperationQueue很相似,都提供了隊列閉包塊來在線程中按必定順序來執行。NSOperationQueue有一個Objecive-C接口(而不是使用GCD的全局C函數),一樣在操做優先級和依賴關係上提供了很好的粒度控制,可是須要更多地設置代碼。
※延遲解壓
一旦圖片文件被加載就必需要進行解碼,解碼過程是一個至關複雜的任務,須要消耗很是長的時間。解碼後的圖片將一樣使用至關大的內存。
用於加載的CPU時間相對於解碼來講根據圖片格式而不一樣。對於PNG圖片來講,加載會比JPEG更長,由於文件可能更大,可是解碼會相對較快,並且Xcode會把PNG圖片進行解碼優化以後引入工程。JPEG圖片更小,加載更快,可是解壓的步驟要消耗更長的時間,由於JPEG解壓算法比基於zip的PNG算法更加複雜。
當加載圖片的時候,iOS一般會延遲解壓圖片的時間,直到加載到內存以後。這就會在準備繪製圖片的時候影響性能,由於須要在繪製以前進行解壓(一般是消耗時間的問題所在)。
最簡單的方法就是使用UIImage的+imageNamed:方法避免延時加載。不像+imageWithContentsOfFile:(和其餘別的UIImage加載方法),這個方法會在加載圖片以後馬上進行解壓(就和本章以前談到的好處同樣)。問題在於+imageNamed:只對從應用資源束中的圖片有效,因此對用戶生成的圖片內容或者是下載的圖片就無法使用了。
另外一種馬上加載圖片的方法就是把它設置成圖層內容,或者是UIImageView的image屬性。不幸的是,這又須要在主線程執行,因此不會對性能有所提高。
第三種方式就是繞過UIKit,使用ImageIO框架,這樣就能夠使用kCGImageSourceShouldCache來建立圖片,強制圖片馬上解壓,而後在圖片的生命週期保留解壓後的版本。
最後一種方式就是使用UIKit加載圖片,可是須要馬上將它繪製到CGContext中去。圖片必需要在繪製以前解壓,因此就要當即強制解壓。這樣的好處在於繪製圖片能夠在後臺線程(例如加載自己)中執行,而不會阻塞UI。
有兩種方式能夠爲強制解壓提早渲染圖片:
* 將圖片的一個像素繪製成一個像素大小的CGContext。這樣仍然會解壓整張圖片,可是繪製自己並無消耗任什麼時候間。這樣的好處在於加載的圖片並不會在特定的設備上爲繪製作優化,因此能夠在任什麼時候間點繪製出來。一樣iOS也就能夠丟棄解壓後的圖片來節省內存了。
* 將整張圖片繪製到CGContext中,丟棄原始的圖片,而且用一個從上下文內容中新的圖片來代替。這樣比繪製單一像素那樣須要更加複雜的計算,可是所以產生的圖片將會爲繪製作優化,並且因爲原始壓縮圖片被拋棄了,iOS就不可以隨時丟棄任何解壓後的圖片來節省內存了。
須要注意的是蘋果特別推薦了不要使用這些詭計來繞過標準圖片解壓邏輯(因此也是他們選擇用默認處理方式的緣由),可是若是你使用不少大圖來構建應用,那若是想提高性能,就只能和系統博弈了。
若是不使用+imageNamed:,那麼把整張圖片繪製到CGContext多是最佳的方式了。儘管你可能認爲多餘的繪製相較別的解壓技術而言性能不是很高,可是新建立的圖片(在特定的設備上作過優化)可能比原始圖片繪製的更快。
一樣,若是想顯示圖片到比原始尺寸小的容器中,那麼一次性在後臺線程從新繪製到正確的尺寸會比每次顯示的時候都作縮放會更有效(儘管在這個例子中咱們加載的圖片呈現正確的尺寸,因此不須要多餘的優化)。
//示例
//ImageIOViewController.h文件
// // ImageIOViewController.m // CoreAnimationLearn // // Created by Vie on 2017/8/7. // Copyright © 2017年 Vie. All rights reserved. // #import "ImageIOViewController.h" @interface ImageIOViewController ()<UICollectionViewDataSource> @property (nonatomic, copy) NSArray *imagePaths; @property (nonatomic, strong) UICollectionView *collectionView; @end @implementation ImageIOViewController #pragma mark 視圖控制器回調 - (void)viewDidLoad { [super viewDidLoad]; [self.view setBackgroundColor:RGB_ALPHA_COLOR(230, 230, 230, 1)]; UIBarButtonItem *leftItem=[[UIBarButtonItem alloc] initWithTitle:@"<" style:UIBarButtonItemStylePlain target:self action:@selector(backAction:)]; self.navigationItem.leftBarButtonItem=leftItem; //讓約束從導航欄底部開始算起 self.edgesForExtendedLayout = UIRectEdgeNone; self.extendedLayoutIncludesOpaqueBars = NO; self.modalPresentationCapturesStatusBarAppearance = NO; UICollectionViewFlowLayout *layout=[[UICollectionViewFlowLayout alloc] init]; layout.itemSize=CGSizeMake(self.view.frame.size.width, 200); self.collectionView=[[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:layout]; self.collectionView.dataSource=self; //register cell class [self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"Cell"]; [self.view addSubview:self.collectionView]; //set up data self.imagePaths = [[NSBundle mainBundle] pathsForResourcesOfType:@"jpg" inDirectory:nil]; } -(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{ return [self.imagePaths count]; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { //dequeue cell UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath]; //add image view const NSInteger imageTag = 99; UIImageView *imageView = (UIImageView *)[cell viewWithTag:imageTag]; if (!imageView) { imageView = [[UIImageView alloc] initWithFrame: cell.contentView.bounds]; imageView.tag = imageTag; [cell.contentView addSubview:imageView]; } cell.tag=indexPath.row; imageView.image=nil; //使用GCD加載傳送圖片 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ NSInteger index=indexPath.row; NSString *imagePath=self.imagePaths[index]; UIImage *image=[UIImage imageWithContentsOfFile:imagePath]; //redraw image using device context;將整張圖片繪製到CGContext中,丟棄原始的圖片,而且用一個從上下文內容中新的圖片來代替。這樣比繪製單一像素那樣須要更加複雜的計算,可是所以產生的圖片將會爲繪製作優化,並且因爲原始壓縮圖片被拋棄了,iOS就不可以隨時丟棄任何解壓後的圖片來節省內存了。 UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, YES, 0); [image drawInRect:imageView.bounds]; image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); dispatch_async(dispatch_get_main_queue(), ^{ if (index==cell.tag) { imageView.image=image; }; }); }); return cell; } #pragma mark Action -(void)backAction:(id)sender{ [self.navigationController popViewControllerAnimated:YES]; } #pragma mark 其餘方法 @end
運行效果
※CATiledLayer
如「專用圖層」章中的例子所示,CATiledLayer能夠用來異步加載和顯示大型圖片,而不阻塞用戶輸入。可是一樣能夠使用CATiledLayer在UICollectionView中爲每一個表格建立分離的CATiledLayer實例加載傳動器圖片,每一個表格僅使用一個圖層。
這樣使用CATiledLayer有幾個潛在的弊端:
* CATiledLayer的隊列和緩存算法沒有暴露出來,因此只能祈禱它能匹配需求
* CATiledLayer須要每次重繪圖片到CGContext中,即便它已經解壓縮,並且和單元格尺寸同樣(所以能夠直接用做圖層內容,而不須要重繪)。
須要解釋幾點:
* CATiledLayer的tileSize屬性單位是像素,而不是點,因此爲了保證瓦片和表格尺寸一致,須要乘以屏幕比例因子。
* 在-drawLayer:inContext:方法中,須要知道圖層屬於哪個indexPath以加載正確的圖片。這裏利用了CALayer的KVC來存儲和檢索任意的值,將圖層和索引打標籤。
結果CATiledLayer工做的很好,性能問題解決了,並且和用GCD實現的代碼量差很少。僅有一個問題在於圖片加載到屏幕上後有一個明顯的淡入。
能夠調整CATiledLayer的fadeDuration屬性來調整淡入的速度,或者直接將整個漸變移除,可是這並無根本性地去除問題:在圖片加載到準備繪製的時候總會有一個延遲,這將會致使滑動時候新圖片的跳入。這並非CATiledLayer的問題,使用GCD的版本也有這個問題。
即便使用上述討論的全部加載圖片和緩存的技術,有時候仍然會發現實時加載大圖仍是有問題。就和「高效繪圖」章中提到的那樣,iPad上一整個視網膜屏圖片分辨率達到了2048x1536,並且會消耗12MB的RAM(未壓縮)。第三代iPad的硬件並不能支持1/60秒的幀率加載,解壓和顯示這種圖片。即便用後臺線程加載來避免動畫卡頓,仍然解決不了問題。能夠在加載的同時顯示一個佔位圖片,但這並無根本解決問題,能夠作到更好。
//示例
// // ImageIOViewController.m // CoreAnimationLearn // // Created by Vie on 2017/8/7. // Copyright © 2017年 Vie. All rights reserved. // #import "ImageIOViewController.h" @interface ImageIOViewController ()<UICollectionViewDataSource,CALayerDelegate> @property (nonatomic, copy) NSArray *imagePaths; @property (nonatomic, strong) UICollectionView *collectionView; @end @implementation ImageIOViewController #pragma mark 視圖控制器回調 - (void)viewDidLoad { [super viewDidLoad]; [self.view setBackgroundColor:RGB_ALPHA_COLOR(230, 230, 230, 1)]; UIBarButtonItem *leftItem=[[UIBarButtonItem alloc] initWithTitle:@"<" style:UIBarButtonItemStylePlain target:self action:@selector(backAction:)]; self.navigationItem.leftBarButtonItem=leftItem; //讓約束從導航欄底部開始算起 self.edgesForExtendedLayout = UIRectEdgeNone; self.extendedLayoutIncludesOpaqueBars = NO; self.modalPresentationCapturesStatusBarAppearance = NO; UICollectionViewFlowLayout *layout=[[UICollectionViewFlowLayout alloc] init]; layout.itemSize=CGSizeMake(self.view.frame.size.width, 200); self.collectionView=[[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:layout]; self.collectionView.dataSource=self; //register cell class [self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"Cell"]; [self.view addSubview:self.collectionView]; //set up data self.imagePaths = [[NSBundle mainBundle] pathsForResourcesOfType:@"jpg" inDirectory:nil]; } -(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{ return [self.imagePaths count]; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { //dequeue cell UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath]; //使用CATiledLayer能夠用來異步加載和顯示大型圖片,而不阻塞用戶輸入 CATiledLayer *tileLayer=(CATiledLayer *)[cell.contentView.layer.sublayers lastObject]; if (!tileLayer) { tileLayer=[CATiledLayer layer]; tileLayer.frame=cell.bounds; tileLayer.contentsScale=[UIScreen mainScreen].scale; tileLayer.tileSize=CGSizeMake(cell.bounds.size.width*[UIScreen mainScreen].scale, cell.bounds.size.height*[UIScreen mainScreen].scale); tileLayer.delegate=self; [tileLayer setValue:@(indexPath.row) forKey:@"index"]; [cell.contentView.layer addSublayer:tileLayer]; } tileLayer.contents=nil; [tileLayer setValue:@(indexPath.row) forKey:@"index"]; [tileLayer setNeedsDisplay]; return cell; } - (void)drawLayer:(CATiledLayer *)layer inContext:(CGContextRef)ctx { //get image index NSInteger index = [[layer valueForKey:@"index"] integerValue]; //load tile image NSString *imagePath = self.imagePaths[index]; UIImage *tileImage = [UIImage imageWithContentsOfFile:imagePath]; //calculate image rect CGFloat aspectRatio = tileImage.size.height / tileImage.size.width; CGRect imageRect = CGRectZero; imageRect.size.width = layer.bounds.size.width; imageRect.size.height = layer.bounds.size.height * aspectRatio; imageRect.origin.y = (layer.bounds.size.height - imageRect.size.height)/2; //draw tile UIGraphicsPushContext(ctx); [tileImage drawInRect:imageRect]; UIGraphicsPopContext(); } #pragma mark Action -(void)backAction:(id)sender{ [self.navigationController popViewControllerAnimated:YES]; } #pragma mark 其餘方法 @end
運行效果
※分辨率交換
視網膜分辨率(根據蘋果營銷定義)表明了人的肉眼在正常視角距離可以分辨的最小像素尺寸。可是這隻能應用於靜態像素。當觀察一個移動圖片時,你的眼睛就會對細節不敏感,因而一個低分辨率的圖片和視網膜質量的圖片沒什麼區別了。
若是須要快速加載和顯示移動大圖,簡單的辦法就是欺騙人眼,在移動傳送器的時候顯示一個小圖(或者低分辨率),而後當中止的時候再換成大圖。這意味着須要對每張圖片存儲兩份不一樣分辨率的副本,可是幸運的是,因爲須要同時支持Retina和非Retina設備,原本這就是廣泛要作到的。
若是從遠程源或者用戶的相冊加載沒有可用的低分辨率版本圖片,那就能夠動態將大圖繪製到較小的CGContext,而後存儲到某處以備複用。
爲了作到圖片交換,須要利用UIScrollView的一些實現UIScrollViewDelegate協議的委託方法(和其餘相似於UITableView和UICollectionView基於滾動視圖的控件同樣):
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
能夠使用這幾個方法來檢測傳送器是否中止滾動,而後加載高分辨率的圖片。只要高分辨率圖片和低分辨率圖片尺寸顏色保持一致,你會很難察覺到替換的過程(確保在同一臺機器使用相同的圖像程序或者腳本生成這些圖片)。
※緩存
若是有不少張圖片要顯示,提早把它們所有都加載進去是不切實際的,可是,這並不意味着,你在遇到加載問題後,當其移出屏幕時就馬上將其銷燬。經過選擇性的緩存,你就能夠避免來回滾動時圖片重複性的加載了。
緩存其實很簡單:就是將昂貴計算後的結果(或者是從閃存或者網絡加載的文件)存儲到內存中,以便後續使用,這樣訪問起來很快。問題在於緩存本質上是一個權衡過程 - 爲了提高性能而消耗了內存,可是因爲內存是一個很是寶貴的資源,因此不能把全部東西都作緩存。
什麼時候將何物作緩存(作多久)並不老是很明顯。幸運的是,大多狀況下,iOS都作好了圖片的緩存。
+imageNamed:方法
以前提到使用[UIImage imageNamed:]加載圖片有個好處在於能夠馬上解壓圖片而不用等到繪製的時候。可是[UIImage imageNamed:]方法有另外一個很是顯著的好處:它在內存中自動緩存瞭解壓後的圖片,即便你本身沒有保留對它的任何引用。
對於iOS應用那些主要的圖片(例如圖標,按鈕和背景圖片),使用[UIImage imageNamed:]加載圖片是最簡單最有效的方式。在nib文件中引用的圖片一樣也是這個機制,因此你不少時候都在隱式的使用它。
可是[UIImage imageNamed:]並不適用任何狀況。它爲用戶界面作了優化,可是並非對應用程序須要顯示的全部類型的圖片都適用。有些時候你仍是要實現本身的緩存機制,緣由以下:
* [UIImage imageNamed:]方法僅僅適用於在應用程序資源束目錄下的圖片,可是大多數應用的許多圖片都要從網絡或者是用戶的相機中獲取,因此[UIImage imageNamed:]就無法用了。
* [UIImage imageNamed:]緩存用來存儲應用界面的圖片(按鈕,背景等等)。若是對照片這種大圖也用這種緩存,那麼iOS系統就極可能會移除這些圖片來節省內存。那麼在切換頁面時性能就會降低,由於這些圖片都須要從新加載。對傳送器的圖片使用一個單獨的緩存機制就能夠把它和應用圖片的生命週期解耦。
* [UIImage imageNamed:]緩存機制並非公開的,因此你不能很好地控制它。例如,你無法作到檢測圖片是否在加載以前就作了緩存,不可以設置緩存大小,當圖片沒用的時候也不能把它從緩存中移除。
※自定義緩存
構建一個所謂的緩存系統很是困難。菲爾 卡爾頓曾經說過:「在計算機科學中只有兩件難事:緩存和命名」。
若是要寫本身的圖片緩存的話,那該如何實現呢?讓咱們來看看要涉及哪些方面:
* 選擇一個合適的緩存鍵 - 緩存鍵用來作圖片的惟一標識。若是實時建立圖片,一般不太好生成一個字符串來區分別的圖片。在圖片傳送帶例子中就很簡單,能夠用圖片的文件名或者表格索引。
* 提早緩存 - 若是生成和加載數據的代價很大,你可能想當第一次須要用到的時候再去加載和緩存。提早加載的邏輯是應用內在就有的,可是在咱們的例子中,這也很是好實現,由於對於一個給定的位置和滾動方向,咱們就能夠精確地判斷出哪一張圖片將會出現。
* 緩存失效 - 若是圖片文件發生了變化,怎樣才能通知到緩存更新呢?這是個很是困難的問題(就像菲爾 卡爾頓提到的),可是幸運的是當從程序資源加載靜態圖片的時候並不須要考慮這些。對用戶提供的圖片來講(可能會被修改或者覆蓋),一個比較好的方式就是當圖片緩存的時候打上一個時間戳以便當文件更新的時候做比較。
* 緩存回收 - 當內存不夠的時候,如何判斷哪些緩存須要清空呢?這就須要到你寫一個合適的算法了。幸運的是,對緩存回收的問題,蘋果提供了一個叫作NSCache通用的解決方案
NSCache
NSCache和NSDictionary相似。你能夠經過-setObject:forKey:和-object:forKey:方法分別來插入,檢索。和字典不一樣的是,NSCache在系統低內存的時候自動丟棄存儲的對象。
NSCache用來判斷什麼時候丟棄對象的算法並無在文檔中給出,可是你能夠使用-setCountLimit:方法設置緩存大小,以及-setObject:forKey:cost:來對每一個存儲的對象指定消耗的值來提供一些暗示。
指定消耗數值能夠用來指定相對的重建成本。若是對大圖指定一個大的消耗值,那麼緩存就知道這些物體的存儲更加昂貴,因而當有大的性能問題的時候纔會丟棄這些物體。你也能夠用-setTotalCostLimit:方法來指定全體緩存的尺寸。
NSCache是一個廣泛的緩存解決方案,咱們建立一個比傳送器案例更好的自定義的緩存類。(例如,能夠基於不一樣的緩存圖片索引和當前中間索引來判斷哪些圖片須要首先被釋放)。可是NSCache對咱們當前的緩存需求來講已經足夠了;不必過早作優化。
※文件格式
圖片加載性能取決於加載大圖的時間和解壓小圖時間的權衡。不少蘋果的文檔都說PNG是iOS全部圖片加載的最好格式。但這是極度誤導的過期信息了。
PNG圖片使用的無損壓縮算法能夠比使用JPEG的圖片作到更快地解壓,可是因爲閃存訪問的緣由,這些加載的時間並無什麼區別。
PNG和JPEG壓縮算法做用於兩種不一樣的圖片類型:JPEG對於噪點大的圖片效果很好;可是PNG更適合於扁平顏色,鋒利的線條或者一些漸變色的圖片。相對於不友好的PNG圖片,相同像素的JPEG圖片老是比PNG加載更快,除非一些很是小的圖片、但對於友好的PNG圖片,一些中大尺寸的圖效果仍是很好的。
但JPEG圖片並非全部狀況都適用。若是圖片須要一些透明效果,或者壓縮以後細節損耗不少,那就該考慮用別的格式了。蘋果在iOS系統中對PNG和JPEG都作了一些優化,因此普通狀況下都應該用這種格式。也就是說在一些特殊的狀況下才應該使用別的格式。
※混合圖片
對於包含透明的圖片來講,最好是使用壓縮透明通道的PNG圖片和壓縮RGB部分的JPEG圖片混合起來加載。這就對任何格式都適用了,並且不管從質量仍是文件尺寸仍是加載性能來講都和PNG和JPEG的圖片相近。
※JPEG 2000
除了JPEG和PNG以外iOS還支持別的一些格式,例如TIFF和GIF,可是因爲他們質量壓縮得更厲害,性能比JPEG和PNG糟糕的多,因此大多數狀況並不用考慮。
可是iOS 5以後,蘋果低調添加了對JPEG 2000圖片格式的支持,因此大多數人並不知道。它甚至並不被Xcode很好的支持 - JPEG 2000圖片都沒在Interface Builder中顯示。
可是JPEG 2000圖片在(設備和模擬器)運行時會有效,並且比JPEG質量更好,一樣也對透明通道有很好的支持。可是JPEG 2000圖片在加載和顯示圖片方面明顯要比PNG和JPEG慢得多,因此對圖片大小比運行效率更敏感的時候,使用它是一個不錯的選擇。
但仍然要對JPEG 2000保持關注,由於在後續iOS版本說不定就對它的性能作提高,可是在現階段,混合圖片對更小尺寸和質量的文件性能會更好。
※PVRTC
當前市場的每一個iOS設備都使用了Imagination Technologies PowerVR圖像芯片做爲GPU。PowerVR芯片支持一種叫作PVRTC(PowerVR Texture Compression)的標準圖片壓縮。
和iOS上可用的大多數圖片格式不一樣,PVRTC不用提早解壓就能夠被直接繪製到屏幕上。這意味着在加載圖片以後不須要有解壓操做,因此內存中的圖片比其餘圖片格式大大減小了(這取決於壓縮設置,大概只有1/60那麼大)。
可是PVRTC仍然有一些弊端:
* 儘管加載的時候消耗了更少的RAM,PVRTC文件比JPEG要大,有時候甚至比PNG還要大(這取決於具體內容),由於壓縮算法是針對於性能,而不是文件尺寸。
* PVRTC必需要是二維正方形,若是源圖片不知足這些要求,那必需要在轉換成PVRTC的時候強制拉伸或者填充空白空間。
* 質量並非很好,尤爲是透明圖片。一般看起來更像嚴重壓縮的JPEG文件。
* PVRTC不能用Core Graphics繪製,也不能在普通的UIImageView顯示,也不能直接用做圖層的內容。你必需要用做OpenGL紋理加載PVRTC圖片,而後映射到一對三角形中,並在CAEAGLLayer或者GLKView中顯示。
* 建立一個OpenGL紋理來繪製PVRTC圖片的開銷至關昂貴。除非你想把全部圖片繪製到一個相同的上下文,否則這徹底不能發揮PVRTC的優點。
* PVRTC使用了一個不對稱的壓縮算法。儘管它幾乎當即解壓,可是壓縮過程至關漫長。在一個現代快速的桌面Mac電腦上,它甚至要消耗一分鐘甚至更多來生成一個PVRTC大圖。所以在iOS設備上最好不要實時生成。
若是你願意使用OpenGL,並且即便提早生成圖片也能忍受得了,那麼PVRTC將會提供相對於別的可用格式來講很是高效的加載性能。好比,能夠在主線程1/60秒以內加載並顯示一張2048×2048的PVRTC圖片(這已經足夠大來填充一個視網膜屏幕的iPad了),這就避免了不少使用線程或者緩存等等複雜的技術難度。
※隱式繪製
寄宿圖能夠經過Core Graphics直接繪製,也能夠直接載入一個圖片文件並賦值給contents屬性,或事先繪製一個屏幕以外的CGContext上下文。在以前的兩章中討論了這些場景下的優化。可是除了常見的顯式建立寄宿圖,你也能夠經過如下三種方式建立隱式的:1,使用特性的圖層屬性。2,特定的視圖。3,特定的圖層子類。
瞭解這個狀況爲何發生什麼時候發生是很重要的,它可以讓你避免引入沒必要要的軟件繪製行爲。
※文本
CATextLayer和UILabel都是直接將文本繪製在圖層的寄宿圖中。事實上這兩種方式用了徹底不一樣的渲染方式:在iOS 6及以前,UILabel用WebKit的HTML渲染引擎來繪製文本,而CATextLayer用的是Core Text.後者渲染更迅速,因此在全部須要繪製大量文本的情形下都優先使用它吧。可是這兩種方法都用了軟件的方式繪製,所以他們實際上要比硬件加速合成方式要慢。
不論如何,儘量地避免改變那些包含文本的視圖的frame,由於這樣作的話文本就須要重繪。例如,若是你想在圖層的角落裏顯示一段靜態的文本,可是這個圖層常常改動,你就應該把文本放在一個子圖層中。
※光柵化
在『視覺效果』章中提到了CALayer的shouldRasterize屬性,它能夠解決重疊透明圖層的混合失靈問題。一樣在『速度的曲調』章中,它也是做爲繪製複雜圖層樹結構的優化方法。
啓用shouldRasterize屬性會將圖層繪製到一個屏幕以外的圖像。而後這個圖像將會被緩存起來並繪製到實際圖層的contents和子圖層。若是有不少的子圖層或者有複雜的效果應用,這樣作就會比重繪全部事務的全部幀划得來得多。可是光柵化原始圖像須要時間,並且還會消耗額外的內存。
當咱們使用得當時,光柵化能夠提供很大的性能優點(如你在『性能調優』章所見),可是必定要避免做用在內容不斷變更的圖層上,不然它緩存方面的好處就會消失,並且會讓性能變的更糟。
爲了檢測你是否正確地使用了光柵化方式,用Instrument查看一下Color Hits Green和Misses Red項目,是否已光柵化圖像被頻繁地刷新(這樣就說明圖層並非光柵化的好選擇,或則你無心間觸發了沒必要要的改變致使了重繪行爲)。
※離屏渲染
當圖層屬性的混合體被指定爲在未預合成以前不能直接在屏幕中繪製時,屏幕外渲染就被喚起了。屏幕外渲染並不意味着軟件繪製,可是它意味着圖層必須在被顯示以前在一個屏幕外上下文中被渲染(不論CPU仍是GPU)。圖層的如下屬性將會觸發屏幕外繪製:
屏幕外渲染和啓用光柵化時類似,除了它並無像光柵化圖層那麼消耗大,子圖層並無被影響到,並且結果也沒有被緩存,因此不會有長期的內存佔用。可是,若是太多圖層在屏幕外渲染依然會影響到性能。
有時候能夠把那些須要屏幕外繪製的圖層開啓光柵化以做爲一個優化方式,前提是這些圖層並不會被頻繁地重繪。
對於那些須要動畫並且要在屏幕外渲染的圖層來講,你能夠用CAShapeLayer,contentsCenter或者shadowPath來得到一樣的表現並且較少地影響到性能。
※CAShapeLayer
cornerRadius和maskToBounds獨立做用的時候都不會有太大的性能問題,可是當他倆結合在一塊兒,就觸發了屏幕外渲染。有時候你想顯示圓角並沿着圖層裁切子圖層的時候,你可能會發現你並不須要沿着圓角裁切,這個狀況下用CAShapeLayer就能夠避免這個問題了。
你想要的只是圓角且沿着矩形邊界裁切,同時還不但願引發性能問題。其實你能夠用現成的UIBezierPath的構造器+bezierPathWithRoundedRect:cornerRadius:這樣作並不會比直接用cornerRadius更快,可是它避免了性能問題。
※可伸縮圖片
另外一個建立圓角矩形的方法就是用一個圓形內容圖片並結合『寄宿圖』章提到的contensCenter屬性去建立一個可伸縮圖片(見清單15.2).理論上來講,這個應該比用CAShapeLayer要快,由於一個可拉伸圖片只須要18個三角形(一個圖片是由一個3*3網格渲染而成),然而,許多都須要渲染成一個順滑的曲線。在實際應用上,兩者並無太大的區別。
使用可伸縮圖片的優點在於它能夠繪製成任意邊框效果而不須要額外的性能消耗。
※shadowPath
在「寄宿圖」章有提到shadowPath屬性。若是圖層是一個簡單幾何圖形如矩形或者圓角矩形(假設不包含任何透明部分或者子圖層),建立出一個對應形狀的陰影路徑就比較容易,並且Core Animation繪製這個陰影也至關簡單,避免了屏幕外的圖層部分的預排版需求。這對性能來講頗有幫助。
若是你的圖層是一個更復雜的圖形,生成正確的陰影路徑可能就比較難了,這樣子的話能夠考慮用繪圖軟件預先生成一個陰影背景圖。
※ 混合和過分繪製
在「性能調優」章有提到,GPU每一幀能夠繪製的像素有一個最大限制(就是所謂的fill rate),這個狀況下能夠輕易地繪製整個屏幕的全部像素。可是若是因爲重疊圖層的關係須要不停地重繪同一區域的話,掉幀就可能發生了。
GPU會放棄繪製那些徹底被其餘圖層遮擋的像素,可是要計算出一個圖層是否被遮擋也是至關複雜而且會消耗處理器資源。一樣,合併不一樣圖層的透明重疊像素(即混合)消耗的資源也是至關客觀的。因此爲了加速處理進程,不到必須時刻不要使用透明圖層。任何狀況下,你應該這樣作:
這樣作減小了混合行爲(由於編譯器知道在圖層以後的東西都不會對最終的像素顏色產生影響)而且計算獲得了加速,避免了過分繪製行爲由於Core Animation能夠捨棄全部被徹底遮蓋住的圖層,而不用每一個像素都去計算一遍。
若是用到了圖像,儘可能避免透明除非很是必要。若是圖像要顯示在一個固定的背景顏色或是固定的背景圖以前,你不必相對前景移動,你只須要預填充背景圖片就能夠避免運行時混色了。
若是是文本的話,一個白色背景的UILabel(或者其餘顏色)會比透明背景要更高效。
最後,明智地使用shouldRasterize屬性,能夠將一個固定的圖層體系摺疊成單張圖片,這樣就不須要每一幀從新合成了,也就不會有由於子圖層之間的混合和過分繪製的性能問題了。
※減小圖層數量
確切的限制數量取決於iOS設備,圖層類型,圖層內容和屬性等。可是總得說來能夠容納上百或上千個,下面將演示即便圖層自己並無作什麼也會遇到的性能問題。
※裁切
在對圖層作任何優化以前,你須要肯定你不是在建立一些不可見的圖層,圖層在如下幾種狀況下回事不可見的:
* 圖層在屏幕邊界以外,或是在父圖層邊界以外。
* 徹底在一個不透明圖層以後。
* 徹底透明
Core Animation很是擅長處理對視覺效果無心義的圖層。可是常常性地,你本身的代碼會比Core Animation更早地想知道一個圖層是不是有用的。理想情況下,在圖層對象在建立以前就想知道,以免建立和配置沒必要要圖層的額外工做。
※對象回收
處理巨大數量的類似視圖或圖層時還有一個技巧就是回收他們。對象回收在iOS頗爲常見;UITableView和UICollectionView都有用到,MKMapView中的動畫pin碼也有用到,還有其餘不少例子。
對象回收的基礎原則就是你須要建立一個類似對象池。當一個對象的指定實例(本例子中指的是圖層)結束了使命,你把它添加到對象池中。每次當你須要一個實例時,你就從池中取出一個。當且僅當池中爲空時再建立一個新的。
這樣作的好處在於避免了不斷建立和釋放對象(至關消耗資源,由於涉及到內存的分配和銷燬)並且也沒必要給類似實例重複賦值。
本例中,咱們只有圖層對象這一種類型,可是UIKit有時候用一個標識符字符串來區分存儲在不一樣對象池中的不一樣的可回收對象類型。
你可能注意到當設置圖層屬性時咱們用了一個CATransaction來抑制動畫效果。在以前並不須要這樣作,由於在顯示以前咱們給全部圖層設置一次屬性。可是既然圖層正在被回收,禁止隱式動畫就有必要了,否則當屬性值改變時,圖層的隱式動畫就會被觸發。
※Core Graphics繪製
當排除掉對屏幕顯示沒有任何貢獻的圖層或者視圖以後,長遠看來,你可能仍然須要減小圖層的數量。例如,若是你正在使用多個UILabel或者UIImageView實例去顯示固定內容,你能夠把他們所有替換成一個單獨的視圖,而後用-drawRect:方法繪製出那些複雜的視圖層級。
這個提議看上去並不合理由於你們都知道軟件繪製行爲要比GPU合成要慢並且還須要更多的內存空間,可是在由於圖層數量而使得性能受限的狀況下,軟件繪製極可能提升性能呢,由於它避免了圖層分配和操做問題。
你能夠本身實驗一下這個狀況,它包含了性能和柵格化的權衡,可是意味着你能夠從圖層樹上去掉子圖層(用shouldRasterize,與徹底遮擋圖層相反)。
※-renderInContex:方法
用Core Graphics去繪製一個靜態佈局有時候會比用層級的UIView實例來得快,可是使用UIView實例要簡單得多並且比用手寫代碼寫出相同效果要可靠得多,更邊說Interface Builder來得直接明瞭。爲了性能而捨棄這些便利實在是不該該。
幸虧,你沒必要這樣,若是大量的視圖或者圖層真的關聯到了屏幕上將會是一個大問題。沒有與圖層樹相關聯的圖層不會被送到渲染引擎,也沒有性能問題(在他們被建立和配置以後)。
使用CALayer的-renderInContext:方法,你能夠將圖層及其子圖層快照進一個Core Graphics上下文而後獲得一個圖片,它能夠直接顯示在UIImageView中,或者做爲另外一個圖層的contents。不一樣於shouldRasterize —— 要求圖層與圖層樹相關聯 —— ,這個方法沒有持續的性能消耗。
當圖層內容改變時,刷新這張圖片的機會取決於你(不一樣於shouldRasterize,它自動地處理緩存和緩存驗證),可是一旦圖片被生成,相比於讓Core Animation處理一個複雜的圖層樹,你節省了至關客觀的性能。
//示例
// // LayerPropertiesViewController.m // CoreAnimationLearn // // Created by Vie on 2017/8/8. // Copyright © 2017年 Vie. All rights reserved. // #import "LayerPropertiesViewController.h" #define WIDTH 100 #define HEIGHT 100 #define DEPTH 10 #define SIZE 100 #define SPACING 150 #define CAMERA_DISTANCE 500 #define PERSPECTIVE(z) (float)CAMERA_DISTANCE/(z + CAMERA_DISTANCE) @interface LayerPropertiesViewController () <UIScrollViewDelegate> @property (nonatomic, strong) UIScrollView *scrollView; @property (nonatomic, strong) NSMutableSet *recyclePool; @end @implementation LayerPropertiesViewController #pragma mark 視圖控制器回調 - (void)viewDidLoad { [super viewDidLoad]; [self.view setBackgroundColor:RGB_ALPHA_COLOR(230, 230, 230, 1)]; UIBarButtonItem *leftItem=[[UIBarButtonItem alloc] initWithTitle:@"<" style:UIBarButtonItemStylePlain target:self action:@selector(backAction:)]; self.navigationItem.leftBarButtonItem=leftItem; //讓約束從導航欄底部開始算起 self.edgesForExtendedLayout = UIRectEdgeNone; self.extendedLayoutIncludesOpaqueBars = NO; self.modalPresentationCapturesStatusBarAppearance = NO; [self create3DAnimationWithPool]; } //建立一個類似對象池。當一個對象的指定實例(本例子中指的是圖層)結束了使命,你把它添加到對象池中。每次當你須要一個實例時,你就從池中取出一個。當且僅當池中爲空時再建立一個新的。 -(void)create3DAnimationWithPool{ self.scrollView=[[UIScrollView alloc] initWithFrame:self.view.bounds]; [self.view addSubview:self.scrollView]; self.recyclePool = [NSMutableSet set]; //set content size self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING); //set up perspective transform CATransform3D transform = CATransform3DIdentity; transform.m34 = -1.0 / CAMERA_DISTANCE; self.scrollView.layer.sublayerTransform = transform; self.scrollView.delegate=self; } - (void)viewDidLayoutSubviews { [self updateLayers]; } - (void)scrollViewDidScroll:(UIScrollView *)scrollView { [self updateLayers]; } - (void)updateLayers { //calculate clipping bounds CGRect bounds = self.scrollView.bounds; bounds.origin = self.scrollView.contentOffset; bounds = CGRectInset(bounds, -SIZE/2, -SIZE/2); //add existing layers to pool [self.recyclePool addObjectsFromArray:self.scrollView.layer.sublayers]; //disable animation [CATransaction begin]; [CATransaction setDisableActions:YES]; //create layers NSInteger recycled = 0; NSMutableArray *visibleLayers = [NSMutableArray array]; for (int z = DEPTH - 1; z >= 0; z--) { //increase bounds size to compensate for perspective CGRect adjusted = bounds; adjusted.size.width /= PERSPECTIVE(z*SPACING); adjusted.size.height /= PERSPECTIVE(z*SPACING); adjusted.origin.x -= (adjusted.size.width - bounds.size.width) / 2; adjusted.origin.y -= (adjusted.size.height - bounds.size.height) / 2; for (int y = 0; y < HEIGHT; y++) { //check if vertically outside visible rect if (y*SPACING < adjusted.origin.y || y*SPACING >= adjusted.origin.y + adjusted.size.height) { continue; } for (int x = 0; x < WIDTH; x++) { //check if horizontally outside visible rect if (x*SPACING < adjusted.origin.x || x*SPACING >= adjusted.origin.x + adjusted.size.width) { continue; } //recycle layer if available CALayer *layer = [self.recyclePool anyObject]; if (layer) { recycled ++; [self.recyclePool removeObject:layer]; } else { layer = [CALayer layer]; layer.frame = CGRectMake(0, 0, SIZE, SIZE); } //set position layer.position = CGPointMake(x*SPACING, y*SPACING); layer.zPosition = -z*SPACING; //set background color layer.backgroundColor = [UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor; //attach to scroll view [visibleLayers addObject:layer]; } } } [CATransaction commit]; //update layers self.scrollView.layer.sublayers = visibleLayers; //log NSLog(@"displayed: %i/%i recycled: %i", [visibleLayers count], DEPTH*HEIGHT*WIDTH, recycled); } //利用形狀圖片建立圓角矩形 -(void)createRoundedRectangleViewWithImg{ CALayer *blueLayer = [CALayer layer]; blueLayer.frame = CGRectMake(50, 50, 100, 100); blueLayer.contentsCenter = CGRectMake(0.5, 0.5, 0.0, 0.0); blueLayer.contentsScale = [UIScreen mainScreen].scale; blueLayer.contents = (__bridge id)[UIImage imageNamed:@"9.png"].CGImage; [self.view.layer addSublayer:blueLayer]; } //建立圓角矩形 -(void)createRoundedRectangleView{ CAShapeLayer *blueLayer=[CAShapeLayer layer]; blueLayer.frame=CGRectMake(50, 50, 100, 100); blueLayer.fillColor=[UIColor blueColor].CGColor; blueLayer.path=[UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 100, 100) cornerRadius:20].CGPath; [self.view.layer addSublayer:blueLayer]; } #pragma mark Action -(void)backAction:(id)sender{ [self.navigationController popViewControllerAnimated:YES]; } #pragma mark 其餘方法 @end
運行效果