I.5 变形

不幸的是,没人能说出矩阵是什么,你得自己去观察。——Morpheus,The Matrix

在第4章“视觉特效”中我们讲解了一些提高图层、内容外观表现的技术。这一章中,我们研究CGAffineTransform用于图层旋转、位移以及变形。CATransform3d可以将一般的平面矩形(即使是有阴影的圆角矩形)变为三维平面。

仿射变形

在第3章“图层几何”中,我们用UIViewtransform属性来旋转时钟的指针,但我们并没有解释其背后的原理。UIViewtransform属性是CGAffineTransform属性,被用于表现二维的旋转、缩放和位移。CGAffineTransform是一个3行2列的矩阵,它可以乘以一个二维行向量(在这里是CGPoint)来转换其值(见图中的黑体值)。

其乘法是将CGPoint向量的每个列值去乘CGAffineTransform矩阵中的每一行,然后结果可加得到一个新的CGPoint。这在图中用灰色值表示;要想矩阵乘法成产,左边的矩阵的列数必须等于右边矩阵的行数。因此我们得用单位矩阵来填满它们,这样可以让计算得以开展而不会影响结果。我们并不真的需要存储额外的值,因为它们并不会改变但在计算时需要它们。

因此,你会经常看见一个二维变形被表示成33的矩阵(而不是23)。你也会经常看见当向量值竖直叠在一起时,这个矩阵以2行3列的格式表示。这被称为列主序格式。我们在图5.1中显示的是行主序格式。任何你喜欢的表示形式都是可以的。

图5.1 CGAffineTransform和CGPoint用来表示矩阵

当变形矩阵应用在图层上时,图层矩阵的第个角上的点会独立变形,这会形成一个新的四边形。CGAffineTransform的“仿射”是指无论矩阵用了什么值,图层中平行线变形后仍是平行的。CGAffineTransform可以用于任何符合标准的变形。图5.2展示了一些仿射或非仿射的变形:

图5.2 仿射和非仿射变形

创建仿射变形

矩阵数学的完整解释超出了本书的范围,如果你对矩阵不是早已了解,变形矩阵的概念可能有点可怕 。幸运的是,Core Graphics提供了一些内置函数来直接构造简单变形,这不需要开发者进行任何数学计算。下面的函数每个都可以创建一个新的CGAffineTransform矩阵:

CGAffineTransformMakeRotation(CGFloat angle)
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)

旋转和缩放变形是显而易见的,它们各自旋转、缩放一个向量。而位移变形仅是给向量增加特定的x和y值,所以如果向量表示点,它会移动点。

让我们用一个简单的项目来演示这些函数的效果。我们从一个简单的视图开始,给它加上一个45度的旋转变形(如图5.3)。

图5.3 用仿射变形旋转45度的视图

UIView可以通过设置transform属性来变形,但如同所有的布局属性一样,UIViewtransform实际上只是一个CALayer特性的封装。

CALayer也有一个transform属性,但它的类型是CATransform3D,而不是CGAffineTransform。我们将在这章后面介绍,但现在并不是我们关心的。CALayer中的affineTransform等同于UIView中的transform属性。表5.1展示了使用affineTransform属性45度顺时针旋转图层的代码。

表5.1 用affineTransform来45度旋转图层
@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *layerView;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    //rotate the layer 45 degrees
    CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4);
    self.layerView.layer.affineTransform = transform; }

@end

译者代码:

   import UIKit
   class ViewController: UIViewController {
       @IBOutlet weak var layerView: UIView!
       override func viewDidLoad() {
           super.viewDidLoad()
           // 45度旋转图层
           let transform = CGAffineTransformMakeRotation(CGFloat(M_PI_4))
           self.layerView.layer.setAffineTransform(transform)
       }
   }

注意我们用于角度的值是一个叫M_PI_4的常量,而不是你可能以为的45。iOS中的所有角度的变形函数都使用弧度而非角度。弧度通常是特定倍数的数学常量π(pi)。弧度π等于180度,所以π除以4等于45度。

C的数学库(会自动在每个iOS项目中引入)提供了一些普通倍数的π便于使用,M_PI_4是一个表示π除以4的常量。如果用弧度思考不方便,你可以使用如下宏来进行弧度、角度的转换:

#define RADIANS_TO_DEGREES(x) ((x) / M_PI * 180.0)
#define DEGREES_TO_RADIANS(x) ((x) / 180.0 * M_PI)

组合变形

Core Graphics也提供一组在已有变形之上进一步变形的函数。当你想创建一个单独的既缩放旋转的变形矩阵时十分有用。如下:

CGAffineTansformRotate(CGAffineTansform t, CGFloat angle)
CGAffineTansformScale(CGAffineTansform t, CGFloat sx, CGFloat sy)
CGAffineTansformTranslate(CGAffineTansform t, CGFloat tx, CGFloat ty)

当你操作变形时,创建一个什么都不做的变形往往十分有用,即CGAffineTansform等于零或空。在矩阵的世界里,这样一个值被称为单位矩阵Core Graphics为这个提供一个十分方便的常量:

CGAffineTansformIdentity

最后,如果你想结合两个已有的变形矩阵,你可以用如下函数,这将会从丙个已有矩阵中创建一个新的CGAffineTransform矩阵:

CGAffineTansformConcat(CGAffineTansform t1, CGAffineTansform t2);

让我们用这些结合函数创造更复杂的变形。我们将依次应用50%缩放、30度旋转以及向右位移200点(如表5.2)。图5.4展示了最终结果。

