よく見かけるチェックマークのアニメーションをSwift+iOSでやってみた

iPhoneやiPadのアプリを使っていると、時々手書きのようにアニメーションするチェックマークを見かけます。
あれってどうやってるんだろ・・・と思い、早速自分でもiOS上で実装してみましたので、その実装内容をご紹介します。
似たような事をやろうとしている人の参考になればと幸いです。

完成したものがこんな感じ。

iOSでアニメーションさせる方法は複数あると思いますが、今回はCoreAminationライブラリを使用しました。

実装の大きな流れとしては、

  1. 外枠の円を描くCAShapeLayerを作成する
  2. 円内部のチェックマークの座標決め
  3. チェックマークを描くCAShapeLayerを作成する
  4. 描画対象のViewに作成したCAShapeLayerをaddしてアニメーション開始

になります。以下、順を追って。

外枠の円を描くCAShapeLayerの作成

まず、線を描画するのでCAShapeLayerのインスタンスを生成して必要な設定をおこないます。ここで、このレイヤーにおける線の色や太さ、線の終端を丸くするなどの設定を行います。

// 円を描くレイヤーを作成
let circleLayer = CAShapeLayer()
// 描画領域の設定
circleLayer.frame = view.bounds
// レイヤーの背景色
circleLayer.backgroundColor = UIColor.clear.cgColor
// 線の色
circleLayer.strokeColor = UIColor.green.cgColor
// 線で囲まれた内部(円の内部)の塗り潰し色は透明
circleLayer.fillColor = UIColor.clear.cgColor
// 線の太さ
circleLayer.lineWidth = CGFloat(25)
// 線の終端を角丸とする
circleLayer.lineCap = .round

次にUIBezierPathを利用して円を描く時のパスを生成し、レイヤーに設定します。
引数のstartAngleendAngleは円を描く開始角度と終了角度の事ですが、開始角度が円の右端となり、そこから時計回りに指定するので、注意です。
clockwiseは、パスの向きです。時計回りの場合はtrue、半時計周りではfalseとなります

// 円周率(π)
let pi = CGFloat.pi
// 中心点
let centerPoint = CGPoint(x: rect.width / 2.0, y: rect.width / 2.0)
// 半径
let radius = CGFloat(rect.width / 2.0)
// 開始角度(円の右端が0、そこから時計回りに1周で2π)
let startAngle = pi / 2    // π/2(90度)で円の下側を開始点とする
// 終了角度
let endAngle = startAngle + (2 * pi)    // 開始角度に2π(360度)足して一周した位置

// ベジェパスを作成
let circlePath = UIBezierPath(arcCenter: centerPoint,
                              radius: radius,
                              startAngle: startAngle,
                              endAngle: endAngle,
                              clockwise: true)  // trueで時計回り
// ベジェパスからCGPathを取得してレイヤーのパスとする
circleLayer.path = circlePath.cgPath

最後に、CABasicAnimationクラスを使用して、設定した円を描くパスをどのようにアニメーションさせるか決めます。
パスの始点を0.0、終点を1.0として、パス上の位置をどのようにアニメーションさせるかを設定します。

let animation = CABasicAnimation(keyPath: "strokeEnd")
// アニメーション開始時の値は0.0
animation.fromValue = 0.0
// アニメーション終了時の値は1.0
animation.toValue = 1.0
// アニメーションする時間(値を変化させる時間)は0.8秒
animation.duration = CFTimeInterval(0.8)
// durationの時間内で値をどのように変化させるか。今回はEaseIn-EaseOutを指定。
animation.timingFunction
    = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
// アニメーションをLayerに設定
circleLayer.add(animation, forKey: nil)

円を描くアニメーションレイヤーが出来上がりました。

円内部のチェックマークの座標決め

続いてチェックマークですが、こちらは単純な図形ではないため、少し面倒臭いです。Bezier曲線で描く方法もありますが、大した図形でもないので力技で座標指定でアニメーションさせます。
そのためにまずは線を描く座標を決めます。

私の場合は、Adobe IllustratorがあったのでViewと同じサイズでチェックマークを描き、そのパスから座標を決めました。Illustratorでなくてもベクタ系画像ツールであれば大体パスから座標が拾えると思います。
他にも方眼紙使ってアナログに決めるとか、python使ってn次曲線を引いて座標をきめるwとか、色々やり方あると思いますが、要は座標さえ決められればよいのでツール・手段は問いません。
今回描いたチェックマークは、こんな感じ。240×240のサイズで、同時に円も描くのでその余白も考慮してます。

Illustratorで作ったチェックマーク

描いたチェックマークのパスに均等にアンカーポイントを割り振り、それぞれの座標をメモ。(エンジニアらしくない力技w)

割り振ったアンカーポイント

結果はこんな感じになります。

# 240x240サイズ基準での座標
 42.2784, 118.7159
 51.9758, 129.4688
 61.6744, 140.2230
 71.3770, 150.9817
 81.0807, 161.7416
 90.7791, 172.4956
 98.5625, 162.2959
106.8509, 152.5011
115.5835, 143.0999
124.7762, 134.1420
134.4632, 125.7198
147.8296, 115.5880
161.8935, 106.4529
176.6142,  98.4059
191.9338,  91.5967
207.8558,  86.4064

