I.5 变形
不幸的是,没人能说出矩阵是什么,你得自己去观察。——Morpheus,The Matrix
在第4章“视觉特效”中我们讲解了一些提高图层、内容外观表现的技术。这一章中,我们研究CGAffineTransform
用于图层旋转、位移以及变形。CATransform3d
可以将一般的平面矩形(即使是有阴影的圆角矩形)变为三维平面。
仿射变形
在第3章“图层几何”中,我们用UIView
的transform
属性来旋转时钟的指针,但我们并没有解释其背后的原理。UIView
的transform
属性是CGAffineTransform
属性,被用于表现二维的旋转、缩放和位移。CGAffineTransform
是一个3行2列的矩阵,它可以乘以一个二维行向量(在这里是CGPoint
)来转换其值(见图中的黑体值)。
其乘法是将CGPoint
向量的每个列值去乘CGAffineTransform
矩阵中的每一行,然后结果可加得到一个新的CGPoint
。这在图中用灰色值表示;要想矩阵乘法成产,左边的矩阵的列数必须等于右边矩阵的行数。因此我们得用单位矩阵来填满它们,这样可以让计算得以开展而不会影响结果。我们并不真的需要存储额外的值,因为它们并不会改变但在计算时需要它们。
因此,你会经常看见一个二维变形被表示成33的矩阵(而不是23)。你也会经常看见当向量值竖直叠在一起时,这个矩阵以2行3列的格式表示。这被称为列主序格式。我们在图5.1中显示的是行主序格式。任何你喜欢的表示形式都是可以的。
图5.1 CGAffineTransform和CGPoint用来表示矩阵当变形矩阵应用在图层上时,图层矩阵的第个角上的点会独立变形,这会形成一个新的四边形。CGAffineTransform
的“仿射”是指无论矩阵用了什么值,图层中平行线变形后仍是平行的。CGAffineTransform
可以用于任何符合标准的变形。图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
属性来变形,但如同所有的布局属性一样,UIView
的transform
实际上只是一个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)。
Core Animation
提供一些如同CGAffineTransform
矩阵一样的函数来创建组合CATransform3D
矩阵。这些函数和Core Grapihcs
很像,但3D位移和缩放提供一个额外的z
参数,而且旋转函数接收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)
你现在应该对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值部分缩放远离镜头多少。
默认下,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
,但并不是对其应用的图层变形,它只影响其子图层。这意味着你可以对一个容器图层应用透视变形来影响它的子图层,它的所有子图层都会自动继承透视效果。
只需要在一个你认为方便的地方设置透视变形,但它还有一个显著优点:尽头被设为窗口图层的中心,而不是每个子图层独立设置。这意味着你可以随意使用图层的position
和frame
来移动子图层而不是先把它们移到屏幕中样再通过变形移动来保持它们的尽头不变。
让我们用一个例子演示这一点。我们会在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场景只是画在图层表面的。
译者自己代码的效果:
绕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
子类以及它们的不同用途。
外图层图像