iOS CALayer總結—圖層變換

今天我們來聊一聊圖層變換,很多動畫都是在變換的基礎上完成的,可以說變換是動畫的基礎。所以要想能夠很好的使用動畫,首先就需要對變換非常熟悉,大家都知道UIView有一個transform屬性是用來做變換的,可以實現二維空間的平移、旋轉和縮放,實際上這也是對內部圖層變換封裝。

一、仿射變換

仿射的意思是無論變換矩陣用什么值,圖層中平行的兩條線在變換之后任然保持平行,所以大家可以根據這個定義來判斷一個變換是不是仿射變換。
那么圖層的平移、旋轉和縮放到底是如何實現的呢?首先,我們先來回憶一下大學線性代數中的矩陣乘法吧。

矩陣乘法:
計算規則:(1)當矩陣A的列數等于矩陣B的行數是,才可以計算
(2)計算的結果矩陣C的行數等于A的行數,列數等于B的列數
(3)結果矩陣C的第 i 行第 j 列的元素Cij 等于矩陣A的第 i 行的元素與矩陣B的第 j 列對應元素乘積之和。
舉例:假設兩個矩陣A和B:
A:
[1 2]
[2 1]
B:
[0 2 3]
[1 1 2]
將兩個矩陣相乘得到矩陣C:
C = A * B =
[(1 * 0+2 * 1) (1 * 2+2 * 1)(1 * 3+2 * 2)]
[(2 * 0+1 * 1) (2 * 2+1 * 1)(2 * 3+1 * 2)]
= [2 4 7]
 [1 5 8]

下面我們來看仿射變換是怎樣使用矩陣計算來實現的:

在二維坐標中,我們將矩陣的坐標點設置為:
A :
[x y 1],
仿射變換的基礎變換矩陣為:
B:
[a b 0]
 c d 0
[tx ty 1]

通過A * B來得到一個變換之后的矩陣:

C = [ (ax+cy+tx)   (bx+dy+ty)  (1) ]

在這里,我們假設C = [x' y' 1];
那么我們就可以得到下面的等式:

x' = ax + cy + tx
y' = bx + dy + ty

平移

通過這兩個等式,我們可以發現,a,b,c,d在等于0或1的時候,會對結果又很大的影響。例如:

a = 1 ,b = 0 ,c = 0,d = 1

上面的等式為變成:

x' = x + tx
y' = y + ty

這樣x'和y‘就分別等于x和y加上一個常量,這樣的點C(x',y') 就相當于點A(x,y) 在原來的基礎上平移了一段距離。
CGAffineTransform CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)就是通過這樣的計算實現的。
將a = 1 ,b = 0 ,c = 0 ,d = 1代入到我們的基礎變換矩陣中就可以得到仿射位移矩陣了:

[1 0 0]
 0 1 0
[tx ty 1]

tx,ty分別對應CGAffineTransform CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)中的兩個參數。
我們平時常見的仿射變換還有縮放和旋轉,他們都是基于同樣的原理實現的。

縮放:

將c,b,tx,ty 均置為0,上面的等式變為:

x' = ax
y' = d
y

這樣x'和y‘就分別等于x和y的a倍和b倍,從而實現了縮放的效果。
將tx = 0 ,ty = 0 ,c = 0 ,b = 0代入到我們的基礎變換矩陣中就可以得到仿射縮放矩陣了:

[a 0 0]
 0 d 0
[0 0 1]

a,d分別對應CGAffineTransform CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)方法中的參數sx和sy

旋轉:

假設旋轉一個度數a:
a = cosa , b = sina , c = -sina , d = cosa , tx = 0 , ty = 0,上面的等式變為:

x' = cosax - sina * y
y' = sina
x + cosa * y
這樣得到的x’和y’就是x和y旋轉角度a之后得到的值。
將a = cosa , b = sina , c = -sina , d = cosa , tx = 0 , ty = 0代入到我們的基礎變換矩陣中就得到仿射旋轉矩陣:

[cosa  sina  0]
 -sina  cosa  0
[0   0   1]

角度a對應方法CGAffineTransformMakeRotation(CGFloat angle)中的angle參數。

以上變換都可以使用基礎變換函數:CGAffineTransformMake(CGFloat a, CGFloat b, CGFloat c, CGFloat d, CGFloat tx, CGFloat ty)帶入a,b,c,d,tx,ty的值得到變換結果。

混合變換

我們再使用CGAffineTransformMake系列方法在做變換的時候,每次做變換的時候都會清楚之前變換的效果,也就是每次的變換都是以圖層的初始位置為參照點進行的,但是如果我們希望視圖每次的變換都是在上一次變換的基礎上進行的話,那么怎么辦呢?
Core Graphics框架還為我們提供了一系列的函數可以在一個變換的基礎上做更深層次的變換。
方法如下:

CGAffineTransformRotate(CGAffineTransform t, CGFloat angle)
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy)
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)

為了了解的更清楚,我們還是直接上代碼看一下:
在視圖上創建一個view和兩個button,view為進行變化的視圖,button1和button2的點擊事件中分別使用CGAffineTransformMakeCGAffineTransform對圖層做變換(對視圖的transform和圖層的affineTransform屬性操作都能看到同樣的效果)。
視圖的創建代碼我就不多說了,直接說變換:
在button1的點擊事件中執行以下代碼:

- (IBAction)button1Click:(UIButton *)sender {
    if (sender.isSelected) {
        sender.selected = NO;
        //通過平移,回到當前視圖相對于frame的(0,0)位置
        self.testView.transform = CGAffineTransformMakeTranslation(0,0);
    }else {
        sender.selected = YES;
        //通過平移,平移到當前視圖相對于frame的(0,50)位置
            self.testView.transform = CGAffineTransformMakeTranslation(0,50);
    }
}

在button1的點擊事件中執行以下代碼:

- (IBAction)button2Click:(UIButton *)sender {
    if (sender.isSelected) {
        sender.selected = NO;
        self.testView.layer.affineTransform = CGAffineTransformTranslate(CGAffineTransformIdentity, 0, 0);
    }else {
        sender.selected = YES;
            self.testView.layer.affineTransform = CGAffineTransformTranslate(CGAffineTransformIdentity, 0, 50);
    }
}

點擊button,我們發現,兩種方式執行的效果是一樣的。

button1-效果圖.gif

button2-效果圖.gif

那是因為我們是使用CGAffineTransformTranslate時,是以CGAffineTransformTranslate為基礎進行的,CGAffineTransformTranslate是最初位置的中心點,每次改變都是基于這個中心點(也就是最初位置)進行改變。
我們將CGAffineTransformIdentity改為self.testView.transform,我們會發現,在不停的點擊button2的時候,self.testView不斷的向下平移,這是因為我們每次在進行平移的時候,都是以上一次平移之后的位置進行的。

button2-效果圖-2.gif

這就是CGAffineTransformMakeCGAffineTransform系列函數的區別。

二、3D變換

我們上面所說到的不管是CGAffineTransformMake系列函數還是CGAffineTransform系列函數都是屬于Core Graphics框架,而Core Graphics是一個2D繪圖API,所以我們使用這兩種函數是無法進行3D變換的,但是,我們在開發中關于圖層做3D變換的需求還是非常常見的,那么這個時候我們應該如何處理呢?
在CALyer中,同樣存在一個transform屬性,不過它是CATransform3D類型,用來做3D變換。之前,我們提到過圖層的zPosition屬性,transform屬性就用用來操作zPosition屬性來控制圖層靠近或者遠離用戶的視角,從而達到3D變換的效果。
CATransform3D也是一個矩陣,但是和2x3的矩陣不同,CATransform3D是一個可以在3維空間內做變換的4x4的矩陣。

我們首先設置一個三維坐標中,需要變換的坐標點:
A :
[x y z 1],z代表的就是zPosition
3D變換的基礎變換矩陣為:
B:
[m11 m21 m31 m41]
 m12 m22 m32 m42
 m13 m23 m33 m43
[m14 m24 m34 m44]
變換之后的坐標點為:
A :
[x‘ y’ z‘ 1]

CGAffineTransform矩陣類似,Core Animation提供了一系列的方法用來創建和組合CATransform3D類型的矩陣,但是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)

下面我們來做一個給圖層在Y軸方向旋轉45度的例子:

UIImageView *imageView = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"image_1.jpg"]];
    imageView.frame = CGRectMake(75, 100, 250, 250);
    [self.view addSubview:imageView];
 CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
    imageView.layer.transform = transform;

但是通過效果圖,我么可以看到圖片并沒有旋轉效果,看起來只不過是變窄了一點:


旋轉前.png
旋轉后.png

但是,其實仔細想一下,在現實生活中,如果我們用一個斜向的角度去看一個物體的時候,它確實會變窄,這就正確的。但是為什么現在的效果看起來并不是我們預期的呢?那是因為,物體繞著Y軸旋轉時,其中的一側會遠離我們的視角,理論上,物體遠離我們的時候,在我們的視角中,物體應該變小,所以物體遠離我們的一側應該比靠近我們的一側要短,但是現在并沒有這個效果,那么應該怎樣實現這個效果呢?
在CALyer的顯示中,默認使用是等距投影,這種投影得到的遠處的物體和近處的物體保持同樣的縮放比例,所以要想實現我們想要的效果,我們需要使用透視投影。在上面提到的3D變換的基礎矩陣中,元素m34就是用來控制透視投影效果的,元素m34用于按比例縮放X和Y的值來計算到底要離視角多遠。我們可以通過設置m34為-1.0 / d來應用透視效果,d代表了想象中視角和屏幕之間的距離,單位為像素,通常情況下,d的值在500-1000之間,看起來會比較舒服。
將我們上面的代碼改為:

 UIImageView *imageView = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"image_1.jpg"]];
    imageView.frame = CGRectMake(75, 100, 250, 250);
    [self.view addSubview:imageView];
    CATransform3D transform = CATransform3DIdentity;
    transform.m34 = - 1.0 / 500.0;
    transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);
    imageView.layer.transform = transform;

現在再來看,是不是效果非常明顯:


效果圖.png

總結

圖層變換就先說到這了,主要涉及到了2D和3D變換的一些原理和簡單的操作。以后有機會,再做更深層次的研究吧。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容