LoginSignup
5
4

More than 3 years have passed since last update.

CGAffineTransformを使ってLayerアニメーションをしてみる

Posted at

はじめに

CoreGraphicsのCGAffineTranaformを使った回転拡大/縮小移動せん断のアニメーションサンプルのメモです。

回転

    //ラジアン計算
    let angle = 180 * CGFloat.pi / 180
    //時計回りに180°
    let transform = CGAffineTransform(rotationAngle: angle)
    UIView.animate(withDuration: 1.0, animations: {
        self.animationView.transform = transform
    }) { _ in
        //元に戻す
        self.identity()
    }

IMB_RCJlHr.GIF

拡大•縮小

拡大

    // `1`がDefaultValue
    let transform = CGAffineTransform(scaleX: 2, y: 2)
    UIView.animate(withDuration: 1.0, animations: {
        self.animationView.transform = transform
    }) { _ in
        self.identity()
    }

IMB_Hblptq.GIF

縮小

    let transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
    UIView.animate(withDuration: 1.0, animations: {
        self.animationView.transform = transform
    }) { _ in
        self.identity()
    }

IMB_D2PNgu.GIF

移動

    let transform = CGAffineTransform(translationX: 100, y: 100)
    UIView.animate(withDuration: 1.0, animations: {
        self.animationView.transform = transform
    }) { _ in
        self.identity()
    }

IMB_aFDeoU.GIF

せん断

    let transform = CGAffineTransform(a: 1, b: 0, c: 1, d: 1, tx: 0, ty: 0)
    UIView.animate(withDuration: 1.0, animations: {
        self.animationView.transform = transform
    }) { _ in
        self.identity()
    }

IMB_oEoArI.GIF

メモ

CGAffineTranaformは回転拡大・縮小移動についてはそれぞれのコンストラクタがありますが、
せん断についてはアフィン変換行列から指定する必要があります。最後に指定方法をまとめます。

アニメーションの原点を左上にしてみる

Point

layerのアニメーションの原点はanchorPointで変更することができます。ただ、anchorPointを修正するとアニメーションの表示位置が意図通りにならないことがあるので、元のFrameにLayer.Frameを合わせることで正しい位置に表示するようにします。

回転

    let angle = 180 * CGFloat.pi / 180
    let transform = CGAffineTransform(rotationAngle: angle)
    // anchorPointを変更しつつ、表示位置を変更させないための小技
    let tmpFrame = animationView.frame
    animationView.layer.anchorPoint = CGPoint(x: 0, y: 0)
    animationView.layer.frame = tmpFrame
    UIView.animate(withDuration: 1.0, animations: {
        self.animationView.transform = transform
    }) { _ in
        self.identity()
    }

IMB_vL4ORl.GIF

拡大

    let transform = CGAffineTransform(scaleX: 2, y: 2)
    let tmpFrame = animationView.frame
    animationView.layer.anchorPoint = CGPoint(x: 0, y: 0)
    animationView.layer.frame = tmpFrame
    UIView.animate(withDuration: 1.0, animations: {
        self.animationView.transform = transform
    }) { _ in
        self.identity()
    }

IMB_l5XcBV 2.GIF

縮小

    let transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
    let tmpFrame = animationView.frame
    animationView.layer.anchorPoint = CGPoint(x: 0, y: 0)
    animationView.layer.frame = tmpFrame
    UIView.animate(withDuration: 1.0, animations: {
        self.animationView.transform = transform
    }) { _ in
        self.identity()
    }

IMB_rkVnXX.GIF

移動

    let transform = CGAffineTransform(translationX: 100, y: 100)
    let tmpFrame = animationView.frame
    animationView.layer.anchorPoint = CGPoint(x: 0, y: 0)
    animationView.layer.frame = tmpFrame
    UIView.animate(withDuration: 1.0, animations: {
        self.animationView.transform = transform
    }) { _ in
        self.identity()
    }

IMB_SQwRcc.GIF

せん断

    let transform = CGAffineTransform(a: 1, b: 0, c: 1, d: 1, tx: 0, ty: 0)
    let tmpFrame = animationView.frame
    animationView.layer.anchorPoint = CGPoint(x: 0, y: 0)
    animationView.layer.frame = tmpFrame
    UIView.animate(withDuration: 1.0, animations: {
        self.animationView.transform = transform
    }) { _ in
        self.identity()
    }

IMB_Rll1f7.GIF

アニメーションを組み合わせてみる

Point

CGAffineTransformをアニメーションとして組み合わせる場合はtransformをセットするタイミングで連結することで上手くアニメーションが作動します。アニメーションをセットする前にCGAffineTransformを連結すると最初のtransformのみ実行されるので注意が必要です。

