今天让我们讨论一下在iOS App的立体图像处理,而要讲这题目就不可不讲解 –CATransform3D。CATransform3D 是一个用来处理 3D 影像,像是旋转、缩放、平移等 3D 影像的控制。CATransform3D 是采用三维坐标系统,x 轴向下为正,y 向右为正,z 轴则是垂直于屏幕向外为正。
你可以这样理解CATransform3D ,它本身就是一个 4×4 的矩阵:
在这边,我们不需要了解矩阵中每一个数字是什么意思,因为在 CATransform3D 中已经有方法可以处理大部分的功能:
? CATransform3DTranslate
? CATransform3DRotate
? CATransform3DInvert
? CATransform3DScale
? CATransform3DAffine
如果只说道理,相信很难理解CATransform3D的运作,最好还是实作一个简单的范例程序以阐释当中的原理。在这篇,我要教大家如何做出一个可以被转动的骰子,下图就是完成品:
开始建立范例App
如常开启Xcode并使用Single View Application建立一个新项目。在Main.storyboard,先在View Controller 做一个简单的 View,将背景颜色转为蓝色(或其他颜色也可以)。
在 ViewController 加入IBOutlet,并将之连接至新建立的View:
@IBOutlet weak var blueView: UIView!
如何将View变得立体
现在要加入一个方法让 View (即blueView) 以 y 轴为中心旋转 45度,CATransform3D 的方法都是输入一个矩阵去改变内容。以 CATransform3DRotate 为例,第一个参数是定义好的矩阵,之后再用角度与 x、y、z 所形成的向量做旋转:
func viewTransform() {
var transform = CATransform3DIdentity
let angle = CGFloat(45)
blueView.layer.transform = CATransform3DRotate(transform, angle, 0, 1, 0)
}
之后,再在viewDidLoad()方法呼叫viewTransform():
override func viewDidLoad() {
super.viewDidLoad()
viewTransform()
}
现在可先试试执行程序,你会发觉那个View实际上旋转之后并没有 3D 的感觉,只是变瘦的 View 。
在真实世界中,当物体远离我们时,由于视角的关系会看起来变小,理论上来说,比较远的边会比近的边要短,但是实际上对系统来说他们是一样长的,所以我们要做一些修正。透过设置 CATransform3D 的 m34 为 -1.0 / d 来让影像有远近的 3D 效果,d 代表了想象中视角与屏幕的距离,这个距离只需要大概估算,不需要很精准的计算。
解说:m34 用于按比例缩放 x 和 y 的值来呈现视角的远近。
现在修改viewTransform()并加入一行程序代码以设置m34:
func viewTransform() {
var transform = CATransform3DIdentity
let angle = CGFloat(45)
transform.m34 = -1 / 500
blueView.layer.transform = CATransform3DRotate(transform, angle, 0, 1, 0)
}
完成后,再试试执行App看看效果如何。现在那个应该有一种立体的感觉:
手势控制
接下来我们要改为手势控制这个 View 的角度,在ViewController类别先加一个angle变量:
angle = CGPoint.init(x: 0, y: 0)
之后,修改viewTransform方法:
func viewTransform(sender: UIPanGestureRecognizer) {
let point = sender.translation(in: blueView)
let angleX = angle.x + (point.x/30)
let angleY = angle.y - (point.y/30)
var transform = CATransform3DIdentity
transform.m34 = -1 / 500
transform = CATransform3DRotate(transform, angleX, 0, 1, 0)
transform = CATransform3DRotate(transform, angleY, 1, 0, 0)
blueView.layer.transform = transform
if sender.state == .ended {
angle.x = angleX
angle.y = angleY
}
}
另外,因为我们会用UIPanGestureRecognizer来识别手势,viewDidLoad也作出相应的修改:
override func viewDidLoad() {
super.viewDidLoad()
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(viewTransform))
blueView.addGestureRecognizer(panGesture)
}
在这边用 CATransform3DRotate 的方法对 X 轴与 Y 轴做转动,以手势在 View 上每次移动的相对坐标为基准(即是sender.translation(in: blueView)),直接去更改整个 View 的翻转角度。
又再试一试执行App,尝试用鼠标转动View,你会发现非常失控!
因为判定旋转角度的依据,就是手势在这个 View 上面移动的位置,而 View 在转动的时候坐标会随着旋转而不停的变动,以至于手势无法准确的控制,所以要拿一个不会动的对象当作基准,并且不能因为转动而改变可控的面积,解决办法如下(改动的程序代码以黄色标示):
override func viewDidLoad() {
super.viewDidLoad()
let subView = UIView.init(frame: blueView.bounds)
subView.backgroundColor = UIColor.blue
blueView.addSubview(subView)
blueView.backgroundColor = UIColor.clear
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(viewTransform))
blueView.addGestureRecognizer(panGesture)
}
func viewTransform(sender: UIPanGestureRecognizer) {
let point = sender.translation(in: blueView)
let angleX = angle.x + (point.x/30)
let angleY = angle.y - (point.y/30)
var transform = CATransform3DIdentity
transform.m34 = -1 / 500
transform = CATransform3DRotate(transform, angleX, 0, 1, 0)
transform = CATransform3DRotate(transform, angleY, 1, 0, 0)
blueView.layer.sublayerTransform = transform
if sender.state == .ended {
angle.x = angleX
angle.y = angleY
}
}
View 需要用手势移动当作依据,所以不直接对这个 View 做旋转,而是旋转 View 里面的 sublayer,layer 里面的有个方法可以实作这个功能 sublayerTransform ,并把内容以 subView 的方式加入,然后把 blueView 的 backgroundColor 拿掉,这样就能很正常的转动了。
将普通的View变成骰子
接下来,就是把这个 blueView 换成骰子。然而骰子并不是平面,是一个立体的对象,那要如何在平面上做出一个立体的对象?在这边要利用 CATransform3DTranslate 与 CATransform3DRotate 来做出立体对象的效果,首先做骰子的 1、2、3 点。
现在先下载骰子的图像,并将所有的图加进Xcode项目。我们会先把 View 修改成 1 点,顺便修改 View 的名称。先将原本的blueView从storyboard 删除,之后在ViewController建立diceView:
let diceView = UIView()
将以下viewDidLoad以及viewTransform方法更新至如下,同时加入一个新方法addDice:
override func viewDidLoad() {
super.viewDidLoad()
addDice()
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(viewTransform))
diceView.addGestureRecognizer(panGesture)
}
func addDice() {
let viewFrame = UIScreen.main.bounds
diceView.frame = CGRect(x: 0, y: viewFrame.maxY / 2 - 50, width: viewFrame.width, height: 100)
//1
let dice1 = UIImageView.init(image: UIImage(named: "dice1"))
dice1.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
diceView.addSubview(dice1)
view.addSubview(diceView)
}
func viewTransform(sender: UIPanGestureRecognizer) {
let point = sender.translation(in: diceView)
let angleX = angle.x + (point.x/30)
let angleY = angle.y - (point.y/30)
var transform = CATransform3DIdentity
transform.m34 = -1 / 500
transform = CATransform3DRotate(transform, angleX, 0, 1, 0)
transform = CATransform3DRotate(transform, angleY, 1, 0, 0)
diceView.layer.sublayerTransform = transform
if sender.state == .ended {
angle.x = angleX
angle.y = angleY
}
}
我们在addDice建立相等于blue view的view并同时加载1点的图像,如你试执行程序,应该会得到以下的效果:
加入骰子第 2、3 点,并分别垂直于 1 点:
func addDice() {
let viewFrame = UIScreen.main.bounds
var diceTransform = CATransform3DIdentity
diceView.frame = CGRect(x: 0, y: viewFrame.maxY / 2 - 50, width: viewFrame.width, height: 100)
//1
let dice1 = UIImageView.init(image: UIImage(named: "dice1"))
dice1.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
//2
let dice2 = UIImageView.init(image: UIImage(named: "dice2"))
dice2.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
diceTransform = CATransform3DRotate(CATransform3DIdentity, (CGFloat.pi / 2), 0, 1, 0)
dice2.layer.transform = diceTransform
//3
let dice3 = UIImageView.init(image: UIImage(named: "dice3"))
dice3.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
diceTransform = CATransform3DRotate(CATransform3DIdentity, (-CGFloat.pi / 2), 1, 0, 0)
dice3.layer.transform = diceTransform
diceView.addSubview(dice1)
diceView.addSubview(dice2)
diceView.addSubview(dice3)
view.addSubview(diceView)
}
又再试一试效果,看来不错!但还是不太像一颗骰子。
要解决这问题,我们就要平移每一个 imageView。CATransform3DRotate 不是只有转了影像,而是整个坐标系统,所以只需要每个面的 z 轴都增加(减少) 50 就能够做一半的正方体:
func addDice() {
let viewFrame = UIScreen.main.bounds
var diceTransform = CATransform3DIdentity
diceView.frame = CGRect(x: 0, y: viewFrame.maxY / 2 - 50, width: viewFrame.width, height: 100)
//1
let dice1 = UIImageView.init(image: UIImage(named: "dice1"))
dice1.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
diceTransform = CATransform3DTranslate(diceTransform, 0, 0, 50)
dice1.layer.transform = diceTransform
//2
let dice2 = UIImageView.init(image: UIImage(named: "dice2"))
dice2.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
diceTransform = CATransform3DRotate(CATransform3DIdentity, (CGFloat.pi / 2), 0, 1, 0)
diceTransform = CATransform3DTranslate(diceTransform, 0, 0, 50)
dice2.layer.transform = diceTransform
//3
let dice3 = UIImageView.init(image: UIImage(named: "dice3"))
dice3.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
diceTransform = CATransform3DRotate(CATransform3DIdentity, (-CGFloat.pi / 2), 1, 0, 0)
diceTransform = CATransform3DTranslate(diceTransform, 0, 0, 50)
dice3.layer.transform = diceTransform
diceView.addSubview(dice1)
diceView.addSubview(dice2)
diceView.addSubview(dice3)
view.addSubview(diceView)
}
现在再试试执行程序,应该有了三个面!
剩下的就是把三个对应的面加进去,就能完成一颗骰子:
func addDice() {
let viewFrame = UIScreen.main.bounds
var diceTransform = CATransform3DIdentity
diceView.frame = CGRect(x: 0, y: viewFrame.maxY / 2 - 50, width: viewFrame.width, height: 100)
//1
let dice1 = UIImageView.init(image: UIImage(named: "dice1"))
dice1.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
diceTransform = CATransform3DTranslate(diceTransform, 0, 0, 50)
dice1.layer.transform = diceTransform
//6
let dice6 = UIImageView.init(image: UIImage(named: "dice6"))
dice6.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
diceTransform = CATransform3DTranslate(CATransform3DIdentity, 0, 0, -50)
dice6.layer.transform = diceTransform
//2
let dice2 = UIImageView.init(image: UIImage(named: "dice2"))
dice2.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
diceTransform = CATransform3DRotate(CATransform3DIdentity, (CGFloat.pi / 2), 0, 1, 0)
diceTransform = CATransform3DTranslate(diceTransform, 0, 0, 50)
dice2.layer.transform = diceTransform
//5
let dice5 = UIImageView.init(image: UIImage(named: "dice5"))
dice5.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
diceTransform = CATransform3DRotate(CATransform3DIdentity, (-CGFloat.pi / 2), 0, 1, 0)
diceTransform = CATransform3DTranslate(diceTransform, 0, 0, 50)
dice5.layer.transform = diceTransform
//3
let dice3 = UIImageView.init(image: UIImage(named: "dice3"))
dice3.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
diceTransform = CATransform3DRotate(CATransform3DIdentity, (-CGFloat.pi / 2), 1, 0, 0)
diceTransform = CATransform3DTranslate(diceTransform, 0, 0, 50)
dice3.layer.transform = diceTransform
//4
let dice4 = UIImageView.init(image: UIImage(named: "dice4"))
dice4.frame = CGRect(x: viewFrame.maxX / 2 - 50, y: 0, width: 100, height: 100)
diceTransform = CATransform3DRotate(CATransform3DIdentity, (CGFloat.pi / 2), 1, 0, 0)
diceTransform = CATransform3DTranslate(diceTransform, 0, 0, 50)
dice4.layer.transform = diceTransform
diceView.addSubview(dice1)
diceView.addSubview(dice2)
diceView.addSubview(dice3)
diceView.addSubview(dice4)
diceView.addSubview(dice5)
diceView.addSubview(dice6)
view.addSubview(diceView)
}
在这边要注意的是 1 跟 6 都没有旋转,所以要一正一负。好了!终于完成,现在执行程就会有一颗会旋转骰子。
希望你透过这个实作对 CATransform3D 有所了解,如有问题,请留言给我。另外,你可以在这里下载完整的范例项目。