核心動畫知識導入
CoreAnimation框架是基于OpenGL ES 手機端/OpenGL PC端(iOS13開始為Metal)與CoreGraphics圖像處理框架的一個跨平臺的框架。
了解CoreAnimation

-
CoreAnimation的封裝核心就是去簡化OpenGL圖形處理,原因是OpenGL的學習成本是非常高的;CoreAnimation也可以用作Mac OS開發(fā) -
Core Graphics核心繪圖 -
Graphics Hardware圖形加速硬件,這個圖形硬件就是GPU芯片,GPU專門用來做計算,GPU并不是顯卡,而是顯卡需要GPU -
iOS 13之后OpenGL更新為Metal,Metal只針對iOS和Mac OS系統(tǒng);而OpenGL ES是可以針對整個嵌入式環(huán)境(安卓、黑莓等都可以使用)
GPU與CPU的區(qū)別
CPU: 中央處理器(英文Central Processing Unit)是一臺計算機的運算核心和控制核心;其功能主要是解釋計算機指令以及處理計算機軟件中的數(shù)據(jù)。
GPU:圖形處理器(英文Graphic Processing Unit)是一個專門的圖形核心處理器;GPU是顯卡的大腦,決定了該顯卡的檔次和大部分性能,同時也是2D顯卡和3D顯卡的區(qū)別依據(jù);2D顯示芯片在處理3D圖像和特效時主要依賴CPU的處理能力,稱為軟加速;3D顯示芯片是將三維圖像和特效處理功能集中在顯示芯片內(nèi),也即所謂的硬件加速。
主要區(qū)別如下:
- CPU需要很強的通用性來處理
各種不同的數(shù)據(jù)類型,同時又要邏輯判斷又會引入大量的分支跳轉(zhuǎn)和中斷的處理,這些都使得CPU的內(nèi)部結(jié)構(gòu)異常復雜;而GPU面對的則是類型高度統(tǒng)一的、相互無依賴的大規(guī)模數(shù)據(jù)和不需要被打斷的純凈的計算環(huán)境 - GPU采用了數(shù)量眾多的
計算單元和超長的流水線,但只有非常簡單的控制邏輯并省去了Cache,而CPU不僅被Cache占據(jù)了大量空間,而且還有有復雜的控制邏輯和諸多優(yōu)化電路,相比之下計算能力只是CPU很小的一部分。
核心動畫的優(yōu)點
- 簡單易用的
高性能混合編程模型 - 用類似于
UIView一樣,使?圖層來創(chuàng)建復雜的編程接口,更加高效的使用 - 輕量化的數(shù)據(jù)結(jié)構(gòu),它可以同時顯示讓上百個圖層產(chǎn)?動畫效果
- 一套?常簡單的動畫接口,能讓動畫運?在獨立的線程中,并可以獨?于主線程之外
- 一旦動畫配置完成并啟動,核?動畫就能獨立并完全控制相應的動畫幀
- 提?應用性能,應?程序只有當發(fā)生改變的時候才會重繪內(nèi)容,使用Core Animation 可以不使?其他圖形API,例如
OpenGL來獲取高效的動畫性能. - 靈活的布局管理模型,允許圖層相對同級圖層的關系來設置屬性的位置和?小
核心動畫圖層樹結(jié)構(gòu)
CoreAnimation核心動畫的結(jié)構(gòu)圖

