利用 CGAffineTransform 控制元件縮放,位移,旋轉的三種方法
利用 CGAffineTransform 我們可以控制元件的縮放,位移,旋轉,而且方法不只一種,因為 CGAffineTransform 可以作用在以下三種元件。
- UIView
- CALayer
- UIBezierPath
將 CGAffineTransform 作用於 UIView
關於 CGAffineTransform 的說明和它如何作用於 view,可參考以下連結的說明。
接下來讓我們以畫星星為例,進一步說明 CGAffineTransform 如何作用於 CALayer & UIBezierPath。
將 CGAffineTransform 作用於 CALayer
呼叫 CALayer 的 setAffineTransform(_:)。
func setAffineTransform(_ m: CGAffineTransform)
畫出星星的右半邊
以下範例採用 SwiftUI 預覽 UIBezierPath 的繪圖結果,因此畫圖的程式寫在 function makeUIView。
func makeUIView(context: Context) -> UIView {
let view = UIView()
let path = UIBezierPath()
path.move(to: CGPoint(x: 182, y: 27))
path.addLine(to: CGPoint(x: 224, y: 120))
path.addLine(to: CGPoint(x: 322, y: 139))
path.addLine(to: CGPoint(x: 258, y: 217))
path.addLine(to: CGPoint(x: 270, y: 326))
path.addLine(to: CGPoint(x: 182, y: 282))
let rightLayer = CAShapeLayer()
rightLayer.path = path.cgPath
rightLayer.fillColor = UIColor.clear.cgColor
rightLayer.strokeColor = UIColor.black.cgColor
rightLayer.lineWidth = 5
view.layer.addSublayer(rightLayer)
return view
}
畫出星星的左半邊
我們可以傻傻地找出星星左半邊的座標,然後將它繪製出來。不過其實有更聰明的方法,我們可以利用 CGAffineTransform 實現鏡像翻轉,將星星的左半邊變成右半邊。
func makeUIView(context: Context) -> UIView {
let view = UIView()
let path = UIBezierPath()
path.move(to: CGPoint(x: 182, y: 27))
path.addLine(to: CGPoint(x: 224, y: 120))
path.addLine(to: CGPoint(x: 322, y: 139))
path.addLine(to: CGPoint(x: 258, y: 217))
path.addLine(to: CGPoint(x: 270, y: 326))
path.addLine(to: CGPoint(x: 182, y: 282))
let rightLayer = CAShapeLayer()
rightLayer.path = path.cgPath
rightLayer.fillColor = UIColor.clear.cgColor
rightLayer.strokeColor = UIColor.black.cgColor
rightLayer.lineWidth = 5
view.layer.addSublayer(rightLayer)
let leftLayer = CAShapeLayer()
leftLayer.path = path.cgPath
let boundingBox = path.cgPath.boundingBox
leftLayer.bounds = boundingBox
leftLayer.position = CGPoint(x: boundingBox.midX, y: boundingBox.midY)
let transform = CGAffineTransform(scaleX: -1, y: 1).translatedBy(x: boundingBox.width, y: 0)
leftLayer.setAffineTransform(transform)
leftLayer.fillColor = UIColor.clear.cgColor
leftLayer.strokeColor = UIColor.red.cgColor
leftLayer.lineWidth = 5
view.layer.addSublayer(leftLayer)
return view
}
結果
rightLayer 是星星的右半邊,leftLayer 是星星的左半邊。透過 CGAffineTransform 的 scaledBy(x: -1, y: 1)
將讓星星水平鏡像翻轉,不過為什麼還要加上 translatedBy(x: boundingBox.width, y: 0)
移動位置呢 ?
因為鏡像翻轉後,星星的左半邊將跟右半邊重疊,因此我們必須將它往左移動半邊星星的寬度,也就是 boundingBox.width。由於 scaledBy(x: -1, y: 1) 讓 x 的座標變成相反,因此 translatedBy(x: boundingBox.width, y: 0) 的 x 傳入大於 0 的數字時將變成往左移動,
UIBezierPath 搭配 CGAffineTransform
呼叫 UIBezierPath 的 apply(_:)。
func apply(_ transform: CGAffineTransform)
概念和剛剛將 CGAffineTransform 作用於 CALayer 的例子類似,只是換成作用在 UIBezierPath 上。
func makeUIView(context: Context) -> UIView {
let view = UIView()
let path = UIBezierPath()
path.move(to: CGPoint(x: 182, y: 27))
path.addLine(to: CGPoint(x: 224, y: 120))
path.addLine(to: CGPoint(x: 322, y: 139))
path.addLine(to: CGPoint(x: 258, y: 217))
path.addLine(to: CGPoint(x: 270, y: 326))
path.addLine(to: CGPoint(x: 182, y: 282))
let rightLayer = CAShapeLayer()
rightLayer.path = path.cgPath
rightLayer.fillColor = UIColor.clear.cgColor
rightLayer.strokeColor = UIColor.black.cgColor
rightLayer.lineWidth = 5
view.layer.addSublayer(rightLayer)
let moveDistance = path.bounds.maxX + path.bounds.minX - path.bounds.width
let transform = CGAffineTransform(translationX: moveDistance, y: 0).scaledBy(x: -1, y: 1)
path.apply(transform)
let leftLayer = CAShapeLayer()
leftLayer.path = path.cgPath
leftLayer.fillColor = UIColor.clear.cgColor
leftLayer.strokeColor = UIColor.red.cgColor
leftLayer.backgroundColor = UIColor.yellow.cgColor
leftLayer.lineWidth = 5
view.layer.addSublayer(leftLayer)
return view
}
利用 CGAffineTransform 繪製三個三角形
func makeUIView(context: Context) -> UIView {
let view = UIView()
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 100, y: 0))
path.addLine(to: CGPoint(x: 100, y: 100))
path.close()
let triangleLayer1 = CAShapeLayer()
triangleLayer1.path = path.cgPath
view.layer.addSublayer(triangleLayer1)
let triangleLayer2 = CAShapeLayer()
triangleLayer2.path = path.cgPath
triangleLayer2.setAffineTransform(CGAffineTransform(translationX: 100, y: 100))
view.layer.addSublayer(triangleLayer2)
let triangleLayer3 = CAShapeLayer()
triangleLayer3.path = path.cgPath
triangleLayer3.setAffineTransform(CGAffineTransform(translationX: 200, y: 200).scaledBy(x: 0.5, y: 0.5))
view.layer.addSublayer(triangleLayer3)
return view
}
利用 CGAffineTransform 繪製多個角落生物
參考可愛 Cathie 繪製的角落貓咪,定義 function createCat 產生 cat 的 CAShapeLayer。
func createCat() -> CAShapeLayer {
let layer = CAShapeLayer()
layer.addSublayer(addTail())
layer.addSublayer(addCatFrame())
layer.addSublayer(addEar())
layer.addSublayer(addSpotBright())
layer.addSublayer(addSpotDark())
layer.addSublayer(addNose())
layer.addSublayer(addWhiskers())
layer.addSublayer(addClaws())
return layer
}
在 function makeUIView 裡產生四種 cat 的 CAShapeLayer,利用 CGAffineTransform 控制牠的縮放,位移和旋轉。
func makeUIView(context: Context) -> UIView {
let view = UIView()
let normalCatLayer = createCat()
view.layer.addSublayer(normalCatLayer)
let smallCatLayer = createCat()
smallCatLayer.setAffineTransform(CGAffineTransform(scaleX: 0.5, y: 0.5).translatedBy(x: 200, y: 480))
view.layer.addSublayer(smallCatLayer)
let mirrorCatLayer = createCat()
mirrorCatLayer.setAffineTransform(CGAffineTransform(scaleX: -0.3, y: 0.3).concatenating(CGAffineTransform(translationX: 200, y: 0)))
view.layer.addSublayer(mirrorCatLayer)
let rotateCatLayer = createCat()
rotateCatLayer.setAffineTransform(CGAffineTransform(rotationAngle: .pi / 180 * 45).translatedBy(x: 300, y: 200))
view.layer.addSublayer(rotateCatLayer)
return view
}