[Swift] CGAffineTransformで、scaleしてからtranslateするときは注意
TL; DR;
CGAffineTransformで、Viewに複数の移動や変形を適用する時は、その適用順によって結果が変わる。
理由:
CGAffineTransformは行列の乗算によって移動先の場所を計算する。
行列の乗算は交換法則が成り立たないので、適応する順番が大事。
CGAffineTransformとは何か
Viewの移動や回転、変形のために使用される3x3行列を表す構造体。
3行目は常に
その他の部分
点
ここで"
このとき、
になる。
CGAffineTransformの例
同じoriginとsizeの2つのViewがある。(黄色のViewと、青のView)
青いViewをAffineTransformする。
Scale
縦横をそれぞれ半分(0.5倍)にする。
// Scale: 0.5
let transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
view2.transform = transform
このときのAffine行列は、
Translate
Viewのsizeはそのまま、x,y方向にそれぞれ50ずつ平行移動する。
// Translate: 50
let transform = CGAffineTransform(translationX: 50, y: 50)
view2.transform = transform
このときのAffine行列は、
Rotation
Viewのsizeと中心はそのまま、
// Rotation: 45
let transform = CGAffineTransform(rotationAngle: 45 * .pi / 180)
view2.transform = transform
このときのAffine行列は、
複数の操作を適用するとき
たとえば、Aという操作とBという操作を適用させたいとき
let transform = CGAffineTransform(A).someTransform(B)
のように書くと
のような順番で計算される。(コードではA → Bのように書いているが、実際はB → Aの順で適用される)
行列の乗算での交換法則は、一般的には成り立たない(
concatenating
を使用すると、コードに書いた順で、操作が適用される。
以下のコードは、変形や移動の操作は、A → Bの順で適用される。
let transform = CGAffineTransform(A).concatenating(CGAffineTransform(B))
Scale -> Translateの順番で適用させたいとき
let transform = CGAffineTransform(translationX: 50, y: 50).scaledBy(x: 0.5, y: 0.5)
view2.transform = transform
もしくは、
let transform2 = CGAffineTransform(scaleX: 0.5, y: 0.5).concatenating(CGAffineTransform(translationX: 50, y: 50))
view2.transform = transform
このときのAffine行列は、
Translate → Scaleの順番で適用させたいとき
// Translate: 50 -> Scale 0.5
let transform = CGAffineTransform(scaleX: 0.5, y: 0.5).translatedBy(x: 50, y: 50)
view2.transform = transform
もしくは、
let transform2 = CGAffineTransform(translationX: 50, y: 50).concatenating(CGAffineTransform(scaleX: 0.5, y: 0.5))
view2.transform = transform
このときのAffine行列は、
上記の2つの例を比較すると、Translate → ScaleとScale → Translateの場合で、Affine行列と最終的なViewが異なる。
まとめ
CGAffineTransformで、Viewに複数の移動や変形を適用する時は、その適用順によって結果が変わる。
CGAffineTransformは行列の乗算によって移動先の場所を計算する。
行列の乗算は交換法則が成り立たないので、適応する順番が大事。
Sample Code
使用したサンプルコード
import UIKit
class ViewController: UIViewController {
let view1 = UIView()
let view2 = UIView()
override func viewDidLoad() {
super.viewDidLoad()
setupView()
// setupSample_Scale()
// setupSample_Translate()
// setupSample_Rotation()
// setupSample_Scale_Translate()
// setupSample_Translate_Scale()
}
private func setupView() {
view.addSubview(view1)
view.addSubview(view2)
view1.backgroundColor = .yellow
view2.layer.borderColor = UIColor.blue.cgColor
view2.layer.borderWidth = 5
view1.frame = CGRect(x: 100, y: 100, width: 200, height: 200)
view2.frame = CGRect(x: 100, y: 100, width: 200, height: 200)
}
// Scale: 0.5
private func setupSample_Scale() {
let transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
view2.transform = transform
print(transform)
}
// Translate: 50
private func setupSample_Translate() {
let transform = CGAffineTransform(translationX: 50, y: 50)
view2.transform = transform
}
// Rotation: 45
private func setupSample_Rotation() {
let transform = CGAffineTransform(rotationAngle: 45 * .pi / 180)
view2.transform = transform
}
// Translate: 50 -> Scale 0.5
private func setupSample_Scale_Translate() {
let transform = CGAffineTransform(scaleX: 0.5, y: 0.5).translatedBy(x: 50, y: 50)
// let transform = CGAffineTransform(translationX: 50, y: 50).concatenating(CGAffineTransform(scaleX: 0.5, y: 0.5))
view2.transform = transform
}
// Scale: 0.5 -> Translate: 50
private func setupSample_Translate_Scale() {
let transform = CGAffineTransform(translationX: 50, y: 50).scaledBy(x: 0.5, y: 0.5)
// let transform = CGAffineTransform(scaleX: 0.5, y: 0.5).concatenating(CGAffineTransform(translationX: 50, y: 50))
view2.transform = transform
}
}
Discussion