CAAnimation是所有動畫對象的父類(抽象類,虛類),實現(xiàn)CAMediaTiming協(xié)議,負責控制動畫的時間、速度和時間曲線等等,是一個抽象類。
核心動畫類中可以直接使用的類有五個,其中CAAnimation、CAPropertyAnimation是抽象類,不能直接使用。
-
CAAnimationGroup: 動畫組,可以將很多種動畫合并到一起,組成動畫效果 -
CATransition: 轉(zhuǎn)場動畫效果 -
CAKeyframeAnimation: 關鍵幀動畫效果;values: 一個NSArray對象;里面的元素稱為關鍵幀(keyframe),動畫對象會在指定的時間(duration)內(nèi),依次顯示values數(shù)組中的每一個關鍵幀;簡單理解為,很多動畫幀執(zhí)行 -
CABasicAnimation: 基礎動畫,簡單常見的動畫效果 -
CASpringAnimation: iOS9.0之后新增的彈簧效果動畫,是CABasicAnimation的子類
CALayer與UIView的區(qū)別
-
CALayer:繼承于NSObject,所以不具備響應不能處理用戶交互,負責繪制、渲染圖形 -
UIView: 繼承于UIResponder,所以可以進行事件響應,屬性CALayer負責圖形繪制與渲染;UIView是CALayer的delegate,可以實現(xiàn)一些簡單的CALayer的方法,但要實現(xiàn)稍微復雜些的動畫效果,就需要借助CALayer,如:陰影,圓角,帶顏色的邊框、3D變換、非矩形范圍、透明遮罩、多級非線性動畫等,這也就是開發(fā)者為什么要使用CALayer的原因 -
UIView是用來管理CALayer,CALayer才是用來展示
疑問:蘋果為什么要拆分成CALayer與UIView兩個類呢?
CoreAnimation是iOS與Mac OS共用的框架,而iOS與Mac OX兩者的用戶交互方式是不同的,iOS是通過手勢觸摸,Mac OX是通過鍵盤鼠標;為了兼容兩者,單獨把CALayer拆出來只用來繪制渲染圖形
CALayer與UIView的關系

每一個UIView上面都會有一個CALayer作為它的實例圖層屬性;我們添加的動畫實際上是針對CALayer來做。


