代码编织梦想

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的博客,如需转载,请标明出处。

 

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接: https://blog.csdn.net/guoyongming925/article/details/111238094

iOS根据图片比例计算显示大小-爱代码爱编程

前言 iOS开发中,很多地方使用到图片浏览,这时候就可能需要旋转屏幕查看图片,下面分享一种计算图片旋转大小的方法,在此抛砖引玉。 作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:196800191,加群密码:112233,不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长

iOS 边学边记 GCD使用详解-爱代码爱编程

一、GCD简介 Grand Central Dispatch(GCD) 是 Apple 开发的一个多核编程的较新的解决方法。 它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。是一个在线程池模式的基础上执行的并发任务 为什么要用 GCD 呢? GCD 可用于多核的并行运算GCD 会自动利用更多的 CPU 内核(比如双核、四核)GCD

iOS 了解 Runtime ,要先了解它的核心 - 消息传递 (Messaging)-爱代码爱编程

iOS Runtime Runtime 介绍 Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。理解 Objective-C 的 Runtime 机制可以帮我们更好的了解这个语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。了解 Runtime

iOS开发-微信SDK Universal Link的配置-爱代码爱编程

微信SDK Universal Link的配置 在苹果开发者中心配置 Associated Domains 在 xcode 中启用 Associated Domains 格式 applinks: + 域名 配置 apple-app-site-association 文件 Xcode中创建文件,命名为apple-app-site-associa

iOS开发之APP内部切换语言-爱代码爱编程

前言 iOS开发中,随着APP越来越完善,很多APP都做了国际化,也实现了APP内部切换语言。 作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:196800191,加群密码:112233,不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长! 原理 国际化都会走到NSB

IOS开发关于容器控制器的一些思考-爱代码爱编程

背景 在IOS开发中,容器类视图控制器有UINavigationController、UITabBarController以及UISplitViewcontroller这么三种。当我们自己要去实现一个视图控制器容器的时候,我们需要做以下操作: 1、我们需要创建一个subViewController 2、调用[self addChildViewCo

2020-12-16-爱代码爱编程

css实现流光按钮特效 今天给大家分享一个流光特效按钮,个人觉得还是有一些小帅的; 代码来喽: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content=

CocosCreator中使用Animation制作动画-爱代码爱编程

CocosCreator中使用Animation制作动画 大家好,今天给大家分享的是关于如何使用CocosCreator来制作动画效果的经验。 我们知道,一个游戏中不可或缺的元素就是动画,比如小鸟展翅动作,自行车咕噜转动,人物走路等。这些简单的动作是一个独立展示的元素,使用动画组件Animation来制作是做合适的。下面我们就来制作一个卡通人物走路的动画效

Vue引用Animate.css动画库实现应用过渡效果(以及不生效避坑)-爱代码爱编程

一、安装animate.css npm install animate.css@3.5.1 -S 划重点:博主使用的vue-cli3.0+,如果不指定animate的(低)版本,动画不生效!!!建议使用@3.x.x版本。 二、在main.js中全局引入 import animated from 'animate.css' // 引入 Vue.us

刚刚,正式宣布:再见,摩拜单车!-爱代码爱编程

来源:21Tech 据摩拜微信小程序公布,摩拜APP、摩拜微信小程序将于2020年12月14日晚23时59分停止服务和运营。摩拜单车全面接入美团并更名美团单车。 原摩拜账号中的余额、骑行卡套餐等权益可进入美团APP内查询,可继续使用。如需要退骑行卡套餐联系人工客服;如持有从外部渠道购买的骑行套餐,联系购买方退款。 2018年4月4日,美

CSS层叠文字动画-爱代码爱编程

  很实用的一个动画,body里定义了文字,可以凭键盘任意修改,灵感来自于站长之家。公众号转型做了恋爱婚庆相关,Yeah! <!DOCTYPE html> <html lang="en" > <head> <meta charset="UTF-8"> <title>css3层叠文字动画<

CSS骰子翻转动画-爱代码爱编程

<!doctype html> <html> <head> <meta charset="utf-8"> <title>CSS3骰子翻转动画</title> <style> body { font-family: Avenir, Helvetica, Arial,