post Image
Instagramのログイン画面みたいなグラデーションのアニメーションを自分で作る

導入

Pastelというライブラリがありまして、これを使うとInstagramのログイン画面ライクなアニメーションを実現できます。どんなアニメーションなのかはREADMEを見ていただくのが一番早いのですが、グラデーションでじわじわと色が変わっていく動きをします。

Pastel_03.gif
(https://github.com/cruisediary/Pastel のREADMEより出典)

おしゃれで素敵なライブラリなのですが、よくよく動きを見てみるとCoreAnimationを少し書けば実現できそうな感じがします。実際にライブラリ自体もとてもスッキリとミニマムなものとなっています。そこで今回これを作るのに必要な要素を分解しながら、自分で同じようなアニメーションを実装してみました。

方法

やることは次のようになります。

  1. CAGradientLayerでグラデーションを作る
  2. CABasicAnimationでCAGradientLayerのcolorsプロパティを変更する
  3. アニメーションが完了したら、2.のアニメーションを再帰的に実行する

ポイントとしては、グラデーションの前回のStart Colorを次のアニメーションではEnd Colorにすることで滑らかな動きを実現できます。それを抑えればそこまで難しくなく、非常にシンプルにいい感じのものが実現できます。

以下に実装の詳細をまとめます。

実装詳細

今回はこちらに掲載されていたInstagramで使われているアイコン色をつかって徐々に変化させていこうと思います。

1. CAGradientLayerでグラデーションを作る

GradientLayerを使って“Instagram Blue”から“Instagram Purple Violet”へのグラデーションを作成します。
Instagramらしくするために、右上から左下にかけてグラデーションを書けます。したがって、startPointは(0, 1), endPoint(1, 0)になるかと思います。

class InstagramLikeView: UIView {
    let gradientLayer = CAGradientLayer()

    override func awakeFromNib() {
        super.awakeFromNib()

        gradientLayer.startPoint = CGPoint(x: 0, y: 1)
        gradientLayer.endPoint = CGPoint(x: 1, y: 0)
        gradientLayer.colors = [
           #colorLiteral(red: 0.5411764706, green: 0.2274509804, blue: 0.7254901961, alpha: 1), // Instagram Purple Violet
           #colorLiteral(red: 0.2980392157, green: 0.4078431373, blue: 0.8431372549, alpha: 1) // Instagram Blue
        ].map { $0.cgColor }
        gradientLayer.drawsAsynchronously = true
        layer.addSublayer(gradientLayer)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        gradientLayer.frame = bounds
    }
}

するとInstagramのログイン画面っぽいグラデーションができます。

スクリーンショット 2017-06-04 22.48.41.png

2. CABasicAnimationでCAGradientLayerのcolorsプロパティを変更する

続いてアニメーションを作成します。はじめの状態からじわじわと色が左下に下っていき、“Instagram Maroon”“Instagram Blue”のグラデーションへ変化してくアニメーションを作成します。
実装自体は割と平凡で、CABasicAnimationを使ってkeyPathへ指定した値を変更するだけです。ここではグラデーション色を変更したいため、colorsの値を指定します。

先程のInstagramLikeViewを次のように変更します。

class InstagramLikeView: UIView {

    let gradientLayer = CAGradientLayer()

    override func awakeFromNib() {
        super.awakeFromNib()

        gradientLayer.startPoint = CGPoint(x: 0, y: 1)
        gradientLayer.endPoint = CGPoint(x: 1, y: 0)
        gradientLayer.colors = [
           #colorLiteral(red: 0.5411764706, green: 0.2274509804, blue: 0.7254901961, alpha: 1),
           #colorLiteral(red: 0.2980392157, green: 0.4078431373, blue: 0.8431372549, alpha: 1)
        ].map { $0.cgColor }
        gradientLayer.drawsAsynchronously = true
        layer.addSublayer(gradientLayer)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        gradientLayer.frame = bounds
    }

    // アニメーションを実行するためにメソッドを実装する。
    // CABasicAnimationをCAGradientLayerオブジェクトに追加して、アニメーションを実行
    func aniamte() {
        let fromColors = gradientLayer.colors
        let anim = CABasicAnimation(keyPath: "colors")
        anim.fromValue = fromColors
        anim.toValue =  [
            #colorLiteral(red: 0.8039215686, green: 0.2823529412, blue: 0.4196078431, alpha: 1), // Instagram Maroon
            #colorLiteral(red: 0.2980392157, green: 0.4078431373, blue: 0.8431372549, alpha: 1)  // Instagram Blue 
        ].map { $0.cgColor }
        anim.duration = 5
        anim.fillMode = kCAFillModeForwards
        anim.isRemovedOnCompletion = false
        gradientLayer.add(anim, forKey: "colors")
    }

}

作成したInstagramLikeViewをインスタンス化して貼り、animate()を実行すると、きれいにグラデーションの色変化が起きるのが見て取れます。

output2.gif

3. アニメーションが完了したら、2.のアニメーションを再帰的に実行する

最後に順番に色が変わっていくアニメーションを作成すれば完成です。
2で作ったInstagramLikeViewを次のように変更します。

class InstagramLikeView: UIView, CAAnimationDelegate { // CAAnimationDelegateを適用
    let gradientLayer = CAGradientLayer()

    // colorsプロパティを追加する。
    // 色の配列を用意。indexの順番にグラデーションが変化していくようにする。
    // https://designpieces.com/palette/instagram-new-logo-2016-color-palette/ の各色
    let colors: [CGColor] = [
       #colorLiteral(red: 0.5411764706, green: 0.2274509804, blue: 0.7254901961, alpha: 1), 
       #colorLiteral(red: 0.2980392157, green: 0.4078431373, blue: 0.8431372549, alpha: 1), 
       #colorLiteral(red: 0.8039215686, green: 0.2823529412, blue: 0.4196078431, alpha: 1), 
       #colorLiteral(red: 0.9843137255, green: 0.6784313725, blue: 0.3137254902, alpha: 1), 
       #colorLiteral(red: 0.9882352941, green: 0.8, blue: 0.3882352941, alpha: 1), 
       #colorLiteral(red: 0.737254902, green: 0.1647058824, blue: 0.5529411765, alpha: 1),
      #colorLiteral(red: 0.9137254902, green: 0.3490196078, blue: 0.3137254902, alpha: 1)
    ].map { $0.cgColor }

    // currentIndexプロパティを追加する。
    // 現在のグラデーションがcolorsのどのIndexであるかを保存する。
    // 右上から左下にかけて変化させていきたので、開始位置が終了位置より1つ上のIndexになるようにする
    var currentIndex = (start: 1, end: 0)

    override func awakeFromNib() {
        super.awakeFromNib()
        gradientLayer.startPoint = CGPoint(x: 1, y: 0)
        gradientLayer.endPoint = CGPoint(x: 0, y: 1)

        // 最初のcurrentIndexが設定されるように変更する
        gradientLayer.colors = [colors[currentIndex.start], colors[currentIndex.end]]

        gradientLayer.drawsAsynchronously = true
        layer.addSublayer(gradientLayer)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        gradientLayer.frame = bounds
    }

    func aniamte() {
        // 次の色が指定されるようにindexをそれぞれ1進める。
        // Indexが配列サイズを超えそうになったらまた0の位置からループされるように剰余計算をする。
        currentIndex = ((currentIndex.start + 1) % colors.count, (currentIndex.end + 1) % colors.count)

        let fromColors = gradientLayer.colors
        let anim = CABasicAnimation(keyPath: "colors")
        anim.fromValue = fromColors

        // 1進めたIndexが設定されるように変更する
        anim.toValue =  [colors[currentIndex.start], colors[currentIndex.end]]

        anim.duration = 5

        // anim.fillMode = kCAFillModeForwards <- animationDidStopで終了状態の値を入れるのでこれはもういらない
        // anim.isRemovedOnCompletion = false <- animationDidStopで終了状態の値を入れるのでこれはもういらない

        // CAAnimationDelegateを設定する
        anim.delegate = self
        gradientLayer.add(anim, forKey: "colors")
    }

    // animationDidStopを新たに実装する。
    // CABasicアニメーションが完了したら、次のアニメーションが再帰的に呼ばれるようにして、永遠とアニメーションを行う
    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        gradientLayer.colors = [colors[currentIndex.start], colors[currentIndex.end]]
        aniamte()
    }
}

InstagramLikeViewを貼って、animate()実行すると以下のようになります。

これで、Instagram風のグラデーションアニメーションが実装できました。

output.gif

まとめ

CAGradientLayerとCABasicAnimationを使ってInstagramのログイン画面風アニメーションがシンプルに実装できました。余談ですがColor LiteralってXcode上では直感的でわかりやすいのに、それをQiitaとかにコピペするとわけわかんないですね。

参考資料


『 Swift 』Article List