文章轉至個人我的博客: https://cainluo.github.io/14777052484078.htmlhtml
以前咱們所瞭解的CALayer都是比較抽象化, 好在《Core Animation》CALayer的視覺效果解決咱們這些視覺動物的學東西的枯燥, 今天咱們就來說講Transforms, 也就是CALayer的Transforms.git
** 最後:** ** 若是你有更好的建議或者對這篇文章有不滿的地方, 請聯繫我, 我會參考大家的意見再進行修改, 聯繫我時, 請備註**`Core Animation`**若是以爲好的話, 但願你們也能夠打賞一下~嘻嘻~祝你們學習愉快~謝謝~**
CALayer Transforms講得是CALayer一些咱們可以看得見的東西, 這些知識點在咱們平常開發中也會有用到的, 好比Affine Transforms, 3D Transforms, Solid Objects等等, 待咱們一一去講解.github
Affine Transforms的中文意思叫作仿射轉換, 在前一篇文章的時候咱們就使用過transform來旋轉UIView, 但那時候咱們只是簡單的使用罷了, 並無說明它的原理. 實際上UIView裏的transform是CAAffineTransform類型, 用於作二維空間的旋轉, 縮放, 平移等操做, 並且CAAffineTransform能夠和一個二維空間的向量, 好比CGPoint作3x2的矩陣. 大概的運算原理就是, 用CGPoint的每一列和CGAffineTransform矩陣的每一列對應的元素進行相乘再求和, 這樣子就會造成一個新的CGPoint. 說到這裏, 應該會有人有疑惑, CGAffineTransform和CGPoint徹底都不是同樣東西, 怎麼能作運算呢? 其實並非的, 當你使用它們兩個進行運算的時候, 系統會自動補上一些缺乏的元素, 使得CGAffineTransform和CGPoint進行一一對應, 但運算完以後, 這些填充值就會被拋棄掉, 不會進行保存, 僅僅只是用來作運算罷了. 因此咱們一般遇到的二維變換都是使用3x3, 而不是剛剛所說到的2x3, 但在某些狀況下咱們也會遇到2x3的格式矩陣, 這就是所謂的以列爲主(這個等下用事例來查看吧), 但不管如何都好, 只要可以保持一致, 用什麼格式又何妨呢? 當對圖層進行矩陣變換時, 圖層矩形內的每個點都被相應的作變換, 從而造成一個新的四邊形的形狀, CGAffineTransform中的"仿射"的意思是不管你如何去改變矩陣的值, 圖層中平行的兩條線在變換以後仍然保持平行, 這就是CGAffineTransform的"仿射".express
其實對矩陣數學的闡述早就超過了Core Animation的討論範圍了, 若是你是對矩陣數學一點都不瞭解的話, 那你就要哭暈在廁所了, 不過還好, Core Graphics提供了一系列的API, 對徹底沒有數學基礎的開發者來說也可以作一些簡單的變換, 好比:微信
CGAffineTransformMakeRotation(CGFloat angle);
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy);
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty);
複製代碼
在UIView能夠經過設置transform屬性進行變換, 但實際上仍是對CGLayer進行了一些圖層轉變的封裝. CALayer一樣也有一個transform屬性, 它叫作affineTransform, 但它的類型是CATransform3D, 而不是CGAffineTransform, 這個後面再解釋一下神馬是CATransform3D. 直接來看Demo吧:函數
- (void)viewTransform {
self.view.backgroundColor = [UIColor grayColor];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"expression"];
[self.view addSubview:imageView];
// 旋轉
CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4);
imageView.layer.affineTransform = transform;
// 縮放
// CGAffineTransform scaleTransform = CGAffineTransformMakeScale(0.5, 0.5);
// imageView.layer.affineTransform = scaleTransform;
// 平移
// CGAffineTransform translationTransform = CGAffineTransformMakeTranslation(50, 50);
// imageView.layer.affineTransform = translationTransform;
}
複製代碼
注意一下, 咱們在這裏使用的是M_PI_4, 而不是咱們本身輸入的神馬45之類的數字, 由於在iOS當中, 使用的的是弧度單位, 而不是角度單位, 弧度用數學常量是表示爲pi, 一個pi就爲180°, 而四分之一度就是45°了. 但這裏會有一個問題, 這些宏都是系統提供給咱們的, 若是你要本身去加載更多或者是擴展的話, 能夠本身手動去寫一個API.佈局
Core Graphics提供了一系列的API能夠在一個transform的基礎上作更深層次的transform, 好比說縮放以後再旋轉, 好比下面幾個API: 學習
CGAffineTransformRotate(CGAffineTransform t, CGFloat angle);
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy);
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)
複製代碼
當你操縱一個transform的時候, 須要先建立一個CGAffineTransform類型的空值, 直接把CGAffineTransformIdentity賦值過去就行了, 這個稱爲單位矩陣. 若是你須要把兩個已經寫好的transform合成爲一個的話, 你可使用系統提供的API:ui
CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);
複製代碼
不說那麼多廢話了, 直接來看Demo吧:spa
- (void)viewCombiningTransforms {
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"expression"];
[self.view addSubview:imageView];
CGAffineTransform transform = CGAffineTransformIdentity;
// 旋轉
transform = CGAffineTransformRotate(transform, M_PI_4);
// 縮放
transform = CGAffineTransformScale(transform, 0.5f, 0.5f);
// 平移
transform = CGAffineTransformTranslate(transform, 200, 0);
imageView.layer.affineTransform = transform;
}
複製代碼
看到圖片的時候, 你會發現結果好像和想象有些差別, 爲何會平移了那麼多? 緣由是在於當你按順序作了transform, 上一個transform會影響到下一個transform, 因此平移以後, 你會發現一樣被縮放和旋轉了, 這就是意味着, 你在旋轉以後的平移和平移以後的旋轉講會獲得兩種不一樣的結果, 這個你們須要注意一下.
在以前, 咱們有說起過zPosition這個屬性, 能夠從用戶角度的來讓讓圖層遠離或者是靠近,CATransform類型的transform能夠真正作到讓圖層在3D空間內平移或者旋轉. 和CGAffineTransform相似,CATransform3D也是一個矩陣, 但和之間所說的2x3矩陣不同,CATransform3D是一個能夠在3D空間內作變換的4x4矩陣. 和CGAffineTransform矩陣相似, Core Animation也提供了一系列的使用方法, 用來建立和組合CATransform3D矩陣, 於Core Graphics的函數相比, 也只是在3D的平移和旋轉中多出了一個z參數, 而旋轉的API除了有angle參數以外, 還多出了x, y, z等三個參數, 分別決定了每一個座標軸方向上的旋轉, 好比:
CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z);
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz);
CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz);
複製代碼
在以前的文章裏, 咱們都應該瞭解了在iOS當中, 原點**{0, 0}是在左上角, x軸正方向爲右邊, y軸正方向爲下邊, 在Mac OS當中則是和iOS相反, 可是Z軸呢, 則是分別和x**, y軸分別垂直, 指向視角外爲正方向, 說那麼多, 直接來看代碼吧:
- (void)viewTransforms3D {
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"expression"];
[self.view addSubview:imageView];
CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
imageView.layer.transform = transform;
}
複製代碼
所謂的Perspective Projection就是透視投影, 這裏須要普及一些知識(雖然我也看不太懂). 在現實生活中, 當物體遠離咱們的時候, 會因爲視角的問題, 物體看起來會變小, 理論上說遠離咱們的視圖邊要比靠近視角邊更短, 但實際上, 咱們的視角是等距離的, 也就是在3D Transform中仍然保持平行, 和以前提到的仿射變換有些相似. 因此爲了作一些修正, 咱們須要引入投影變換, 又稱爲z變換, 來對一些作了變換的矩陣作一些修改, 旋轉的除外, Core Animation, 當中並無給咱們提供直接設置透視變換的函數, 因此咱們須要手動去修改矩陣值, 但很慶幸的是, 這個修改是很簡單的, 直接來看代碼吧:
- (void)viewPerspectiveProjection {
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"expression"];
[self.view addSubview:imageView];
CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
imageView.layer.transform = transform;
CATransform3D transform3DIdentity = CATransform3DIdentity;
transform3DIdentity.m34 = - 1.0 / 500.0;
transform3DIdentity = CATransform3DRotate(transform3DIdentity, M_PI_4, 0, 1, 0);
imageView.layer.transform = transform3DIdentity;
}
複製代碼
在CATransform3D中, 有一個m34的元素, 它是用於按比例來縮放X和Y的值, 從而來計算離視角的距離. m34的默認值爲0, 咱們能夠經過設置m34來應用透視效果, 公式是**-1.0/d**, d表明了想象中視角相機和屏幕之間的距離, 以像素爲單位, 一般設置500-1000之間, 可是對於一些特殊視圖, 設置的值要小一些, 或者大一些要比500-1000要好一些, 因此這些值並非固定的, 最好是根據需求來調節, 否則會出現溼疹, 或者是失去透視效果.
The Vanishing Point翻譯過來叫作消失點, 意思是當在透視角度繪圖時, 原理視覺角度的物體將會變小變遠, 遠離到一個極限的時候, 全部物體最後都會匯聚而且消失在同一個點. 在現實生活中, 這個點一般都是視圖的中心, 若是要在應用中建立擬真效果的透視, 這個點通常是在屏幕的重點, 至少是全部3D對象的視圖中點. 在Core Animation中, 這個點是位於變換圖層的anchorPoint(固然也有一些特殊的狀況), 也就是說, 當圖層發生變換的時候, 這個點永遠位於圖層變換錢的anchorPoint位置. 當咱們改變一個圖層的position時, 也同時改變了它的消失點, 因此在咱們作3D變換的時候要記住. 當咱們去調整視圖的m34來讓視圖更加有3D效果, 一般要把它放置在屏幕的中央, 而後經過平移來把它移動到指定的位置, 這樣子作, 就可讓全部的3D圖層都有同一個消失點.
若是在開發中, 咱們有多個視圖或者多個圖層, 並且他們都要作3D變換, 那咱們就要對這些視圖或者圖層每一個都設置相同的m34值, 而且還要確保在變換錢都在屏幕中央都有一個相同的position, 固然, 咱們能夠本身封裝一下, 但這樣子也很是的蛋疼, 那該怎麼作呢? 在CALayer中有一個屬性叫作sublayerTransform, 它也是CATransform3D類型, 但和咱們一個一個的去設置圖層不一樣, 它將會影響全部的子圖層, 這就是說明了, 咱們只要使用sublayerTransform, 就能夠一次性的把全部子圖層都改變. 這也能夠提供另外一個好處, 就是當咱們使用sublayerTransform屬性時, 咱們就不須要再對子圖層挨個挨個的去設置消失點, 由於消失點將會被設置在容器圖層的中心點, 那咱們就能夠隨意設置position和frame來放置子圖層, 仍是直接來看Demo吧:
- (void)viewSublayerTransform {
UIImageView *imageViewOne = [[UIImageView alloc] initWithFrame:CGRectMake(80, 100, 100, 100)];
imageViewOne.image = [UIImage imageNamed:@"expression"];
UIImageView *imageViewTwo = [[UIImageView alloc] initWithFrame:CGRectMake(250, 100, 100, 100)];
imageViewTwo.image = [UIImage imageNamed:@"expression"];
[self.view addSubview:imageViewOne];
[self.view addSubview:imageViewTwo];
CATransform3D perspective = CATransform3DIdentity; perspective.m34 = - 1.0 / 500.0;
self.view.layer.sublayerTransform = perspective;
CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
imageViewOne.layer.transform = transform1;
CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 1, 0);
imageViewTwo.layer.transform = transform2;
}
複製代碼
咱們既然能夠在3D場景下旋轉圖層, 固然也能夠從背面去觀察它, 好比咱們把翻轉的角度設置爲M_PI, 那麼就會顯示一個鏡像的圖層, 咱們來看看代碼:
- (void)viewBackfaces {
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"expression"];
[self.view addSubview:imageView];
CATransform3D transform3DIdentity = CATransform3DIdentity;
transform3DIdentity.m34 = - 1.0 / 500.0;
transform3DIdentity = CATransform3DRotate(transform3DIdentity, M_PI, 0, 1, 0);
imageView.layer.transform = transform3DIdentity;
}
複製代碼
有人會問, 若是咱們對已經作過變換的圖層作反方向的會發生啥事? 在理論上來說, 咱們若是對內部圖層作了一個-45度的旋轉, 若是要恢復正常, 則要作相反的變換, 才能相互抵消, 爲了驗證一下, 咱們先試試:
- (void)viewLayerFlattening {
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 200, 200)];
view.backgroundColor = [UIColor blueColor];
[self.view addSubview:view];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(50, 50, 100, 100)];
imageView.image = [UIImage imageNamed:@"expression"];
[view addSubview:imageView];
CATransform3D outer = CATransform3DMakeRotation(M_PI_4, 0, 0, 1);
view.layer.transform = outer;
CATransform3D inner = CATransform3DMakeRotation(-M_PI_4, 0, 0, 1);
imageView.layer.transform = inner;
}
複製代碼
看結果, 和咱們想象的同樣, 再試試再3D變化的狀況下能不能抵消, 繼續看代碼:
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 200, 200)];
view.backgroundColor = [UIColor blueColor];
[self.view addSubview:view];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(50, 50, 100, 100)];
imageView.image = [UIImage imageNamed:@"expression"];
[view addSubview:imageView];
// CATransform3D outer = CATransform3DMakeRotation(M_PI_4, 0, 0, 1);
// view.layer.transform = outer;
//
// CATransform3D inner = CATransform3DMakeRotation(-M_PI_4, 0, 0, 1);
// imageView.layer.transform = inner;
// 3D Trans
CATransform3D outer = CATransform3DIdentity; outer.m34 = -1.0 / 500.0;
outer = CATransform3DRotate(outer, M_PI_4, 0, 1, 0); view.layer.transform = outer;
CATransform3D inner = CATransform3DIdentity; inner.m34 = -1.0 / 500.0;
inner = CATransform3DRotate(inner, -M_PI_4, 0, 1, 0); imageView.layer.transform = inner;
複製代碼
這裏我並無使用sublayerTransform屬性, 由於這裏面的圖層並非容器圖層直接的子圖層, 因此這裏分別對圖層設置了Perspective Projection. 結果也是和咱們所預期的不太同樣, 雖然按道理來說是顯示正常的方塊, 但實際上並非的. 在Core Animation當中, 3D圖層存在於3D空間以內, 但它們並非存在同一個, 其實每個圖層的3D場景都是扁平化的, 當咱們正面觀察一個圖層時, 看到的圖層實際上是由子圖層建立的3D場景, 當你傾斜這個圖層時, 會發現這個3D場景只是被繪製在圖層的表面罷了. 總之一句話說完, 用Core Animation建立很是負責的3D場景是很蛋疼的, 由於咱們不能直接建立一個個圖層的去套, 而後構建成一個3D結構的圖層關係, 剛剛也說了, 在相同場景下任何3D表面必須和一樣的圖層保持一致, 這是由於每個父視圖都把它的子視圖扁平化了. 那這個有辦法解決嗎? 固然有, 使用CALayer就能夠啦, 在CALayer中, 有一個叫作CATransformLayer的子類就能夠解決這個問題, 這個後面再說吧.
Solid Objects翻譯過來就叫作固體對象, 前面咱們懂得了一丟丟的3D空間圖層佈局, 如今咱們嘗試着來建立一個固態的3D對象(也就是咱們所謂的骰子), 直接來看代碼吧:
- (void)viewSolidObjects {
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;
CATransform3D transform = CATransform3DMakeTranslation(0, 0, 100);
for (NSInteger i = 0; i < 6; i++) {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
label.backgroundColor = [UIColor whiteColor];
label.textColor = [UIColor redColor];
label.layer.borderColor = [UIColor blackColor].CGColor;
label.layer.borderWidth = 0.5;
label.tag = i;
label.text = [NSString stringWithFormat:@"%ld", i + 1];
label.font = [UIFont systemFontOfSize:30];
label.textAlignment = NSTextAlignmentCenter;
switch (label.tag) {
case 0: {
[self addLabel:label withTransform:transform];
}
break;
case 1: {
transform = CATransform3DMakeTranslation(100, 0, 0);
transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
[self addLabel:label withTransform:transform];
}
break;
case 2: {
transform = CATransform3DMakeTranslation(0, -100, 0);
transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);
[self addLabel:label withTransform:transform];
}
break;
case 3: {
transform = CATransform3DMakeTranslation(0, 100, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);
[self addLabel:label withTransform:transform];
}
break;
case 4: {
transform = CATransform3DMakeTranslation(-100, 0, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 0, 1, 0);
[self addLabel:label withTransform:transform];
}
break;
case 5: {
transform = CATransform3DMakeTranslation(0, 0, -100);
transform = CATransform3DRotate(transform, M_PI, 0, 1, 0);
[self addLabel:label withTransform:transform];
}
break;
default:
break;
}
}
}
- (void)addLabel:(UILabel *)label withTransform:(CATransform3D)transform {
[self.view addSubview:label];
CGSize containerSize = self.view.bounds.size;
label.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
label.layer.transform = transform;
}
複製代碼
剛剛咱們弄了一個看上去像是立方體的, 可是它們以前的每個面之間的鏈接壓根就分辨不出, 雖然在Core Animation能夠用3D顯示圖層, 但它並無光線的概念, 若是要讓這個立方體看起來更加的真實, 那咱們就要手動給它加個陰影效果, 這個就根據本身的需求來看了. 這裏咱們簡單的來看看事例:
- (void)addLightingToLabel:(CALayer *)labelLayer {
CALayer *layer = [CALayer layer];
layer.frame = labelLayer.bounds;
[labelLayer addSublayer:layer];
CATransform3D transform = labelLayer.transform;
GLKMatrix4 matrix4 = [self matrixFrom3DTransformation:transform];
GLKMatrix3 matrix3 = GLKMatrix4GetMatrix3(matrix4);
GLKVector3 normal = GLKVector3Make(0, 0, 1);
normal = GLKMatrix3MultiplyVector3(matrix3, normal);
normal = GLKVector3Normalize(normal);
GLKVector3 light = GLKVector3Normalize(GLKVector3Make(LIGHT_DIRECTION));
CGFloat dotProduct = GLKVector3DotProduct(normal, light);
CGFloat shadow = 1 + dotProduct - AMBIENT_LIGHT;
UIColor *color = [UIColor colorWithWhite:0 alpha:shadow];
layer.backgroundColor = color.CGColor;
}
- (GLKMatrix4)matrixFrom3DTransformation:(CATransform3D)transform {
GLKMatrix4 matrix = GLKMatrix4Make(transform.m11, transform.m12, transform.m13, transform.m14,
transform.m21, transform.m22, transform.m23, transform.m24,
transform.m31, transform.m32, transform.m33, transform.m34,
transform.m41, transform.m42, transform.m43, transform.m44);
return matrix;
}
複製代碼
雖說咱們如今用的是UILabel, 若是咱們把3, 4, 5, 6換成UIButton和UIView的組合, 那4, 5, 6點擊按鈕是沒法觸發點擊事件的. 這是由於因爲視圖的順序, 在以前咱們就說過, 點擊事件的處理是由視圖再父視圖中的順序決定的, 並非在3D空間的Z軸順序上. 但在這個例子當中, 咱們的視圖的確是按照順序來添加的, 那爲何把4, 5, 6換成UIButton和UIView以後就沒法處理點擊事件了呢? 那是由於被前面的三個視圖擋住了, 在表面上截斷了4, 5, 6的點擊事件, 這個是和普通的2D佈局在按鈕上覆蓋物體是同樣的. 咱們能夠把除了3視圖以外的視圖userInteractionEnabled屬性都設置成NO, 這樣子就能夠禁止事件傳遞, 或者經過簡單的代碼, 把視圖3覆蓋在視圖6上, 那這樣子不管你如何點, 均可以點擊到按鈕了.
總結一下:
- AffineTransforms的使用
- AffineTransforms的混合變換
- 3D Transforms的Perspective Projection
- 3D Transforms的The Vanishing Point
- 3D Transforms的Sublayer Transform
- 3D Transforms的Backfaces
- 3D Transforms的Layer Flattening
- 最後再來一丟丟的Solid Objects
項目地址: https://github.com/CainRun/CoreAnimation