Layer Tree圖層樹(模型數(shù))主要是設置一些屬性
-
模型樹( layer tree):程序中接觸最頻繁,模型樹的對象是模型對象,儲存著動畫的目標值;當你修改layer的屬性時,便是通過模型樹上的對象 -
呈現(xiàn)樹(presentation tree):包含正在運行中的動畫的動態(tài)值,與模型樹不同,呈現(xiàn)樹始終存儲著layer在屏幕當前的狀態(tài)值,呈現(xiàn)樹無法修改,只讀;可以通過讀取當前值,來做一些其他處理 -
渲染樹(render tree):執(zhí)行實際的動畫,為CoreAnimation私有
小結(jié):動畫的三個動作創(chuàng)建執(zhí)行動畫的CALayer、創(chuàng)建動畫、添加動畫
CALayer常用屬性詳解
動畫案例準備工作:新建空工程CoreAnimation,在Main.storyboard文件拖入一個UIView,背景色配置成紅色并進行關連,命名為redView
動畫案例一
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *redView;
@property (nonatomic,strong) CALayer *layer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 新建layer,并添加到self.view.layer上
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(100, 100, 100, 100);
layer.backgroundColor = [UIColor greenColor].CGColor;
_layer = layer;
[self.view.layer addSublayer:layer];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
CABasicAnimation *animation = [CABasicAnimation animation];
// 修改動畫y的位置到600
animation.keyPath = @"position.y";
animation.toValue = @600;
animation.duration = 1;
[_redView.layer addAnimation:animation forKey:nil];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
運行工程并點擊屏幕,我們會發(fā)現(xiàn)動畫執(zhí)行完畢后,又回到了初始位置,為什么會這樣?
其實在執(zhí)行動畫的過程中會有兩個圖層:layer層與presentation層,真正移動的是presentation層;動畫開始時會先把layer層隱藏,讓presentation層做動畫;動畫結(jié)束后presentation層就會移除,layer層再出現(xiàn);原因是視圖的layer層根本沒有發(fā)生變化,動畫結(jié)束就會恢復到原來的狀態(tài)。
解決辦法:設置animation的兩個屬性
//解決動畫恢復到初始位置
//當動畫完成后,不把presentation層從render樹中移除(默認是移除的)
animation.removedOnCompletion = NO;
//當動畫結(jié)束后,把layer層狀態(tài)同步到presentation層;此時_redView的frame才會發(fā)生變化
animation.fillMode = kCAFillModeForwards;
CABasicAnimation相當于是一個數(shù)據(jù)模型,把該數(shù)據(jù)模型綁定到layer上面。
CABasicAnimation動畫的fillMode屬性介紹
-
kCAFillModeForwards:動畫結(jié)束后,layer會一直保持動畫最后的狀態(tài) -
kCAFillModeBackwards:動畫開始前,只要將動畫加入一個layer,layer便立即進入動畫的初始狀態(tài)并等待動畫開始 -
kCAFillModeBoth:kCAFillModeForwards與kCAFillModeBackwards兩者的結(jié)合,開始前保持動畫初始狀態(tài),結(jié)束后保持動畫的最后狀態(tài) -
kCAFillModeRemoved:默認屬性
動畫案例二:隱式動畫
疑問:上面紅色圖層動畫結(jié)束恢復到原來的狀態(tài),恢復的過程給人的感覺是回彈動畫,這就是隱式動畫?
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
_layer.backgroundColor = [UIColor orangeColor].CGColor;
}
我們沒有給_layer層添加動畫,但是點擊頁面_layer層由綠色變成橙色的過程,有一種動畫的效果,這就是隱式動畫;隱式動畫是由CoreAnimation框架幫我們做的,其默認動畫時長是0.25秒,通過runloop來執(zhí)行。上面添加的CABasicAnimation屬于顯式動畫。
動畫案例三:修改隱式動畫
如何修改系統(tǒng)的隱式動畫呢?
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//begin a new transaction
[CATransaction begin];
//設置隱式動畫的時長
[CATransaction setAnimationDuration:2.0];
_layer.backgroundColor = [UIColor orangeColor].CGColor;
//動畫執(zhí)行完成的回調(diào)
[CATransaction setCompletionBlock:^{
//添加轉(zhuǎn)場動畫
//rotate the layer 90 degrees
CGAffineTransform transform = self.layer.affineTransform;
transform = CGAffineTransformRotate(transform, M_PI_2);
self.layer.affineTransform = transform;
}];
[CATransaction commit];
}
CATransaction類沒有屬性,也沒有實例方法;不能通過alloc init去創(chuàng)建,只能通過begin、commit壓棧出棧的方式來管理。
動畫案例四:CALayer常用屬性 - Contents
- (void)viewDidLoad {
[super viewDidLoad];
//除了UIImageView 能夠顯示圖片,layer也可以加載圖片
UIImage *image = [UIImage imageNamed:@"test.png"];
//不用CGImage的話,圖片加載不出來
self.view.layer.contents = (__bridge id)(image.CGImage);
//填充方式
self.view.contentMode = UIViewContentModeScaleAspectFit;
self.view.layer.contentsGravity = kCAGravityResizeAspect;
}
Contents屬性是id類型,原因是在Mac OS系統(tǒng)上Contents屬性對CGImage和NSImage都會起作用,image.CGImage實際上賦值的是CGImageRef類型,CGImageRef指向的是CGImage結(jié)構(gòu),需要進行橋接處理,開發(fā)中如果需要設置背景圖可以使用layer;Contents是id類型就可以直接在layer的contents上面加載一張圖片。
-
CALayer常用屬性 -contentsScale
self.view.layer.contentsScale = [[UIScreen mainScreen] scale];
當用代碼設置contents圖片時,要?動設置圖層的contentsScale屬性,避免Retina屏幕顯示錯誤。
CALayer常用屬性 -makeToBounds
makeToBounds屬性類似于UIView中的clipsToBounds屬性,含義:是否顯示超出邊界的內(nèi)容?CALayer常用屬性 -contentsRect
-
contentsRect不是按點來計算的,而是按照單位坐標; -
OpenGL的坐標系橫向是從-1到1,縱向從1到-1的過程,center坐標就是{0, 0},而手機都是長方形的,所以OpenGL會把單位坐標系轉(zhuǎn)換為設備坐標系,不同設備有不同的坐標;


contentsRect比contentsGravity屬性要靈活很多,contentsGravity屬性只能展示圖片固定的位置與大小,而contentsRect可以展示圖片的任意內(nèi)容(只要把單位坐標計算好即可);如果contentsGravity不能滿足我們的需求時,可以使用contentsRect屬性。
CALayer中HitTest屬性的實際使用
下面我們來了解一下UIView與CALayer的圖層幾何