これをコードに落とし込んでいきます。

チェックマークを描くCAShapeLayerの作成

円の時と同じく、まずはCAShapeLayerのインスタンスを生成して、各種設定。

// 線を描くレイヤーを作成
let checkmarkLayer = CAShapeLayer()
// 描画領域の設定
checkmarkLayer.frame = view.bounds
// レイヤーの背景色
checkmarkLayer.backgroundColor = UIColor.clear.cgColor
// 線の色
checkmarkLayer.strokeColor = lineStrokeColor
// 線で囲まれた内部の塗り潰し色
checkmarkLayer.fillColor = UIColor.clear.cgColor
// 線の太さ
checkmarkLayer.lineWidth = lineWidth
// 線の終端を四角とする
checkmarkLayer.lineCap = .square

続いてパスをレイヤーに流し込んでいきます。
ベタで座標を書き込んでいくのはコードが冗長になるので、まずは座標をプロパティに定義。(座標調整のやりかたがダサくてゴメンナサイ)

// チェックマークの開始点
private var checkmarkStartPoints: CGPoint {
    // 下の定数座標は240x240のサイズの場合のため実際のサイズに調整する
    return CGPoint(x: 42.2784 * bounds.width / 240.0, y: 118.7159 * bounds.height / 240.0)
}
// チェックマークの線の移動先座標群
private var checkmarkMovePoints: [CGPoint] {
    // 下の定数座標は240x240のサイズの場合のため実際のサイズに調整する
    let ratioX = bounds.width / 240.0
    let ratioY = bounds.height / 240.0
    return [
       CGPoint(x: 51.9758 * ratioX, y: 129.4688 * ratioY),
       CGPoint(x: 61.6744 * ratioX, y: 140.2230 * ratioY),
       CGPoint(x: 71.3770 * ratioX, y: 150.9817 * ratioY),
       CGPoint(x: 81.0807 * ratioX, y: 161.7416 * ratioY),
       CGPoint(x: 90.7791 * ratioX, y: 172.4956 * ratioY),
       CGPoint(x: 98.5625 * ratioX, y: 162.2959 * ratioY),
       CGPoint(x: 106.8509 * ratioX, y: 152.5011 * ratioY),
       CGPoint(x: 115.5835 * ratioX, y: 143.0999 * ratioY),
       CGPoint(x: 124.7762 * ratioX, y: 134.1420 * ratioY),
       CGPoint(x: 134.4632 * ratioX, y: 125.7198 * ratioY),
       CGPoint(x: 147.8296 * ratioX, y: 115.5880 * ratioY),
       CGPoint(x: 161.8935 * ratioX, y: 106.4529 * ratioY),
       CGPoint(x: 176.6142 * ratioX, y: 98.4059 * ratioY),
       CGPoint(x: 191.9338 * ratioX, y: 91.5967 * ratioY)
   ]
}

チェックマークのパスとなるUIBezierPathを生成します。
まずはパスの開始点をチェックマーク開始点に移動して、そこから先ほどのプロパティを利用してパスをUIBezierPathにaddLineしていき、最後にレイヤーにパスを設定。

// ベジェパスを作成
let checkmarkPath = UIBezierPath()
// 開始点に移動
checkmarkPath.move(to: checkmarkStartPoints)
// 移動先にパスを引いていく
checkmarkMovePoints.forEach {
    checkmarkPath.addLine(to: $0)
}
// ベジェパスからCGPathを取得してレイヤーのパスとする
checkmarkLayer.path = checkmarkPath.cgPath

最後に、円の時と同様にレイヤーにアニメーション設定を行います。
ここでポイントとなるのがtimingFunctionです。
今回円のレイヤーと同時にアニメーションさせるので、チェックマークだけアニメーションの開始タイミングを遅らせる必要があります。
そのため、timingFunctionで円を描くアニメーションのdurationだけアニメ開始時間を遅らせる事で、連続したアニメーションに見せます。

// アニメーションをLayerに設定
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fillMode = .backwards     // アニメーション開始まで始点に戻しておく
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = CFTimeInterval(0.4)  // チェックマークを描くアニメーション時間
animation.timingFunction
    = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
animation.beginTime = CACurrentMediaTime() + CFTimeInterval(0.8)  // 円が描画完了するまでアニメーションを遅延させる
checkmarkLayer.add(animation, forKey: nil)

チェックマークを描くアニメーションレイヤーが出来上がりました。

描画対象のViewに作成したCAShapeLayerをaddしてアニメーション開始。

最後に、作成したCAShapeLayerをアニメーションさせるViewにaddSubViewすると、アニメーションが開始されます。

// アニメーションレイヤーを描画対象のViewのレイヤーに設定
view.layer.addSublayer(circleLayer)
view.layer.addSublayer(checkmarkLayer)

最後に

最終的に出来上がったXcodeのプロジェクトは、githubにも公開していますので、興味がある方は是非試してみてください。

今回はたくさんあるCoreAminationの機能うち一部しか使っていませんが、他にも面白そうな機能があり、色々試したくなりました。
また何か面白いアニメーションネタを思いついたら投稿したいと思います。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA