iOS CALayer仿射变换与3D变换(CGAffineTransform、 CATransform3D)

1. 前言

上一篇文章主要讲解了CALayer的一些基础动画,本篇文章将要研究一下有关图层旋转、放缩以及平移或倾斜所用的CGAffineTransform,还有可以将扁平物体转换成三维空间对象的CATransform3D

2. CGAffineTransform

An affine transformation matrix for use in drawing 2D graphics.

一个用于绘制二维图形的仿射变换矩阵。

CGAffineTransform类型属于Core Graphics框架,Core Graphics是一个2D绘图API,并且CGAffineTransform仅仅对2D变换有效。

代码结构如下:

public struct CGAffineTransform {
    public var a: CGFloat
    public var b: CGFloat
    public var c: CGFloat
    public var d: CGFloat
    public var tx: CGFloat
    public var ty: CGFloat

    public init()
    public init(a: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat, tx: CGFloat, ty: CGFloat)
}

一个仿射变换矩阵是一个3×3的矩阵,如下图所示:

因为第三列总是(0,0,1),所以CGAffineTransform数据结构只包含前两列的值。

如果一个仿射变换矩阵乘以一个代表图形上一点(x, y)的行向量,则会生成一个新的向量表示对应的点(x', y'),公式表示如下:

至于是如何计算的,看下面公式:

至于如何计算,大概了解一下就可以,真正开发中是不用自己计算的,苹果提供了很多好用的API,具体如下:

方法说明
func rotated(by: CGFloat) -> CGAffineTransform返回一个通过旋转当前仿射变换结构而得到仿射变换矩阵。
func scaledBy(x: CGFloat, y: CGFloat) -> CGAffineTransform返回一个通过放缩当前仿射变换结构而得到仿射变换矩阵。
func translatedBy(x: CGFloat, y: CGFloat) -> CGAffineTransform返回一个通过平移当前仿射变换结构而得到仿射变换矩阵。
func concatenating(CGAffineTransform) -> CGAffineTransform返回一个由两个现有仿射变换组合而成的仿射变换矩阵。

使用上面前三个方法的时候,我们不用再单独创建一个CGAffineTransform对象,而是由当前图形对象的AffineTransform对象调用上面方法得到一个新的仿射变换矩阵,然后再赋值给当前图形对象的transform属性。代码如下:

    func methodTest1() {
        // 通过imageView的仿射变换矩阵扩大1.5倍,得到一个新的仿射变换矩阵,并赋值给imageView.transform。
        let scaleTransform = imageView.transform.scaledBy(x: 1.5, y: 1.5)
        imageView.transform = scaleTransform
        
        // 通过imageView的仿射变换矩阵沿y轴移动50,得到一个新的仿射变换矩阵,并赋值给imageView.transform。
        let translateTransform = imageView.transform.translatedBy(x: 0, y: 50)
        imageView.transform = translateTransform
        
        // 通过imageView的仿射变换矩阵旋转45度,得到一个新的仿射变换矩阵,并赋值给imageView.transform。
        let rotateTransform = imageView.transform.rotated(by: CGFloat(Double.pi/4))
        imageView.transform = rotateTransform
    }

上面methodTest1方法执行后,imageView放大、下移、旋转同时执行。效果如下图,左边是原图,右边是执行了methodTest1方法之后的效果:

     

除了通过上面的方法,还可以直接创建一个仿射变换矩阵,再赋值给当前图形对象的transform属性,比如:

    func methodTest2() {
        // 创建一个基于单位矩阵的1.5倍的仿射变换矩阵,并赋值给imageView.transform。
        let scaleTransform = CGAffineTransform(scaleX: 1.5, y: 1.5)
        imageView.transform = scaleTransform
        
        // 创建一个基于单位矩阵沿着y轴移动50的仿射变换矩阵,并赋值给imageView.transform。
        let translateTransform = CGAffineTransform(translationX: 0, y: 50)
        imageView.transform = translateTransform
        
        // 创建一个基于单位矩阵旋转45度的仿射变换矩阵,并赋值给imageView.transform。
        let rotateTransform = CGAffineTransform(rotationAngle: CGFloat(Double.pi/4))
        imageView.transform = rotateTransform
    }

上面methodTest2方法执行后,imageView只会旋转45度,没有放缩和平移效果,因为代码依次执行后,最后imageView.transform只得到了一个旋转仿射矩阵,而methodTest1方法每次赋值都是基于上一次imageView的transform。效果如下图,左边是原图,右边是执行了methodTest2方法之后的效果:

     

所以methodTest2方法只是说明可以直接创建一个仿射变换矩阵,再赋值给当前图形对象的transform属性。

下面将methodTest2改版一下,即methodTest3

    func methodTest3() {
        // 创建一个基于单位矩阵的1.5倍的仿射变换矩阵
        let scaleTransform = CGAffineTransform(scaleX: 1.5, y: 1.5)
        
        // 创建一个基于单位矩阵沿着y轴移动50的仿射变换矩阵
        let translateTransform = CGAffineTransform(translationX: 0, y: 150)
        
        // 创建一个基于单位矩阵旋转45度的仿射变换矩阵
        let rotateTransform = CGAffineTransform(rotationAngle: CGFloat(Double.pi/4))
        
        // 三个矩阵相乘得到一个新的矩阵,,并赋值给imageView.transform。
        let finalTransform = scaleTransform.concatenating(translateTransform).concatenating(rotateTransform)
        imageView.transform = finalTransform
    }

methodTest3中,先创建了三个仿射变换矩阵,然后通过concatenating方法将三个矩阵组合为一个矩阵,最终赋值给imageView.transform。效果如下图,左边是原图,右边是执行了methodTest3方法之后的效果:

     

那么methodTest3methodTest1的执行效果是否一样呢?答案是不一样的,执行结果图已经给出了答案。

2. CATransform3D

CATransform3D在Core Animation框架中使用的标准变换矩阵,它能够让图层在3D空间内移动、旋转或者放缩。CATransform3D 是一个可以在3维空间内做变换的4x4的矩阵。