frame是相對于父視圖的坐標,bounds是從視圖自身出發(fā)即內(nèi)部坐標,center與position相當于父視圖上面的一個錨點

錨點就是視圖中的center屬性和position屬性,實際上就是一個坐標,錨點anchorPoint是layer的屬性(即position)。
-
CALayer常用屬性 -ZPosition

手機開發(fā)是基于二維平面的,并不是一個優(yōu)秀的三維圖形顯示載體,但是手機中會出現(xiàn)一些立體的粒子效果,于是CALayer就提供了一個屬性ZPosition,也就意味著CALayer是三維的;OpenGL和Metal默認是一個3D圖形api,默認坐標系是三維坐標系,在描述平面圖形的時候,z坐標是0;核心動畫把z坐標單獨摘出來就是ZPosition;呈現(xiàn)粒子的時候就必須要用到ZPosition。
動畫案例準備工作:新建空工程CoreAnimation2,在Main.storyboard文件拖入兩個UIView,背景色配置成橙色與紅色并進行關連,命名為view1與view2,兩個view的層級關系如下

// ViewController.h文件
- (void)viewDidLoad {
[super viewDidLoad];
self.view1.layer.zPosition = 1.0;
}
運行工程,我們發(fā)現(xiàn)橙色view出現(xiàn)在了紅色view上面,原因是什么呢?
在平面圖形上面z軸默認值是0,這里把橙色view的z軸值設置為1,就意味著把橙色view放在了上面;修改了zPosition屬性的值,就更改了深度緩沖區(qū)-深度測試,這里主要與深度緩沖區(qū)有關,本質(zhì)并不是修改圖層的層級關系。
-
CALayer常用屬性 -Hit Testing
動畫案例準備工作:新建空工程CoreAnimation3,在Main.storyboard文件拖入一個UIView,背景色配置成紅色并進行關連,命名為layerView,如下圖所示

#import "ViewController.h"
@interface ViewController ()
@property (strong, nonatomic) UIView *layerView;
@property (strong,nonatomic) CALayer *blueLayer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//1.Create sublayer
self.blueLayer = [CALayer layer];
self.blueLayer.frame = CGRectMake(0, 0, 100, 100);
self.blueLayer.backgroundColor = [UIColor blueColor].CGColor;
[self.layerView.layer addSublayer:self.blueLayer];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//1.get touchu position relative to main view(獲取相對于主視圖的觸摸位置)
CGPoint point = [[touches anyObject]locationInView:self.view];
//2.get touched layer
CALayer *layer = [self.layerView.layer hitTest:point];
//3.get layer using using hitTest
if(layer == self.blueLayer)
{
NSLog(@"Inside Blue layer");
} else if (layer == self.layerView.layer) {
NSLog(@"Inside Red layer");
}
}
@end
// 運行工程,查看打印日志,成功獲取點擊的layer層
2022-09-23 22:25:56.044062+0800 CoreAnimation3[22331:14483795] Inside Blue layer
2022-09-23 22:25:59.342807+0800 CoreAnimation3[22331:14483795] Inside Red layer
CALayer不能響應事件,但是CALayer的Hit Testing屬性能夠獲取到點擊的圖層。
hitTest方法介紹
// point : 在接收器的局部坐標系(界)中指定的點
// event : 系統(tǒng)保證調(diào)用此方法的事件。如果從事件處理代碼外部調(diào)用此方法,則可以指定nil
// returnValue : 視圖對象是當前視圖和包含點的最遠的后代。
// 如果點完全位于接收方的視圖層次結(jié)構(gòu)之外,則返回nil
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
不止CALayer中有hitTest方法,UIView中同樣有hitTest方法;UIView中該方法的作用:在視圖的層次結(jié)構(gòu)中尋找一個最合適的view來響應觸摸事件;該方法會被系統(tǒng)調(diào)用,調(diào)用時如果返回nil,即事件被丟棄,否則返回最合適的view來響應事件。
-
Hit Test調(diào)用順序
touch -> UIApplication -> UIWindow -> UIViewController.view -> subViews -> ....-> 合適的view
- 事件的傳遞順序,與
Hit Test調(diào)用順序剛好相反
view -> superView ...- > UIViewController.view -> UIViewController ->UIWindow -> UIApplication -> 丟棄事件
說明:
- 首先由
view來嘗試處理事件,如果處理不了,事件將被傳遞到父視圖superView -
superView也嘗試處理事件,如果處理不了,繼續(xù)傳遞給它的父視圖UIViewController.view -
UIViewController.view嘗試處理事件,如果處理不了,把該事件傳遞給UIViewController -
UIViewController嘗試處理事件,如果處理不了,把事件傳遞給UIWindow - 主窗口
UIWindow嘗試來處理事件, 如果處理不了,將傳遞給應用單例UIApplication - 如果
UIApplication也處理不了,該事件將被丟棄
UIView的Hit Test底層實現(xiàn)思路
常見的hitTest不實現(xiàn)的四種情況(即view不響應事件情況)
view.userInteractionEnabled = NO;view.hidden = YES;view.alpha < 0.05;view 超出 superview 的 bounds;
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
//1.是否響應事件的必然性條件
if (self.userInteractionEnabled == NO || self.alpha < 0.05 || self.hidden == YES)
{
return nil;
}
//2.touch的point在self.bounds內(nèi)
if ([self pointInside:point withEvent:event])
{
for (UIView *subView in self.subviews)
{
//進行坐標轉(zhuǎn)化
CGPoint coverPoint = [subView convertPoint:point fromView:self];
// 調(diào)用子視圖的 hitTest 重復上面的步驟。找到了,返回hitTestview ,沒找到返回有自身處理
UIView *hitTestView = [subView hitTest:coverPoint withEvent:event];
if (hitTestView)
{
return hitTestView;
}
}
return self;
}
return nil;
}
Hit Testing應?場景-?視圖超出?視圖范圍,點擊沒有響應;如果需求是讓超出父視圖的范圍也能點擊,代碼實現(xiàn)如下


仿射變換數(shù)學原理講解

-
剛體變換:只有物體的位置(平移變換)和朝向(旋轉(zhuǎn)變換)發(fā)生改變,而形狀不變;剛性變換是最一般的變換。 -
仿射變換:仿射變換具有兩個旋轉(zhuǎn)因子和兩個縮放因子,因此具有6個自由度。不具有保角性和保持距離比的性質(zhì),但是原圖平行線變換后仍然是平行線。仿射變換主要包括平移變換、旋轉(zhuǎn)變換、縮放變換(也叫尺度變換)、傾斜變換(也叫錯切變換、剪切變換、偏移變換)、翻轉(zhuǎn)變換,有六個自由度。 -
投影變換:是最一般的線性變換,有8個自由度;射影變換保持重合關系和交比不變。但不會保持平行性。即它會使得仿射變換產(chǎn)生非線性效應。


對于復雜的立體圖形,我們要想平移是非常困難的;但是通過仿射變換,我們就可以很容易的實現(xiàn),可以對立體圖形的任意頂點進行平移,實現(xiàn)代碼如下圖所示:

下面我們介紹官方文檔中的幾個矩陣

上圖展現(xiàn)了常見的transformations的矩陣配置;任何乘以identity矩陣的coordinate將不會變化,當乘以其他矩陣時,coordinate的變化和矩陣每個分量都有關;例如,沿著X軸平移,我們需要提供非零的 tx 分量并讓ty和tz為0;對于旋轉(zhuǎn)操作,我們應該提供合適的 sine 和 cosine 值。
-
Identity:單元矩陣 -
Translate:平移 -
Scale:縮放 -
Rotate around X axis:圍繞X軸旋轉(zhuǎn),X值不變;這里的四維原因是OpenGL ES/Metal中描述頂點除了圍繞X、Y、Z軸,還有一個W縮放因子。 -
Rotate around Y axis:圍繞Y軸旋轉(zhuǎn),Y值不變 -
Rotate around Z axis:圍繞Z軸旋轉(zhuǎn),Z值不變

圍繞任意軸旋轉(zhuǎn)中參數(shù)n表示向量(x,y,z),第二個參數(shù)表示角度。