iOS CoreAnimation (九) transform

本系列之前的文章或多或少地提到了 transform,本文进行完全解析。transform 直译就是改变形态、变形、变换,用来改变图层的显示效果,我们使用 CoreAnimation 框架,就从它提供的几个类和属性展开介绍。

CGAffineTransform

常用的变换如 平移 拉伸 旋转等,有一个共同的特点:原坐标系中的两条平行线,经过这类变换,仍然保持平行。于是把这类变换称为“保留平行关系的变换”,英语叫 affine transform,再翻译回来叫 仿射变换(是不是听起来高大上了)。仿射,保留了平行关系,能大幅度保持原图的特点,因此也最常用。

如何描述这类变换呢(下面是初中数学)?

因为要保留两条线的平行关系,那么在转换坐标时肯定要线性转换:
x‘ = ax
再加一个固定偏移量仍然是线性的:
x’ = ax + c
因为原坐标系中的直线 y = kx + m,y 与 x 是线性关系,那么
x’ = ax + by + c 本质也是 ax + c 的形式(y 代入进去)。

由此,可以先猜测这类变换的描述方式就是:
x‘ = ax + cy + t1
y’ = bx + dy + t2
其中 a b c d t1 t2 都是自己指定的常数。这种描述方式能不能描述平移、缩放、旋转呢?
例如 a = 1,d = 1,c = 0,b = 0 时,相当于在 x 方向平移 t1,在 y 方向平移 t2;
例如 a = 2,d = 2,c = 0,b = 0,t1 = 0,t2 = 0 时,相当于 x 方向放大 2 倍,y 方向放大 2 倍;
那旋转呢?一个点旋转后得到的新位置,和原位置肯定在一个圆上(第四篇文章提到,圆心(锚点)在 layer 的 position 处,为方便计算,假设 position 就是 (0, 0)),先让 t1 = 0,t2 = 0,我们寻找是否有 abcd 满足:
在这里插入图片描述
经过计算,有多组解,其中一组是:
a = sinθ
b = cosθ
c = cosθ
d = -sinθ
即该变换可以描述旋转。

那么这个变换一定能保留平行关系吗?假设有两条平行直线 y = kx + b,y = kx + c,算了 不证了:显然,这两条线变换后斜率仍一样,保持平行。有没有更简洁高效的写法呢?
现在的 CPU GPU 都是对矩阵运算特殊优化过的(图形学都是用矩阵的),于是:
在这里插入图片描述
我们对前面的式子换了一种写法,不熟悉也没关系,初中数学就可以理解:行列式相乘,结果矩阵的第 m 行、第 n 列的值等于左矩阵的 m 行乘右矩阵的 n 列。。具体上图的 x’ 就是 x y 1 乘以 a c tx 结果就是 ax + cy + tx。。不展开说了。。

现在知道了仿射变换的原理,iOS 开发中调用相关 API 简单得飞起,甚至可以自己实现简易的仿射变换函数(自己绘制位图)。系统 API 包括快速构造 identity 矩阵(a=1 b=0 c=0 d=1 t=0,所以 x’ = x, y’ = y),平移缩放旋转矩阵(我们也能推出相应的 abcd),或直接指定 abcdt 值;可以对一个 transform 结构拼接其他 transform,本质是矩阵乘法,不符合交换律,因此拼接多个 transform 的顺序不同会导致不同的结果。(不熟悉的话可以按照前面的行列式乘法试一下,顺序算多个矩阵)

CATransform3D

前面介绍了在平面二维空间中的变换,其原理也可以推广到三维空间中。我们只需要加一个 Z 轴:
在这里插入图片描述
这个矩阵可以实现所有二维的变换,实际上,设置 view.transform(CGAffineTransform) 时,也会同步相应改变到 view.layer.transform(CATransform3D)。注意这里的 Z 轴和 zPosition 不是一回事,上篇文章的伪代码里提过 zPosition 改变的是图层渲染顺序。系统默认我们是在无限远处观察视图的等距投影),所以 Z 轴(垂直于 XY 平面)我们暂时无法看出来,现在仍然保持着仿射的特性。

在 X、Y 轴上的平移、缩放和之前一样;API 支持对三个轴旋转不同的角度,围绕 Z 轴的旋转效果也就是二维空间的旋转,围绕 X、Y 轴的旋转看起来像是缩放。在 Z 轴上的平移即 m34 没有意义,因为我们在无限远处,那如何才能看到三维效果呢?我们不能在无穷远处观察了:m34 被专门用来描述相机与视图的距离,m34 = -1 / d,d 是距离,单位为像素,一般估算个几百到几千(看需求)就可以,对应的 m34 例如就是 -0.002;当距离无限远时,m34 趋近于默认值 0(即透视投影 -> 等距投影)。距离越近,透视效果越强。运行一下 Demo(横屏):

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let v = UILabel(frame: CGRect(x: 260, y: 200, width: 0, height: 0))
        view.addSubview(v)
        v.text = "CATRANSFORM3D"
        v.textColor = .red
        v.font = .boldSystemFont(ofSize: 28)
       	v.sizeToFit()  
		
        var t = CATransform3DIdentity
        // 先注释下面一行看看效果
        t.m34 = -1.0 / 300
        t = CATransform3DRotate(t, .pi / -3, 0, 1, 0)
        v.layer.transform = t
        // 后面介绍旋转轴的位置用到
        // v.layer.anchorPointZ += 100
    } 
}