回転&拡大

    let tmpFrame = animationView.frame
    let angle = 180 * CGFloat.pi / 180
    let transform = CGAffineTransform(scaleX: 2, y: 2)
    let transform2 = CGAffineTransform(rotationAngle: angle)
    animationView.layer.anchorPoint = CGPoint(x: 0, y: 0)
    animationView.layer.frame = tmpFrame
    UIView.animate(withDuration: 1.0, animations: {
        // 組み合わせる時はtransformをセットするタイミングで連結しないとうまくいかない
        self.animationView.transform = transform.concatenating(transform2)
    }) { _ in
        self.identity()
    }

IMB_zRoyvZ.GIF

アフィン変換について

swiftからは回転移動拡大・縮小などに関するアフィン変換についてはそれぞれラッパーとなるコンストラクタがありますが、せん断やカスタムでLayerを描画したい場合は下記の行列を指定して実装する必要があります。三列目は常に(0, 0, 1)などで実質データ構造としては最初の二列を指定します。

\begin{bmatrix}
a & b & 0 \\
c & d & 0\\
tx & ty & 1
\end{bmatrix}

a, b, c, d, tx, ty それぞれの役割はこんな感じになり、tx・tyはそのままの意味で、それぞれ平行移動する距離をあらわします。

\begin{bmatrix}
a & b & 0 \\
c & d & 0\\
tx & ty & 1
\end{bmatrix}

👉

\begin{bmatrix}
x.scale & y' & 0 \\
x' & y.scale & 0\\
tx & ty & 1
\end{bmatrix}


ちなみに何も指定していない場合のDefault値は以下になります。

\begin{bmatrix}
a & b & 0 \\
c & d & 0\\
tx & ty & 1
\end{bmatrix}

👉

\begin{bmatrix}
1 & 0 & 0 \\
0 & 1 & 0\\
0 & 0 & 1
\end{bmatrix}


行列を指定して実装してみる

拡大

    // x, y とも2倍
    let transform = CGAffineTransform(a: 2, b: 0,
                                      c: 0, d: 2,
                                      tx: 0, ty: 0)
    UIView.animate(withDuration: 1.0, animations: {
        self.animationView.transform = transform
    }) { _ in
        self.identity()
    }

縮小

    // x, y とも1/2倍
    let transform = CGAffineTransform(a: 0.5, b: 0,
                                      c: 0, d: 0.5,
                                      tx: 0, ty: 0)
    UIView.animate(withDuration: 1.0, animations: {
        self.animationView.transform = transform
    }) { _ in
        self.identity()
    }

移動

     // x方向に100pt移動
     let transform = CGAffineTransform(a: 1, b: 0,
                                       c: 0, d: 1,
                                       tx: 100, ty: 0)
     UIView.animate(withDuration: 1.0, animations: {
         self.animationView.transform = transform
     }) { _ in
         self.identity()
     }

せん断

せん断前のセクションで実装した通り下記のようになります。なお指定するCGFloatの移動距離はx.scale * cになります。今回は1を指定しているのでviewのframe.width分xの正方向に軸が傾きます。

    // x.scale(width) * 1 軸が傾く
    let transform = CGAffineTransform(a: 1, b: 0,
                                      c: 1, d: 1,
                                      tx: 100, ty: 0)
    UIView.animate(withDuration: 1.0, animations: {
        self.animationView.transform = transform
    }) { _ in
        self.identity()
    }

回転

回転に関するアフィン変換は少し複雑で、それぞれx'y'を導き出す数式を知っていると上手く変換できます。回転の場合下記のようなアフィン変換で考えるとわかりやすい。



\begin{bmatrix}
x' \\
y'  
\end{bmatrix}

👉

\begin{bmatrix}
cosθ & -sinθ \\
sinθ & cosθ
\end{bmatrix}

アフィン変換で表すと

\begin{bmatrix}
cosθ & sinθ & 0 \\
-sinθ & cosθ & 0\\
tx & ty & 1
\end{bmatrix}


数学的な見解が知りたい場合はここで説明してるサイトがわかりやすかったです。

     // 90°時計回りに回転
     let angle = 90 * CGFloat.pi / 180
     let sinAngle = sin(angle)
     let cosAngle = cos(angle)
     let transform = CGAffineTransform(a: cosAngle, b: sinAngle,
                                          c: -sinAngle, d: cosAngle,
                                          tx: 0, ty: 0)
     UIView.animate(withDuration: 1.0, animations: {
         self.animationView.transform = transform
     }) { _ in
            self.identity()
     }

さいごに

Githubにコードアップしました!
https://github.com/YamatoOtaka/CGAffineTransFormSample

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4