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变换的一些原理和简单的操作。以后有机会,再做更深层次的研究吧。

推荐阅读更多精彩内容