表5.2 用多个函数创建一个组合变形
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var layerView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // 创建一个新的变形
        var transform = CGAffineTransformIdentity

        // 缩放50%
        transform = CGAffineTransformScale(transform, 0.5, 0.5)

        // 旋转30度
        transform = CGAffineTransformRotate(transform, CGFloat(M_PI) / 180.0 * 30.0)

        // 位移200点
        transform = CGAffineTransformTranslate(transform, 200, 0)

        // 应用到图层上
        self.layerView.layer.setAffineTransform(transform)
    }

}
图5.4 依次应用多个仿射变形的效果

有一点和图5.4不符的是:图像右移距离不是指定的200点,它也同样向下移动而不是仅仅平移。其原因在于你是依次施加变形的,前的的变形会影响后面的变形。200点的向右位移被旋转30度然后缩放50%,所以它实际上变成斜向下的100点。

这意味着你应用变形的顺序会影响结果;先旋转后位移不同于先位移后旋转。

剪切变换

因为Core Graphics为你提供了正确计算变形矩阵的函数,你很少需要直接设置CGAffineTransform的值,除非你想创建剪切形变,这在Core Graphics中是没有内置函数的。

剪切变形是第四种放射变形。它比位移、旋转和缩放少见(这是为什么Core Graphics没有为它提供内置函数),但它有时十分有用。它可以很好地应用于图片(如图5.5)。术语就是“扭曲”。表5.3展示了用于剪切变形函数的代码。

图5.5 一个水平的扭曲
表5.3 实现扭曲变形
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var layerView: UIView!

    func CGAffineTransformMakeShear(x: CGFloat, y: CGFloat) -> CGAffineTransform {
        var transform = CGAffineTransformIdentity
        transform.c = -x
        transform.b = y
        return transform
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // 扭曲45度图层
        self.layerView.layer.setAffineTransform(CGAffineTransformMakeShear(1, y: 0))
    }

}

3D变形

正好CG前缀所示,CGAffineTransform类型属于Core Graphics框架。Core Graphics是一个严格的2D绘图API,CGAffineTransform只能用于2D变形(就是只能用于二维平面)。

在第3章中,我们看了zPosition属性,这允许我们前后移动图层(相对于用户视窗)。transform属性(是一个CATransform3D烦死)包含了这一方法,允许我们在三维空间移动旋转图层。

CGAffineTransform一样,CATransform3D是一个矩阵。但并不是一个23矩阵,CATransform3D是一个44矩阵可以直接在3D中变形一个点(如图5.6)。

图5.6 CATransform3D矩阵变形一个3D点

Core Animation提供一些如同CGAffineTransform矩阵一样的函数来创建组合CATransform3D矩阵。这些函数和Core Grapihcs很像,但3D位移和缩放提供一个额外的z参数,而且旋转函数接收xyz角度表示,它们共同组成了旋转轴的向量。

CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z)
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz)
CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)

你现在应该对X轴和Y轴很熟悉,它们是分别向右和下沿伸的(尽管你可能回忆起第3章中介绍说在这只是在iOS上,而在Mac OS上Y轴向上指)。Z轴与它们垂直并且指向镜头(如图5.7)。

图5.7 X、Y和Z轴及绕它们旋转的平面

正如你在图中所见,绕Z轴旋转相当于我们先前提及的3D仿射旋转。而绕X轴或Y轴旋转会旋转出屏幕的二维平面向镜头倾斜。

让我举个例子:表5.4的代码使用CATransform3DMakeRotation来绕Y轴旋转我们视图的主图层。我们可以把它想成向右倾斜视图,因此我们是以一个角度来看它的。

结果在图5.8,但它并不大像我们想像中的样子。

表5.4 绕Y轴旋转图层
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var layerView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // 绕Y轴45度旋转图层
        let transform = CATransform3DMakeRotation(CGFloat(M_PI_4), 0, 1, 0)
        self.layerView.layer.transform = transform
    }

}
图5.8 一个绕Y轴旋转45度的视图

它起来压根不像是旋转了;它看起来更像是使用缩放变形水平压缩了。我们做错了吗?

不,它的确是正确的。视图看起来窄了是因为我们斜斜的看它,所以只一部分面对镜头。它看起来不正确的原因是没有透视

透视投影

在现实中,由于透视会导致近大远小。我们可能期望远处的视图看起来比近处的视图小,但这并没有发生。我们现在看到的视图是等轴测投影,这是一个提供平行线的3D绘图方法,与我们先前提及的仿射变形更为相似。

在等轴测投影中,远处的物体和近处的物体缩放比例一致。这种投影有其用处(如,对于建筑绘图或鸟瞰图以及伪3D视频游戏等),但它不是我们现在想要的。

为了修复这点,我们需要修改我们的变形矩阵来在我们先前用的旋转变形之外引入透视变形(有时也称作Z变形)。Core Animation没有提供给我们任何函数来设置透视变形,所以我们不得不手动修改我们的矩阵。幸运的是,这十分简单:CATransform#D的透视效果由矩阵中的m34元素的值控制。m34值(如图5.9所示)被用于计算X和Y值部分缩放远离镜头多少。

图5.9 CATransform3D中用于透视的m34值

默认下,m34值为0。我们可以对我们的场景设置m34属性一个-1.0/d的值,这里d是想像中的镜头和屏幕的距离,用点来衡量。我们如何计算这个距离应该是多少?我们并不需要真的计算,我们只需要编造一点。

