利用 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
}

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com