一个三维空间上的点(x, y, z)乘以一个CATransform3D矩阵,则会得到一个对应的三维空间上的点(x', y' z')。

 

Core Animation中提供了一系列的方法用来创建和组合CATransform3D类型的矩阵,与2D不同的是,3D多了一个z轴,在旋转上可以分别绕x轴、y轴或者z轴旋转。

回顾一下iOS设备上的三维坐标系以及分别绕x轴、y轴和z轴旋转的示意图:

x轴和y轴分别是向右和向下为正方向,而z轴分别垂直于x轴和y轴(也就是垂直于屏幕),指向视角外(屏幕外靠近用户)为正方向。

沿x轴或者y轴旋转,则视图与屏幕平面会发生一定的角度,看起来像是倾斜了。

沿z轴旋转则类似于仿射变换中的旋转。

说了这么多,下面看一下CATransform3D常用的方法:

全局方法或变量说明

let CATransform3DIdentity: CATransform3D

3D单位矩阵,[1 0 0 0; 0 1 0 0; 0 0 1 0; 0 0 0 1]

func CATransform3DIsIdentity(_ t: CATransform3D) -> Bool

判断矩阵t是否是单位矩阵。

func CATransform3DEqualToTransform(_ a: CATransform3D, _ b: CATransform3D) -> Bool

判断矩阵a和矩阵b是否是相同的矩阵。

func CATransform3DMakeTranslation(_ tx: CGFloat, _ ty: CGFloat, _ tz: CGFloat) -> CATransform3D

返回一个由tx、ty、tz平移变化的矩阵,平移矩阵。

t' =  [1 0 0 0; 0 1 0 0; 0 0 1 0; tx ty tz 1].

func CATransform3DMakeScale(_ sx: CGFloat, _ sy: CGFloat, _ sz: CGFloat) -> CATransform3D

返回一个由sx、sy、sz放缩变化的矩阵,放缩矩阵。

t' = [sx 0 0 0; 0 sy 0 0; 0 0 sz 0; 0 0 0 1].

func CATransform3DMakeRotation(_ angle: CGFloat, _ x: CGFloat, _ y: CGFloat, _ z: CGFloat) -> CATransform3D

返回一个由向量(x, y, z)旋转angle弧度的矩阵,旋转矩阵。

func CATransform3DTranslate(_ t: CATransform3D, _ tx: CGFloat, _ ty: CGFloat, _ tz: CGFloat) -> CATransform3D

将矩阵t分别在x轴、y轴、z轴平移tx、ty、tz得到一个新的矩阵。

func CATransform3DScale(_ t: CATransform3D, _ sx: CGFloat, _ sy: CGFloat, _ sz: CGFloat) -> CATransform3D

将矩阵t分别在x轴、y轴、z轴放缩sx、sy、sz得到一个新的矩阵。

func CATransform3DRotate(_ t: CATransform3D, _ angle: CGFloat, _ x: CGFloat, _ y: CGFloat, _ z: CGFloat) -> CATransform3D

将矩阵t沿着向量(x, y, z)旋转angle弧度得到一个新的矩阵。

func CATransform3DConcat(_ a: CATransform3D, _ b: CATransform3D) -> CATransform3D

将矩阵a和矩阵b组合,得到一个新的矩阵。

func CATransform3DMakeAffineTransform(_ m: CGAffineTransform) -> CATransform3D

放回一个和仿射变换矩阵m相同效果的3D矩阵。

func CATransform3DIsAffine(_ t: CATransform3D) -> Bool

判断3D矩阵是否可以通过一个仿射变换矩阵表示出来。

func CATransform3DGetAffineTransform(_ t: CATransform3D) -> CGAffineTransform

返回由t表示的仿射变换矩阵。如果't'不能用仿射变换精确表示,则返回值是未定义的。

func CATransform3DInvert(_ t: CATransform3D) -> CATransform3D

反转t并返回结果。如果t没有逆矩阵,返回原始矩阵。

上面的方法中,常用的也就是平移、放缩、旋转等方法。

常用代码示例如下:

    func transform3DMethods1() {
        // 将单位矩阵移动后得到一个新的平移后的矩阵。
        let translateTransform = CATransform3DTranslate(CATransform3DIdentity, 10, 50, 100)
        // 将imageLayer移后得到的矩阵再放缩得到一个新的矩阵。
        let scaleTransform = CATransform3DScale(translateTransform, 1.5, 1.5, 1.5)
        // 将imageLayer移放缩后得到的矩阵再旋转得到一个新的矩阵。
        let rotateTransform = CATransform3DRotate(scaleTransform, CGFloat(Double.pi/4), 0, 0, 1)
        // 将最终得到的变换矩阵赋值给imageLayer.transform
        imageLayer.transform = rotateTransform
    }
    
    func transform3DMethods2() {
        // 创建一个平移矩阵
        let translateTransform = CATransform3DMakeTranslation(10, 50, 100)
        // 创建一个放缩矩阵
        let scaleTransform = CATransform3DMakeScale(1.5, 1.5, 1.5)
        // 创建一个旋转矩阵
        let rotateTransform = CATransform3DMakeRotation(CGFloat(Double.pi/4), 0, 0, 1)
        // 将平移矩阵与放缩矩阵结合得到一个新矩阵
        let tempTransform = CATransform3DConcat(translateTransform, scaleTransform)
        // 将上一步得到的新矩阵与旋转矩阵结合得到一个新矩阵
        let finalTransform = CATransform3DConcat(tempTransform, rotateTransform)
        // 将最终得到的混合矩阵赋值给imageLayer.transform
        imageLayer.transform = finalTransform
    }

将上面两个方法放到touchesBegan的方法中执行,运行结果如下图:

      

方法1和方法2的执行结果是不一样的,变换矩阵组合的顺序不同,执行出来的效果是不一样的。

上面的两个方法里面,都对imageLayer进行了z轴方向的移动的放缩,但是从执行的结果看,却看不出来,如果想有一些效果,还需要加入透视效果。

如果将上面的图沿着y轴旋转45度,那么左边的边会靠近我们,右边的边会远离我们,按照近大远小的效果,靠近我们的边应该大于远离我们的边,看一下代码和效果:

    func transfrom3DRotateY() {
        var rotateTransform = CATransform3DIdentity
        rotateTransform = CATransform3DRotate(rotateTransform, CGFloat(Double.pi/4), 0, 1, 0)
        imageLayer.transform = rotateTransform
    }

实际的执行效果并不是按照上面所理解的那样,而且看起来,图片在水平方向上被压缩了。如果想达到上面说到的近大远小的透视效果,请看下面的透视投影

3. 透视投影

为了做一些修正,我们需要引入投影变换(又称作z变换)来对除了旋转之外的变换矩阵做一些修改,Core Animation并没有给我们提供设置透视变换的函数,因此我们需要手动修改矩阵值,CATransform3D的透视效果通过矩阵中的一个元素来控制: m34。

m34 的默认值是0,我们可以通过设置m34为(-1.0 / d)来应用透视效果,d代表了想象中视角相机和屏幕之间的距离,以像素为单位,代码中通常500-1000就可以了。一个非常微小的值会让它看起来更加失真,而一个非常大的值会让它基本失去透视效果。

我们将上面沿着y轴旋转45度的示例改一下:
    func transfrom3DRotateY() {
        var rotateTransform = CATransform3DIdentity
        rotateTransform.m34 = -1.0/500.0
        rotateTransform = CATransform3DRotate(rotateTransform, CGFloat(Double.pi/4), 0, 1, 0)
        imageLayer.transform = rotateTransform
    }

执行结果如下,是不是有一些立体感了呢。

修改 m34可以实现透视效果,那么如果有多个视图或者图层,每个都做3D变换,那就需要分别设置相同的m34值,势必会很麻烦。CALayer 有一个属性叫做 sublayerTransform ,也是 CATransform3D型,但和对一个图层的变换不同,它影响到所有的子图层。这意味着我们可以一次性对包含这些图层的容器(父视图)做变换,于是所有的子图层都自动继承了这个变换方法。
下面来看一下示例代码:imageView1和imageView2分别是self.view中的两个子视图。
    func sublayerTransformTest() {
        var transform = CATransform3DIdentity
        transform.m34 = -1.0/500
        self.view.layer.sublayerTransform = transform
        
        let imageView1Transform = CATransform3DMakeRotation(CGFloat(Double.pi/4), 0, 1, 0)
        imageView1.layer.transform = imageView1Transform
        
        let imageView2Transform = CATransform3DMakeRotation(CGFloat(-Double.pi/4), 0, 1, 0)
        imageView2.layer.transform = imageView2Transform
    }

在这里还需提到一个知识点就是 灭点。
当在透视角度绘图的时候,远离相机视角的物体将会变小变远,当远离到一个极限距离,它们可能就缩成了一个点,于是所有的物体最后都汇聚消失在同一个点,该点就叫 灭点
这个点通常是视图的中心,于是为了在应用中创建拟真效果 的透视,这个点应该聚在屏幕中点,或者至少是包含所有3D 对象的视图中点。如下图:
 
Core Animation 定义了这个点位于变换图层的 anchorPoint (通常位于图层中心,但也有例外),这就是说,当图层发生变换时,这个点永远位于图层变换之前 anchorPoint 的位置。 当改变一个图层的 position ,同时改变了它的灭点,做 3D 变换的时候,如果想把视图通过调整 m34 来让它更加有 3D 效果,应该首先把它放置于屏幕中央,然后通过平移来把它移动到指定位置(而不是直接改变它的position ),这样所有的 3D 图层都共享一个灭点。

举个例子,创建4个imageView,都放到屏幕中间,此时4个imageView位置重叠,然后将其想4个角落移动,代码如下:

    func sublayerTransformTest() {
        var transform = CATransform3DIdentity
        transform.m34 = -1.0/500
        self.view.layer.sublayerTransform = transform
        
        var imageView1Transform = CATransform3DIdentity
        imageView1Transform = CATransform3DTranslate(imageView1Transform, -80, -100, 0)
        imageView1Transform = CATransform3DRotate(imageView1Transform,CGFloat(Double.pi/4), 0, 1, 0)
        imageView1.layer.transform = imageView1Transform
        
        var imageView2Transform = CATransform3DIdentity
        imageView2Transform = CATransform3DTranslate(imageView2Transform, 80, -120, 0)
        imageView2Transform = CATransform3DRotate(imageView2Transform,CGFloat(-Double.pi/4), 0, 1, 0)
        imageView2.layer.transform = imageView2Transform
        
        var imageView3Transform = CATransform3DIdentity
        imageView3Transform = CATransform3DTranslate(imageView3Transform, -90, 120, 0)
        imageView3Transform = CATransform3DRotate(imageView3Transform,CGFloat(Double.pi/4), 0, 1, 0)
        imageView3.layer.transform = imageView3Transform
        
        var imageView4Transform = CATransform3DIdentity
        imageView4Transform = CATransform3DTranslate(imageView4Transform, 100, 140, 0)
        imageView4Transform = CATransform3DRotate(imageView4Transform,CGFloat(-Double.pi/4), 0, 1, 0)
        imageView4.layer.transform = imageView4Transform
    }

执行结果图如下:

4个图都移动到了屏幕的不同位置,因为是从中心位置移动出去的,所以他们共享一个灭点,刚才移动的代码没有在z轴移动,现在把4个图沿着z轴远离视角,将z轴值0改为-100000,则得到下面的图,越远则图越小,4个图越居于一点。

 
试想一下,如果将一个图沿着x轴或者y轴旋转180度,会怎么样呢?
    func rotate180() {
        var rotateTransform = CATransform3DIdentity
        rotateTransform = CATransform3DRotate(rotateTransform,CGFloat(Double.pi), 0, 1, 0)
        imageView.layer.transform = rotateTransform
    }

事实证明,图片翻转过来了,并且还进行了绘制,这其实不是很友好的,按理说相机视角背面是看不见的,如果是一个立方体的盒子,那个每个面(图层)的背面是不需要绘制的,绘制了反而浪费系统资源。
CALayer有个属性isDoubleSided,默认值为true,即绘制背面,如果设置为false,则背面不会只,当旋转180度的时候,图层就在屏幕上看不见了。
 

4. 总结

本篇文章主要讲述了CALayer的2D仿射变换和3D变化,比如平移、放缩以及旋转,同时列举了常用的方法和方法示例,效果示例等等。随后又讲解了透视投影,灭点,以及关闭背面绘制等。

文中如果有阐述不正确的地方,还请路过的朋友指正。

 

本篇文章出自https://blog.csdn.net/guoyongming925的博客,如需转载,请标明出处。

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值