往期回顧:
序章
第一章 - 圖層樹
第二章 - 寄宿圖
第三章 - 圖層幾何
第四章 - 視覺效果
項目中使用的代碼
這一章我們主要來研究一下可以用來對圖層進行旋轉,平移和縮放的CGAffineTransform
已經可以將平面圖層轉換為3D對象的CATransform3D
。
仿射變化
還記得第三章中我們創建的那個時鐘么,在那里面我們用到了UIView
的transform
屬性,下面我們來具體說明一下背后的原理,transform
屬性是一個CGAffineTransform
類型,用于在二維空間做旋轉,縮放和平移。CGAffineTransform
是一個可以和二維空間向量(例如CGPoint
)做乘法的3X2的矩陣。
通過矩陣計算就可以得到新的CGPoint,書中提到雖然CGAffineTransform是3x3的矩陣,即 以行為主的格式,但是也會出現三行兩列(注意不能使兩行三列,那樣的話無法與矩陣相乘)也就是以列為主的格式。只要能保持一致,哪種格式都是可以的。
四邊形中的每一個點都會做出對應的變換,從而得出一個新的四邊形。本節標題中仿射的意思是無論矩陣用什么值變換,變換前后保持平行的對邊依然保持平行。
創建CGAffineTransform
Core Graphics
提供了一系列函數,方便我們即使對矩陣不是很了解也可以輕松地創建變換矩陣,如下幾個函數都創建了一個CGAffineTransform
實例:
CGAffineTransformMakeRotation(CGFloat angle)
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)
上面三個方法分別進行了旋轉,縮放和平移的變換。下面我們來做一個簡單的例子,把一個視圖旋轉45°。
- (IBAction)rotationClick:(id)sender {
CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4);
self.layerView.layer.affineTransform = transform;
}
這里使用了CALayer
的屬性affineTransform
,在UIView
中對應的屬性為transform
。順便再說一下,UIView
中與顯示相關的屬性都是在對CALayer
中的屬性做存取,UIView
本身不處理顯示渲染。
M_PI_4
表示四分之一π
,iOS
中變換使用的是弧度而不是度數,所以我們在做旋轉的時候應該傳入弧度而不是度數,使用下面的宏方便你獲取各種角度對應的弧度:
#define RADIANS_TO_DEGREES(x) ((x)/M_PI*180.0)
混合變換
Core Graphics提供了一系列的函數可以在一個變換的基礎上做更深層次的變換,比如同時進行縮放和旋轉等:
CGAffineTransformRotate(CGAffineTransform t, CGFloat angle)
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy)
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)
對應的除了進行變換,我們也需要一個空值,原始狀態,不進行任何變化呢,Core Graphics
提供了一個常量
CGAffineTransformIdentity
最后,混合兩個已經存在的變換矩陣使用的方法為:
CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);
接下來我們來做一個混合變化,先縮小50%,再旋轉30°,最后再向右平移200:
- (IBAction)complexChangeClick:(id)sender {
//創建一個transform
CGAffineTransform transform = CGAffineTransformIdentity;
//縮小50%
transform = CGAffineTransformScale(transform, 0.5, 0.5);
//旋轉30°
transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 30.0);
//平移
transform = CGAffineTransformTranslate(transform, 200, 0);
self.layerView.layer.affineTransform = transform;
}
你可能會發現我們的視圖并沒有水平的向下平移200的長度,而且還有點向下偏,那是因為在視圖旋轉的同時,我們平移的方向也被旋轉了30°,也就是說先旋轉再平移跟先平移再旋轉的結果是不同,這也跟矩陣相乘順序的特性保持一致。
3D變換
因為CGAffineTransform
屬于Core Graphics
框架,所以這是一個嚴格意義上的2D變換的框架,我們在第一章也提到過圖層的一大特性是可以進行3D變換,也提到過zPosition
,在這里我們要用得到屬性就是CATransform3D
。
和CGAffineTransform
矩陣類似,Core Animation
提供了一系列的方法用來創建和組合CATransform3D
類型的矩陣,和Core Graphics
的函數類似,但是3D的平移和旋轉多處了一個z參數,并且旋轉函數除了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)
如果你對圍繞某個軸旋轉感到陌生,那么你可以通過下圖來了解一下:
可以看出來2D仿射變換中的旋轉實際就是在圍繞z軸進行旋轉。現在我們把視圖繞Y軸旋轉45°來看一下效果:
- (IBAction)YRotationClick:(id)sender {
CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
self.layerView.layer.transform = transform;
}
看起來好像我們的視圖只是被壓縮了一點,這是因為沒有透視效果的原因。
透視投影
雖然Core Animation
幫我們計算了變化矩陣,但我們依然可以自己修改矩陣來實現自己想要的效果,比如修改CATransform
的元素m34
。m34
元素用于按比例縮小X
和Y
的值來計算離視角有多遠。
m34
的默認值是0,我們可以通過設置m34
為-1.0 / d
來應用透視效果,d
代表了想象中視角相機和屏幕之間的距離,具體的值可以自己把握。
- (IBAction)prespectiveRotation:(id)sender {
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0 / 500.0;
transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);
self.layerView.layer.transform = transform;
}
滅點
當在透視角度繪圖的時候,遠離相機視角的物體將會變小變遠,當遠離到一個極限距離,它們可能就縮成了一個點,于是所有的物體最后都匯聚消失在同一個點。
在現實中,這個點通常是視圖的中心,于是為了在應用中創建擬真效果的透視,這個點應該聚在屏幕中點,或者至少是包含所有3D對象的視圖中點。
當改變一個圖層的
position
,你也改變了它的滅點,做3D變換的時候要時刻記住這一點,當你視圖通過調整m34
來讓它更加有3D效果,應該首先把它放置于屏幕中央,然后通過平移來把它移動到指定位置(而不是直接改變它的position
),這樣所有的3D圖層都共享一個滅點。
sublayerTransform屬性
上文提到了滅點的概念也就是說多個圖層的m34的值應該保持一致。CALayer有一個叫做sublayerTransform屬性,它也是CATransform3D類型,而且它能夠影響到所有的自圖層。
- (IBAction)vanishClick:(id)sender {
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / 500.0;
self.containerView.layer.sublayerTransform = perspective;
CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
self.layerView.layer.transform = transform1;
CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 1, 0);
self.layerView2.layer.transform = transform2;
}
背面
我們既然可以在3D場景下旋轉圖層,那么也可以從背面去觀察它。如果我們在把角度修改為M_PI
(180°)而不是當前的M_PI_4
(45°),那么將會把圖層完全旋轉一個半圈,于是完全背對了相機視角。
如你所見,圖層是雙面繪制的,反面顯示的是正面的一個鏡像圖片。
但這并不是一個很好的特性,因為如果圖層包含文本或者其他控件,那用戶看到這些內容的鏡像圖片當然會感到困惑。另外也有可能造成資源的浪費:想象用這些圖層形成一個不透明的固態立方體,既然永遠都看不見這些圖層的背面,那為什么浪費GPU
來繪制它們呢?
CALayer
有一個叫做doubleSided
的屬性來控制圖層的背面是否要被繪制。這是一個BOOL
類型,默認為YES
,如果設置為NO
,那么當圖層正面從相機視角消失的時候,它將不會被繪制。
固體對象
上面說了那么多接下來我們來嘗試創建一個立方體。
#define kSize 200
#define kFontSize 60
@interface CubeViewController ()
@property (strong, nonatomic) NSMutableArray <UIView *> *faces;
@end
@implementation CubeViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.faces = [NSMutableArray new];
//set up the container sublayer transform
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / 500.0;
self.view.layer.sublayerTransform = perspective;
//add cube face 1
CATransform3D transform = CATransform3DMakeTranslation(0, 0, 100);
[self addFace:0 withTransform:transform];
//add cube face 2
transform = CATransform3DMakeTranslation(100, 0, 0);
transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
[self addFace:1 withTransform:transform];
//add cube face 3
transform = CATransform3DMakeTranslation(0, -100, 0);
transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);
[self addFace:2 withTransform:transform];
//add cube face 4
transform = CATransform3DMakeTranslation(0, 100, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);
[self addFace:3 withTransform:transform];
//add cube face 5
transform = CATransform3DMakeTranslation(-100, 0, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 0, 1, 0);
[self addFace:4 withTransform:transform];
//add cube face 6
transform = CATransform3DMakeTranslation(0, 0, -100);
transform = CATransform3DRotate(transform, M_PI, 0, 1, 0);
[self addFace:5 withTransform:transform];
}
- (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform {
UIView *face = [[UIView alloc] initWithFrame:CGRectMake(0, 0, kSize, kSize)];
face.backgroundColor = [UIColor colorWithRed:1.0f green:1.0f blue:1.0f alpha:1.0f];
face.center = CGPointMake([UIScreen mainScreen].bounds.size.width / 2, [UIScreen mainScreen].bounds.size.height / 2);
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(50, 50, 100, 100)];
button.titleLabel.textAlignment = NSTextAlignmentCenter;
button.titleLabel.font = [UIFont systemFontOfSize:kFontSize weight:10];
[button setTitleColor:[UIColor colorWithRed:arc4random_uniform(255) / 255.0 green:arc4random_uniform(255) / 255.0 blue:arc4random_uniform(255) / 255.0 alpha:1] forState:UIControlStateNormal];
[button setTitle:@(index + 1).stringValue forState:UIControlStateNormal];
button.layer.cornerRadius = 5.0;
button.layer.borderWidth = 1.0f;
button.layer.borderColor = [UIColor lightGrayColor].CGColor;
[button addTarget:self action:@selector(btnClick:) forControlEvents:UIControlEventTouchUpInside];
[face addSubview:button];
[self.view addSubview:face];
[self.faces addObject:face];
face.layer.transform = transform;
}
- (void)btnClick:(UIButton *)sender {
}
@end
這個角度我們看到的是一個方形,接下來我們來換一個角度,在viewDidLoad
中添加如下代碼
perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0);
perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0);
光亮和陰影
Core Animation可以顯示3D的圖層,但是它本身并沒有光線的概念,我們這里可以使用GLKit來計算陰影和光線:
#define kSize 200
#define kFontSize 60
#define LIGHT_DIRECTION 0, 1, -0.5
#define AMBIENT_LIGHT 0.5
@interface CubeViewController ()
@property (strong, nonatomic) NSMutableArray <UIView *> *faces;
@end
@implementation CubeViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.faces = [NSMutableArray new];
//set up the container sublayer 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;
//add cube face 1
CATransform3D transform = CATransform3DMakeTranslation(0, 0, 100);
[self addFace:0 withTransform:transform];
//add cube face 2
transform = CATransform3DMakeTranslation(100, 0, 0);
transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
[self addFace:1 withTransform:transform];
//add cube face 3
transform = CATransform3DMakeTranslation(0, -100, 0);
transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);
[self addFace:2 withTransform:transform];
//add cube face 4
transform = CATransform3DMakeTranslation(0, 100, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);
[self addFace:3 withTransform:transform];
//add cube face 5
transform = CATransform3DMakeTranslation(-100, 0, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 0, 1, 0);
[self addFace:4 withTransform:transform];
//add cube face 6
transform = CATransform3DMakeTranslation(0, 0, -100);
transform = CATransform3DRotate(transform, M_PI, 0, 1, 0);
[self addFace:5 withTransform:transform];
}
- (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform {
UIView *face = [[UIView alloc] initWithFrame:CGRectMake(0, 0, kSize, kSize)];
face.backgroundColor = [UIColor colorWithRed:1.0f green:1.0f blue:1.0f alpha:1.0f];
face.center = CGPointMake([UIScreen mainScreen].bounds.size.width / 2, [UIScreen mainScreen].bounds.size.height / 2);
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(50, 50, 100, 100)];
button.titleLabel.textAlignment = NSTextAlignmentCenter;
button.titleLabel.font = [UIFont systemFontOfSize:kFontSize weight:10];
[button setTitleColor:[UIColor colorWithRed:arc4random_uniform(255) / 255.0 green:arc4random_uniform(255) / 255.0 blue:arc4random_uniform(255) / 255.0 alpha:1] forState:UIControlStateNormal];
[button setTitle:@(index + 1).stringValue forState:UIControlStateNormal];
button.layer.cornerRadius = 5.0;
button.layer.borderWidth = 1.0f;
button.layer.borderColor = [UIColor lightGrayColor].CGColor;
[button addTarget:self action:@selector(btnClick:) forControlEvents:UIControlEventTouchUpInside];
[face addSubview:button];
[self.view addSubview:face];
[self.faces addObject:face];
face.layer.transform = transform;
[self applyLightingToFace:face.layer];
}
- (void)applyLightingToFace:(CALayer *)face {
//添加光線圖層
CALayer *layer = [CALayer layer];
layer.frame = face.bounds;
[face addSublayer:layer];
//轉換transform到矩陣
//GLKMatrix4和CATransform3D內存結構一致,但坐標類型有長度區別,所以理論上應該做一次float到CGFloat的轉換
CATransform3D transform = face.transform;
GLKMatrix4 matrix4 = [self matrixFrom3DTransformation:transform];
GLKMatrix3 matrix3 = GLKMatrix4GetMatrix3(matrix4);
//get face normal
GLKVector3 normal = GLKVector3Make(0, 0, 1);
normal = GLKMatrix3MultiplyVector3(matrix3, normal);
normal = GLKVector3Normalize(normal);
//get dot product with light direction
GLKVector3 light = GLKVector3Normalize(GLKVector3Make(LIGHT_DIRECTION));
float dotProduct = GLKVector3DotProduct(light, normal);
//set lighting layer opacity
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;
}
- (void)btnClick:(UIButton *)sender {
}
@end
點擊事件
你可能會發現我們明明已經給Button綁定了點擊事件,但是卻沒有觸發,實際上是因為4,5,6這三個圖層的位置位于1,2,3上面。這里我們設定為除了3以外的face的userInteractionEnabled均為NO。
總結
這一章主要講述了仿射變化與3D變換,以及變換背后的原理。