因为镜头并非真实存在,我们可以随意指定它的位置只要看起来不错就可以。通常500到1000的值效果不错,但你可能发现或大或小的值可能对特定排列的视图效果更好。缩小距离值会增加透视效果,所以一个非常小的值会看起来特别扭曲,一个很大的值会看起来和没有透视效果一样(等轴测)。表5.5展示了给我们视图添加透视的代码,图5.10展示了结果。

表5.5 给变形添加透视
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var layerView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // 创建一个变形
        var transform = CATransform3DIdentity

        // 应用透视
        transform.m34 = -1.0 / 500.0

        // 绕Y轴45度旋转图层
        transform = CATransform3DRotate(transform, CGFloat(M_PI_4), 0, 1, 0)

        // 应用到图层
        self.layerView.layer.transform = transform
    }

}
图5.10 我们应用了透视变形的视图

尽头

当以透视绘制时,远离镜头的物体会变小。如果离的足够远,它们会缩小成一个点。所有远处的物体最终会交汇于同一个尽头。

在现实中,尽头通常是你视图的中心(如图5.11),一般我们要在应用里创建真实的透视,尽头应该在屏幕中心,或者至少有你3D物体的视图中心。

图5.11 尽头

Core Animation定义尽头在被变形的图层anchorPoint处(这通常是图层中心,也可能不是,第3章有详解)。这是在说,尽头在于anchorPoint在视图应用变形的位置;如果变形包括一个将图层移到屏幕某处的位移,尽头将在它位移前的位置。

当你改变图层的position,你也改变了尽头位置。当你处理3D时应当牢记这一点。如果你想改变某图层的m34值来使其看起来像是3D的,你应该将它放在屏幕中心然后使用位移(而不是改变它的position)来移至最终的位置,这样它才可以和屏幕上的其它物体有共同的尽头。

sublayerTransform属性
如果存在多个有3D变形的视图或图层,对它们每个独自应用相同的m34值来确保它们在变形前共有一个在屏幕中心的positon。如果你自定义一个常量或函数来创建并指定它们位置,这一切会相当简单,但同样有限制(例如,你不能通过Interface Builder排列视图)。这是一个更好的方式。

CALayer有另一个更变形属性叫做sublayerTransform。这也是一个CATransform3D,但并不是对其应用的图层变形,它只影响其子图层。这意味着你可以对一个容器图层应用透视变形来影响它的子图层,它的所有子图层都会自动继承透视效果。

只需要在一个你认为方便的地方设置透视变形,但它还有一个显著优点:尽头被设为窗口图层的中心,而不是每个子图层独立设置。这意味着你可以随意使用图层的positionframe来移动子图层而不是先把它们移到屏幕中样再通过变形移动来保持它们的尽头不变。

让我们用一个例子演示这一点。我们会在Interface Builder中并排放置两个视图(如图5.12)。然后通过设置它们容器视图的透视变形,我们可以给它们应用相同的透视和尽头。看表5.6中相对于图5.13结果的代码。

图5.12 在容器视图中并排的两个视图
表5.6 使用sublayerTransform
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var containerView: UIView!

    @IBOutlet weak var layerView1: UIImageView!
    @IBOutlet weak var layerView2: UIImageView!
    override func viewDidLoad() {
        super.viewDidLoad()

        // 给容器应用透视变形
        var perspective = CATransform3DIdentity
        perspective.m34 = -1.0 / 500.0
        self.containerView.layer.sublayerTransform = perspective

        // layerView1绕Y轴45度旋转
        let transform1 = CATransform3DMakeRotation(CGFloat(M_PI_4), 0, 1, 0)
        self.layerView1.layer.transform = transform1

        // layerView2绕Y轴45度旋转
        let transform2 = CATransform3DMakeRotation(CGFloat(-M_PI_4), 0, 1, 0)
        self.layerView2.layer.transform = transform2
    }

}
同样透视的两个独立的变形视图

背面

既然我们可以在三维空间上旋转图层,我们也可以从后面观察它们。如果我们在表5.4中把角度改为M_PI(180度)而非现在的M_PI_4(45度),我们将把图形旋转一个半圆,这样它就会背对镜头。

图层从背面看起来什么样?如图5.14所示。

图5.14 我们视图的背面,显示一个雪人图像的镜像

正如你所见,图层是双面的;前后是成镜面对称的。尽管这并不是一个必须想要的特性。如果你的图层有文字或控件,看见它们的镜像会让用户感到疑惑。它也有潜在的浪费:想像由图层中不透明的立方体组成的实体,为什么我们要浪费GPU周期来绘制我们永远不会看见的背面?

CALayer有一个属性叫doubleSided用来控制图层的背面是否被绘制。doubleSided是一个BOOL值且默认为YES。如果你将其设为NO,那么当图层背向镜头,它压根不会被绘制。

图层扁平化

如果我们把一个对一个图层施加变形,而这个图层包含一个被向相反方向变形的图层,会发生什么?疑惑骊?看图5.15。

图5.15 内嵌图层施加相反变形

注意看内图层的负45度旋转是如何抵消外图层的45度旋转的,这导致内图层最终还是指向正上。

逻辑上来说,如果内图层和外图层有相反的变形(在这里,是绕Z轴的旋转),我们可能以为这两个变形会相互抵消。

让我们用实践证实这点。表5.7展示了相关代码,图5.16展示了结果。[1]

表5.7 绕Z轴反方向旋转
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var outerView: UIView!
    @IBOutlet weak var innerView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // 外图层45度旋转
        let outer = CATransform3DMakeRotation(CGFloat(M_PI_4), 0, 0, 1)
        self.outerView.layer.transform = outer

        // 内图层-45度旋转
        let inner = CATransform3DMakeRotation(CGFloat(-M_PI_4), 0, 0, 1)
        self.innerView.layer.transform = inner
    }
}
图5.16 旋转视图符合图5.15中的预期

然而译者实现效果如下:


反方向旋转的外视图和内视图

这看起来像是按照预期的工作的。现在让我们试试3D的表现。我们将修改代码让它们绕Y轴旋转而非Z轴,同时加上透视来看清楚发生了什么。我们不能用表5.6中的sublayerTransform方法,因为我们内图层不是容器的直接子图层,所以我们应该为它们分别添加透视(如表5.8)。

表5.8 绕Y轴反方向旋转
override func viewDidLoad() {
    super.viewDidLoad()

    // 外图层45度旋转
    var outer = CATransform3DIdentity
    outer.m34 = -1.0 / 500.0
    outer = CATransform3DRotate(outer, CGFloat(M_PI_4), 0, 1, 0)
    self.outerView.layer.transform = outer

    // 内图层-45度旋转
    var inner = CATransform3DIdentity
    inner.m34 = -1.0 / 500.0
    inner = CATransform3DRotate(inner, CGFloat(-M_PI_4), 0, 1, 0)
    self.innerView.layer.transform = inner
}

我们预期会看到如图5.17的结果。

图5.17 预期的绕Y轴旋转的结果

但是并不是如我们预期,我们看见如图5.18的东西。发生了什么?我们的内图层仍然向左倾斜,也扭曲了;它被预期在方块面上啊!

事实证明虽然Core Animation图层存在于3D空间,它们并不存在于同样的3D空间中。每个图层的3D场景是扁平化的。当你从正面看图层时,你会看见由其子图层创建的3D场景的假象,当你倾斜图层时,你会意识到3D场景只是画在图层表面的。

图5.18 实际上绕Y轴反方向旋转的结果

译者自己代码的效果:


绕Y轴反方向旋转

这类似于你在玩3D游戏时倾斜屏幕。你可能在你游戏中看见了一面墙,但倾斜屏幕并不能让你四下观察这面墙。屏幕上的场景并不随你看的角度不同而改变;图层内容也是同样的。

这使得用Core Animation制作复杂3D场景十分困难。你不能用图层树构造层次化的3D结构,同一场景的任何3D表面必须与同一图层为兄弟,这是因为每个父图层都会扁平化其子图层。

至少,在你使用正常的CALayer实例时这是正确的。CALayer有一个叫CATransformLayer的子类被设计来解决这一问题。这将在第6章“特定图层”中讲解。

实体

既然你已经了解了3D空间中放置图层的基础,让我们尝试构造一个3D实体(好吧,从技术上讲是一个中空物体,但它看起来是实心的)。我们将用六个独立的视图作为表面来构造一个立方体。

为了实现我们例子的目的,立方体在Interface Builder会如图5.19排列。我们可以用代码创建表面,但使用Interface Builder的优势在于我们可以轻松地为每个表面添加、排列子视图。记住这些表面是普通的用户界面元素,它们可以包含其它视图和控件。它们是完整的且会互相影响,所以在我们把它们折叠成一个立方体后还会存在。

由于使用xib混编stroyboard较为麻烦,译者使用代码创建表面。

表5.9 创建立方体
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var containerView: UIView!
    var faces: Array<UIView> = []

    func getRandomColor() -> UIColor {
        // 创建随机颜色
        let red: CGFloat = CGFloat(arc4random() % 255) / 255.0
        let green: CGFloat = CGFloat(arc4random() % 255) / 255.0
        let blue: CGFloat = CGFloat(arc4random() % 255) / 255.0
        return UIColor(red: red, green: green, blue: blue, alpha: 1)
    }

    func createFace(number: NSInteger) -> UIView {
        // 创建表面视图
        let face = UIView(frame: CGRectMake(0, 0, 200, 200))
        face.backgroundColor = UIColor.whiteColor()

        // 创建标签
        let label = UILabel(frame: CGRectZero)
        label.text = number.description
        label.textColor = getRandomColor()
        label.font = UIFont.systemFontOfSize(40)

        // 将标签放在中间
        label.sizeToFit()   // 用于将标签的bounds大小设为恰好符合
        let faceSize = face.bounds.size
        label.center = CGPointMake(faceSize.width / 2.0, faceSize.height / 2.0)

        // 将标签加入表面视图
        face.addSubview(label)

        return face
    }

    func addFace(index: NSInteger, withTransform transform: CATransform3D) {
        // 获得表面视图并把它加入容器中
        let face = self.faces[index]
        self.containerView.addSubview(face)

        // 将表面视图在容器中居中
        let containerSize = self.containerView.bounds.size
        face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0)

        // 应用变形
        face.layer.transform = transform
    }

    override func viewDidLayoutSubviews() {
        // 创建六个表面
        for (var i = 1; i <= 6; i++) {
            let face = createFace(i)
            faces.append(face)
        }

        // 设置容器子视图视图形变
        var perspective = CATransform3DIdentity
        perspective.m34 = -1.0 / 500.0
        self.containerView.layer.sublayerTransform = perspective

        // 添加立方体的表面1
        var transform = CATransform3DMakeTranslation(0, 0, 100)
        self.addFace(0, withTransform: transform)

        // 添加立方体的表面2
        transform = CATransform3DMakeTranslation(100, 0, 0)
        transform = CATransform3DRotate(transform, CGFloat(M_PI_2), 0, 1, 0)
        self.addFace(1, withTransform: transform)

        // 添加立方体的表面3
        transform = CATransform3DMakeTranslation(0, -100, 0)
        transform = CATransform3DRotate(transform, CGFloat(M_PI_2), 1, 0, 0)
        self.addFace(2, withTransform: transform)

        // 添加立方体的表面4
        transform = CATransform3DMakeTranslation(0, 100, 0)
        transform = CATransform3DRotate(transform, CGFloat(-M_PI_2), 1, 0, 0)
        self.addFace(3, withTransform: transform)

        // 添加立方体的表面5
        transform = CATransform3DMakeTranslation(-100, 0, 0)
        transform = CATransform3DRotate(transform, CGFloat(-M_PI_2), 0, 1, 0)
        self.addFace(4, withTransform: transform)

        // 添加立方体的表面6
        transform = CATransform3DMakeTranslation(0, 0, -100)
        transform = CATransform3DRotate(transform, CGFloat(M_PI), 0, 1, 0)
        self.addFace(5, withTransform: transform)
        

    }
}
图5.20 朝前显示的立方体

我们的立方体从这个角度看并不显眼;它看起来就像是正方形。为了正确的表现它,我们需要从一个不一样的视角看它。

旋转立方体本身可能会很麻烦,因为我们不得不独立旋转每一面。一个简单的方法是旋转镜头,这个我们可以能过修改容器视图的sublayerTransform来实现。

在其应用于containterView前,加上下面几行来旋转perspective变形矩阵。

perspective = CATransform3DRotate(perspective, CGFloat(-M_PI_4), 1, 0, 0)
perspective = CATransform3DRotate(perspective, CGFloat(-M_PI_4), 0, 1, 0)

它有绕Y轴45度旋转镜头的效果(或者说相对于镜头旋转整个屏幕,这取决于你怎么想),然后再一个绕X轴旋转45度。我们现在从一个角看立方体,我们可以看到它实际的样子(如图5.21)。

图5.21 从一个角看立方体

光影

它现在看起来是像一个立方体,但很难辨别不同面的连接。Core Animation可以用3D显示图层,但没有的概念。如果你想让你的立方体看起来更真实,你需要自己加上阴影效果。你可以通过调整不同视图的背影色来实现或对它们提前使用有光线效果的图像。

如果你需要创建动态的光影效果,你可以在每个图层上覆盖一个半透明的黑色阴影图层,然后根据视图方向改变透明度。为了计算阴影图层的透明度,你需要获得每一面的正常向量(与该面垂直的向量)然后计算它们与想象中光线来源的的向量积。向量积可以告诉你光线源和这个图层的角度,我表明了它应当被照亮的程度。

这个方法的一种实现是表5.10。我们用了GLKit框架来做向量计算(你需要在你的项目中引入这一框架)。每一个面的CATransform3D使用一些指针方法转型为一个GLKMatrix4,然后这个3*3_旋转矩阵_使用GLKMatrix4GetMatrix3函数扩展。这个旋转矩阵是用来决定图层方向的变形的一部分,我们可以用它来计算正常向量。

图5.22展示了结果。尝试微调LIGHT_DIRECTION向量和AMBIENT_LIGHT值来调整光影效果。

表5.10 给立方体表面应用动态光影效果
import UIKit
import GLKit

let LIGHT_DIRECTON = GLKVector3Make(0, 1, -0.5)
let AMBIENT_LIGHT: CGFloat = 0.5

class ViewController: UIViewController {

    @IBOutlet weak var containerView: UIView!
    var faces: Array<UIView> = []

    func getRandomColor() -> UIColor {
        // 创建随机颜色
        let red: CGFloat = CGFloat(arc4random() % 255) / 255.0
        let green: CGFloat = CGFloat(arc4random() % 255) / 255.0
        let blue: CGFloat = CGFloat(arc4random() % 255) / 255.0
        return UIColor(red: red, green: green, blue: blue, alpha: 1)
    }

    func createFace(number: NSInteger) -> UIView {
        // 创建表面视图
        let face = UIView(frame: CGRectMake(0, 0, 200, 200))
        face.backgroundColor = UIColor.whiteColor()

        // 创建标签
        let label = UILabel(frame: CGRectZero)
        label.text = number.description
        label.textColor = getRandomColor()
        label.font = UIFont.systemFontOfSize(40)

        // 将标签放在中间
        label.sizeToFit()   // 用于将标签的bounds大小设为恰好符合
        let faceSize = face.bounds.size
        label.center = CGPointMake(faceSize.width / 2.0, faceSize.height / 2.0)

        // 将标签加入表面视图
        face.addSubview(label)

        return face
    }

    func applyLightingToFace(face: CALayer) {
        // 增加光线层
        let layer = CALayer()
        layer.frame = face.bounds
        face.addSublayer(layer)

        // 转换face的变形的矩阵
        // GLKMatrix4有和CATransform3D一样的结构
        let transform = face.transform
        let matrix4: GLKMatrix4 = GLKMatrix4Make(Float(transform.m11), Float(transform.m12), Float(transform.m13), Float(transform.m14), Float(transform.m21), Float(transform.m22), Float(transform.m23), Float(transform.m24), Float(transform.m31), Float(transform.m32), Float(transform.m33), Float(transform.m34), Float(transform.m41), Float(transform.m42), Float(transform.m43), Float(transform.m44))
        let matrix3: GLKMatrix3 = GLKMatrix4GetMatrix3(matrix4)

        // 获得face的正常向量
        var normal = GLKVector3Make(0, 0, 1)
        normal = GLKMatrix3MultiplyVector3(matrix3, normal)
        normal = GLKVector3Normalize(normal)

        // 获得与光向量的点积
        let light = GLKVector3Normalize(LIGHT_DIRECTON)
        let dotProduct = GLKVector3DotProduct(light, normal)

        // 设置光线层的透明度
        let shadow = 1 + CGFloat(dotProduct) - AMBIENT_LIGHT
        let color = UIColor(white: 0, alpha: shadow)
        layer.backgroundColor = color.CGColor

    }

    func addFace(index: NSInteger, withTransform transform: CATransform3D) {
        // 获得表面视图并把它加入容器中
        let face = self.faces[index]
        self.containerView.addSubview(face)

        // 将表面视图在容器中居中
        let containerSize = self.containerView.bounds.size
        face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0)

        // 应用变形
        face.layer.transform = transform

        // 应用光影
        self.applyLightingToFace(face.layer)
    }

    override func viewDidLayoutSubviews() {
        // 创建六个表面
        for (var i = 1; i <= 6; i++) {
            let face = createFace(i)
            faces.append(face)
        }

        // 设置容器子视图视图形变
        var perspective = CATransform3DIdentity
        perspective.m34 = -1.0 / 500.0
        perspective = CATransform3DRotate(perspective, CGFloat(-M_PI_4), 1, 0, 0)
        perspective = CATransform3DRotate(perspective, CGFloat(-M_PI_4), 0, 1, 0)
        self.containerView.layer.sublayerTransform = perspective

        // 添加立方体的表面1
        var transform = CATransform3DMakeTranslation(0, 0, 100)
        self.addFace(0, withTransform: transform)

        // 添加立方体的表面2
        transform = CATransform3DMakeTranslation(100, 0, 0)
        transform = CATransform3DRotate(transform, CGFloat(M_PI_2), 0, 1, 0)
        self.addFace(1, withTransform: transform)

        // 添加立方体的表面3
        transform = CATransform3DMakeTranslation(0, -100, 0)
        transform = CATransform3DRotate(transform, CGFloat(M_PI_2), 1, 0, 0)
        self.addFace(2, withTransform: transform)

        // 添加立方体的表面4
        transform = CATransform3DMakeTranslation(0, 100, 0)
        transform = CATransform3DRotate(transform, CGFloat(-M_PI_2), 1, 0, 0)
        self.addFace(3, withTransform: transform)

        // 添加立方体的表面5
        transform = CATransform3DMakeTranslation(-100, 0, 0)
        transform = CATransform3DRotate(transform, CGFloat(-M_PI_2), 0, 1, 0)
        self.addFace(4, withTransform: transform)

        // 添加立方体的表面6
        transform = CATransform3DMakeTranslation(0, 0, -100)
        transform = CATransform3DRotate(transform, CGFloat(M_PI), 0, 1, 0)
        self.addFace(5, withTransform: transform)
        

    }
}
图5.22 有动态计算光影的立方体

触摸事件

由于译者前期并没有加上按钮,因此更新了一下新的代码如下:

import UIKit
import GLKit

let LIGHT_DIRECTON = GLKVector3Make(0, 1, -0.5)
let AMBIENT_LIGHT: CGFloat = 0.5

class ViewController: UIViewController {

    @IBOutlet weak var containerView: UIView!
    var faces: Array<UIView> = []

    func getRandomColor() -> UIColor {
        // 创建随机颜色
        let red: CGFloat = CGFloat(arc4random() % 255) / 255.0
        let green: CGFloat = CGFloat(arc4random() % 255) / 255.0
        let blue: CGFloat = CGFloat(arc4random() % 255) / 255.0
        return UIColor(red: red, green: green, blue: blue, alpha: 1)
    }

    func createFace(number: NSInteger) -> UIView {
        // 创建表面视图
        let face = UIView(frame: CGRectMake(0, 0, 200, 200))
        let faceSize = face.bounds.size
        face.backgroundColor = UIColor.whiteColor()

        // 创建按钮
        let button = UIButton(frame: CGRectMake(0, 0, 100, 100))
        button.layer.cornerRadius = 25
        button.backgroundColor = UIColor.redColor()
        button.alpha = 0.1

        // 将按钮移到中间
        button.center = CGPointMake(faceSize.width / 2.0, faceSize.height / 2.0)

        // 将按钮加入表面
        face.addSubview(button)

        // 创建标签
        let label = UILabel(frame: CGRectZero)
        label.text = number.description
        label.textColor = getRandomColor()
        label.font = UIFont.systemFontOfSize(40)

        // 将标签放在中间
        label.sizeToFit()   // 用于将标签的bounds大小设为恰好符合
        label.center = CGPointMake(faceSize.width / 2.0, faceSize.height / 2.0)

        // 将标签加入表面视图
        face.addSubview(label)

        return face
    }

    func applyLightingToFace(face: CALayer) {
        // 增加光线层
        let layer = CALayer()
        layer.frame = face.bounds
        face.addSublayer(layer)

        // 转换face的变形的矩阵
        // GLKMatrix4有和CATransform3D一样的结构
        let transform = face.transform
        let matrix4: GLKMatrix4 = GLKMatrix4Make(Float(transform.m11), Float(transform.m12), Float(transform.m13), Float(transform.m14), Float(transform.m21), Float(transform.m22), Float(transform.m23), Float(transform.m24), Float(transform.m31), Float(transform.m32), Float(transform.m33), Float(transform.m34), Float(transform.m41), Float(transform.m42), Float(transform.m43), Float(transform.m44))
        let matrix3: GLKMatrix3 = GLKMatrix4GetMatrix3(matrix4)

        // 获得face的正常向量
        var normal = GLKVector3Make(0, 0, 1)
        normal = GLKMatrix3MultiplyVector3(matrix3, normal)
        normal = GLKVector3Normalize(normal)

        // 获得与光向量的点积
        let light = GLKVector3Normalize(LIGHT_DIRECTON)
        let dotProduct = GLKVector3DotProduct(light, normal)

        // 设置光线层的透明度
        let shadow = 1 + CGFloat(dotProduct) - AMBIENT_LIGHT
        let color = UIColor(white: 0, alpha: shadow)
        layer.backgroundColor = color.CGColor

    }

    func addFace(index: NSInteger, withTransform transform: CATransform3D) {
        // 获得表面视图并把它加入容器中
        let face = self.faces[index]
        self.containerView.addSubview(face)

        // 将表面视图在容器中居中
        let containerSize = self.containerView.bounds.size
        face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0)

        // 应用变形
        face.layer.transform = transform

        // 应用光影
        self.applyLightingToFace(face.layer)
    }

    override func viewDidLayoutSubviews() {
        // 创建六个表面
        for (var i = 1; i <= 6; i++) {
            let face = createFace(i)
            faces.append(face)
        }

        // 设置容器子视图视图形变
        var perspective = CATransform3DIdentity
        perspective.m34 = -1.0 / 500.0
        perspective = CATransform3DRotate(perspective, CGFloat(-M_PI_4), 1, 0, 0)
        perspective = CATransform3DRotate(perspective, CGFloat(-M_PI_4), 0, 1, 0)
        self.containerView.layer.sublayerTransform = perspective

        // 添加立方体的表面1
        var transform = CATransform3DMakeTranslation(0, 0, 100)
        self.addFace(0, withTransform: transform)

        // 添加立方体的表面2
        transform = CATransform3DMakeTranslation(100, 0, 0)
        transform = CATransform3DRotate(transform, CGFloat(M_PI_2), 0, 1, 0)
        self.addFace(1, withTransform: transform)

        // 添加立方体的表面3
        transform = CATransform3DMakeTranslation(0, -100, 0)
        transform = CATransform3DRotate(transform, CGFloat(M_PI_2), 1, 0, 0)
        self.addFace(2, withTransform: transform)

        // 添加立方体的表面4
        transform = CATransform3DMakeTranslation(0, 100, 0)
        transform = CATransform3DRotate(transform, CGFloat(-M_PI_2), 1, 0, 0)
        self.addFace(3, withTransform: transform)

        // 添加立方体的表面5
        transform = CATransform3DMakeTranslation(-100, 0, 0)
        transform = CATransform3DRotate(transform, CGFloat(-M_PI_2), 0, 1, 0)
        self.addFace(4, withTransform: transform)

        // 添加立方体的表面6
        transform = CATransform3DMakeTranslation(0, 0, -100)
        transform = CATransform3DRotate(transform, CGFloat(M_PI), 0, 1, 0)
        self.addFace(5, withTransform: transform)
        

    }
}

原著继续如下:

你可能注意到虽然我们可以看见第三个面上的按钮,但按下去后没有用。为什么呢?

这并不是因为iOS不能正确的传递触摸事件到3D中的按钮位置;它实际上是有这个能力的。这个问题是视图层次。正如我们在第3章中简单提及的,触摸事件根据它们父视图的视图顺序传递,而不是它们在3D空间的Z坐标。当我们给立方体增加面视图时,我们是以数字顺序添加的,所以面4、5、6会在面3的视图/图层顺序前(画家理论)。

即使我们看不见面4、5、6(因为它们被面1、2、3挡住了),iOS仍给予它们在触摸事件中的第一响应权。当我们尝试触摸面3上的按钮的时候,面5或面6(取决于我们按哪个)拦截了相应的触摸事件,就像我们在一个普通的2D布局中将它们放到了前面一样。

你可能认为将doubleSided设为NO可能有所帮助,因为它会将远的面视图渲染不见,但不幸的是这并不起作用;由于背对镜头而被隐藏的视图仍会拦截触摸事件(并不像用hidden属性隐蔽或设置透明度为0的视图一样),所以禁用双面渲染并不能解决这一问题(尽管它可能因为性能原因被采用)。

而解决方法也有很多:我们可以将除了面3 外的所有面视图的userInteractionEnabled设为NO,这样它们不再接受触摸。或者我们可以简单的在程序中最后添加我们的面3。无论何种方式,我们都可以点击这个按钮(如图5.23)。最终代码如下:

import UIKit
import GLKit

let LIGHT_DIRECTON = GLKVector3Make(0, 1, -0.5)
let AMBIENT_LIGHT: CGFloat = 0.5

class ViewController: UIViewController {

    @IBOutlet weak var containerView: UIView!
    var faces: Array<UIView> = []

    func getRandomColor() -> UIColor {
        // 创建随机颜色
        let red: CGFloat = CGFloat(arc4random() % 255) / 255.0
        let green: CGFloat = CGFloat(arc4random() % 255) / 255.0
        let blue: CGFloat = CGFloat(arc4random() % 255) / 255.0
        return UIColor(red: red, green: green, blue: blue, alpha: 1)
    }