关于旋转轴的位置,前面二维空间中的旋转我们知道圆心在锚点,本质是三维空间围绕 Z 轴的旋转,Z 轴垂直于 X、Y 形成的平面,相交于 anchorPoint。想象一下,围绕 X 轴或 Y 轴旋转的时候,也有 YZ 平面、XZ 平面,相交的点在哪?有一个 anchorPointZ 属性,这个 Z 轴锚点不使用单位坐标 (0-1),而是点(想想图层本来就是二维的,图层 Z 轴上确实没有“比例”这个概念,只是三维变换用,所以用点也说得过去)。这个值默认 0,具体意义是,想象一个笔,一张纸与笔平行,纸围绕着笔旋转,笔与纸的间距就是 anchorPointZ。正值是靠近相机/眼睛方向的,负值在“纸背面”的,可以横屏验证一下:上面的代码最后一行加上后,会发现整体偏右了(顺时针,轴在纸前方),如果值是 -100,整体会偏左(顺时针,轴在纸背后)。 围绕 Y/X 轴旋转时,轴的 X/Y 坐标和 anchorPoint 位置一样。

关于透视效果,不平行于投影面的平行线透视后会相交,交点称为 vanishing point,灭点。原书中说灭点位于 anchorPoint,也就是 layer.position 处,但很难理解,因为实际运行时,只有 d 趋近于 0(m34 负无穷;-1 时也能看到些效果)时才能看到真正的相交在 position 处。下面从实际需求的角度,用几个简单的 Demo 诠释透视效果,不一定完全在解释书中观点。前提:position 代表相机在 XY 平面的坐标,m34 描述了相机与 XY 的距离。假设现在要实现一种视觉效果:
在这里插入图片描述
四个 label,旋转+透视后,“汇聚”到屏幕中央。第一感觉 每个 label 旋转不同的角度、设置相同的 m34 即可(此时每个 label 的相机位置都不同),但是太麻烦了,需要设置不同角度、相同的 m34。能不能让不同的 label 的相机放在相同位置,这样经过透视之后,两个 label 即使设置相同的旋转角度,也会有自然的透视效果(现实中就是这样)。因此需要设置 label 的 position 在同一个位置,但这样 label 就重合了,所以还要设置平移 transform 来弥补 position 的变化量。这样做也比较麻烦。系统提供了 sublayerTransform 属性,绘制一个图层时,会读取其 transform 乘以父图层的 sublayerTransform 作为最终使用的变换;如果设置了该属性非默认的 identity,子图层的相机都会被设定在父图层的 position 处!!(所有子图层灭点相同!!)子图层的 position 现在可以随意摆放,且仍有统一的透视效果。此时回看上面的需求,只需要在父图层设置 sublayerTransform 的 m34,左边两个子图层设置 transform 相同的旋转角度 60度,右边两个设置相同的 -60度,就可以实现:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        横屏 demo
        for i in 0..<4 {
            let v = UILabel()
            view.addSubview(v)
            v.font = .boldSystemFont(ofSize: 28)
            v.text = "TRANSFORM"
            v.textColor = .red
            v.backgroundColor = .lightGray
            v.sizeToFit()
            v.layer.position = CGPoint(x: 120 + i * 185, y: 200)
            
            var t = CATransform3DIdentity
            let rate = CGFloat((i - 2) < 0 ? 1 : -1)
            t = CATransform3DRotate(t, .pi / 3 * rate, 0, 1, 0)
            v.layer.transform = t
        }
        var t = CATransform3DIdentity
        t.m34 = -1.0 / 500
        view.layer.sublayerTransform = t
    }
}

注意,sublayerTransform 只作用于当前子图层,子图层的子图层不会起作用,如果真遇到这种需求,目前想到的只能手动设置 position + 平移 transform 了。

一个图层旋转 180 度时,我们默认会看到它的背面,如果不希望绘制图层的背面(提升性能),设置 doubleSided 属性为 false。

当多级图层各有自己的 transform 时,最终会产生什么混合效果?具体案例是:父图层旋转 60 度,子图层旋转 -60 度,那么子图层最终表现像是不旋转吗?
我们首先要明确,每个图层有自己的“相机”,也就是自己的 3D 场景,自己的相机拍照之后照片发给父图层就结束了。强调:不同图层存在于不同的 3D 空间。因此子图层旋转后的样子已经拍照固定,父子图层对于是否旋转互不知情,父图层整合子图层的照片后,施加父图层的 transform。继续完善上篇文章的伪代码:

func draw():
	drawSelf() // 当前图层的位图,还没有“拍照”
	sublayer.draw() // 子图层的照片,整合到当前位图
	return applyTransform() // 形成当前图层的照片 返回

(这里代码要保存之前的位图 然后计算变换后的图片,实际系统可能会优化 生成位图时直接变换,不过这不是本文重点)

前面案例的实际结果并非左图,而是右图:
在这里插入图片描述 ---------在这里插入图片描述
如果恰好需要左图的特性,后面会介绍 CATransformLayer。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值