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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容