    func createFace(number: NSInteger) -> UIView {
        // 创建表面视图
        let face = UIView(frame: CGRectMake(0, 0, 200, 200))
        let faceSize = face.bounds.size
        face.backgroundColor = UIColor.whiteColor()

        // 创建按钮
        let button = UIButton(frame: CGRectMake(0, 0, 100, 100))
        button.layer.cornerRadius = 25
        button.backgroundColor = UIColor.redColor()
        button.alpha = 0.1
        button.tag = number
        button.addTarget(self, action: "touchDown:", forControlEvents: UIControlEvents.TouchDown)

        // 将按钮移到中间
        button.center = CGPointMake(faceSize.width / 2.0, faceSize.height / 2.0)

        // 将按钮加入表面
        face.addSubview(button)

        // 创建标签
        let label = UILabel(frame: CGRectZero)
        label.text = number.description
        label.textColor = getRandomColor()
        label.font = UIFont.systemFontOfSize(40)

        // 将标签放在中间
        label.sizeToFit()   // 用于将标签的bounds大小设为恰好符合
        label.center = CGPointMake(faceSize.width / 2.0, faceSize.height / 2.0)

        // 将标签加入表面视图
        face.addSubview(label)

        return face
    }

    func touchDown(sender: UIButton!) {
        if (sender.tag == 3) {
            sender.alpha = 1
        }
    }

    func applyLightingToFace(face: CALayer) {
        // 增加光线层
        let layer = CALayer()
        layer.frame = face.bounds
        face.addSublayer(layer)

        // 转换face的变形的矩阵
        // GLKMatrix4有和CATransform3D一样的结构
        let transform = face.transform
        let matrix4: GLKMatrix4 = GLKMatrix4Make(Float(transform.m11), Float(transform.m12), Float(transform.m13), Float(transform.m14), Float(transform.m21), Float(transform.m22), Float(transform.m23), Float(transform.m24), Float(transform.m31), Float(transform.m32), Float(transform.m33), Float(transform.m34), Float(transform.m41), Float(transform.m42), Float(transform.m43), Float(transform.m44))
        let matrix3: GLKMatrix3 = GLKMatrix4GetMatrix3(matrix4)

        // 获得face的正常向量
        var normal = GLKVector3Make(0, 0, 1)
        normal = GLKMatrix3MultiplyVector3(matrix3, normal)
        normal = GLKVector3Normalize(normal)

        // 获得与光向量的点积
        let light = GLKVector3Normalize(LIGHT_DIRECTON)
        let dotProduct = GLKVector3DotProduct(light, normal)

        // 设置光线层的透明度
        let shadow = 1 + CGFloat(dotProduct) - AMBIENT_LIGHT
        let color = UIColor(white: 0, alpha: shadow)
        layer.backgroundColor = color.CGColor

    }

    func addFace(index: NSInteger, withTransform transform: CATransform3D) {
        // 获得表面视图并把它加入容器中
        let face = self.faces[index]
        self.containerView.addSubview(face)

        // 将表面视图在容器中居中
        let containerSize = self.containerView.bounds.size
        face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0)

        // 应用变形
        face.layer.transform = transform

        // 应用光影
        self.applyLightingToFace(face.layer)
    }

    override func viewDidLayoutSubviews() {
        // 创建六个表面
        for (var i = 1; i <= 6; i++) {
            let face = createFace(i)
            faces.append(face)
        }

        // 设置容器子视图视图形变
        var perspective = CATransform3DIdentity
        perspective.m34 = -1.0 / 500.0
        perspective = CATransform3DRotate(perspective, CGFloat(-M_PI_4), 1, 0, 0)
        perspective = CATransform3DRotate(perspective, CGFloat(-M_PI_4), 0, 1, 0)
        self.containerView.layer.sublayerTransform = perspective

        // 添加立方体的表面1
        var transform = CATransform3DMakeTranslation(0, 0, 100)
        self.addFace(0, withTransform: transform)

        // 添加立方体的表面2
        transform = CATransform3DMakeTranslation(100, 0, 0)
        transform = CATransform3DRotate(transform, CGFloat(M_PI_2), 0, 1, 0)
        self.addFace(1, withTransform: transform)

        // 添加立方体的表面4
        transform = CATransform3DMakeTranslation(0, 100, 0)
        transform = CATransform3DRotate(transform, CGFloat(-M_PI_2), 1, 0, 0)
        self.addFace(3, withTransform: transform)

        // 添加立方体的表面5
        transform = CATransform3DMakeTranslation(-100, 0, 0)
        transform = CATransform3DRotate(transform, CGFloat(-M_PI_2), 0, 1, 0)
        self.addFace(4, withTransform: transform)

        // 添加立方体的表面6
        transform = CATransform3DMakeTranslation(0, 0, -100)
        transform = CATransform3DRotate(transform, CGFloat(M_PI), 0, 1, 0)
        self.addFace(5, withTransform: transform)

        // 添加立方体的表面3
        transform = CATransform3DMakeTranslation(0, -100, 0)
        transform = CATransform3DRotate(transform, CGFloat(M_PI_2), 1, 0, 0)
        self.addFace(2, withTransform: transform)
        

    }
}
未点击前
图5.23 现在背面视图不会阻拦按钮,我们可以点击它

总结

这一章介绍了2D和3D变形。你学了一些矩阵数学,以及如何用Core Animation创建3D场景。你看见了背面的图层的样子并且了解了你不能在一个平面图像中四面观察一个物体。最后,这一章演示了当处理触摸事件时,视图或图层在层次中的顺序比它们显示在屏幕上的顺序重要。

第6章第讲解Core Animation提供的特定的CALayer子类以及它们的不同用途。

内图层图像

外图层图像

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

推荐阅读更多精彩内容