2022-09-06

react-native-skiaでチェックマークアニメーションを実装する

iOSで購入完了時等に表示される、チェックマークが描かれるアニメーションを実装してみたいと思います。以下は完成形です。

Loading Skia..

周りの円を実装

まず周囲を回る円(CircularProgress)を実装します。

円に対し一定の角度の弧を描き開始角度をアニメーションで変更していきます。

Skiaで弧を描くのは以前の記事 Skiaで円弧を描くでも行ったのと同様にPathを使ってaddArcします。

const path = Skia.Path.Make()
const arcRect = {
  x: center.x - radius,
  y: center.y - radius,
  width: radius * 2,
  height: radius * 2,
}
path.addArc(arcRect, progress.current * 360, 300)

<Canvas>
  <Path
    path={path}
    color="#0091FF"
    style="stroke"
    strokeWidth={6}
    strokeCap="round"
  />
</Canvas>

円弧

これをrunTimingでアニメーションループし進捗に応じて円弧の開始角度を変更させます。

const arcPath = useComputedValue(() => {
  const path = Skia.Path.Make()
  const arcRect = {
    x: center.x - radius,
    y: center.y - radius,
    width: radius * 2,
    height: radius * 2,
  }
  path.addArc(arcRect, progress.current * 360, 300)

  return path
}, [progress])

const runAnimation = useCallback(() => {
  runTiming(progress, { loop: true, yoyo: false }, { duration: 2000 })
}, [progress])

useEffect(() => {
  runAnimation()
}, [runAnimation])
Loading Skia..

チェックマークを描く

次にチェックマークを描きます。前提として事前に3つのポイントの座標を把握しておきます。

まず静的な状態でPathで描いてみます。lineToを使って各ポイントからポイントに直線を引くだけです。

const path = Skia.Path.Make()
path.moveTo(checkMarkPoints[0].x, checkMarkPoints[0].y)
path.lineTo(checkMarkPoints[1].x, checkMarkPoints[1].y)
path.lineTo(checkMarkPoints[2].x, checkMarkPoints[2].y)

<Canvas>
  <Path
    path={path}
    color="#0091FF"
    style="stroke"
    strokeWidth={6}
    strokeCap="round"
    strokeJoin="round"
  />
</Canvas>
動かない状態のチェックマーク

これを順番に線を描くようなアニメーションにしてみたいと思います。アニメーション進捗に応じて線の長さを変更してあげればできそうです。

今回はチェックマークの線を2つのPathに分けて1本ずつ順番にアニメーションしていきます。1つのPathで描くことも可能ですが、その場合進捗と線の長さの計算が少し複雑になるため今回はあえて分けます。(線が多い場合や線を最終的につなげる場合は1つのPathで行った方がコントロールしやすいと思います)

線を引くため現在のアニメーション進捗に応じて線の終点を計算します。まず1つ目のポイントから2つ目のポイントまでのベクトルを取得します。

function getVector(p1: { x: number; y: number }, p2: { x: number; y: number }) {
  return {
    x: p2.x - p1.x,
    y: p2.y - p1.y,
  }
}

const vector = getVector(checkMarkPoints[0], checkMarkPoints[1])

取得したベクトルのxとyに対して現在の進捗比率をかけることにより、進捗(0〜1)に応じた長さにすることができます。

path.rLineTo(vector.x * progress.current, vector.y * progress.current)

lineToではなくrLineToを利用していますが、rLineToは現在の座標からの相対指定を利用して線を引くことができます。

線が途中まで描かれた状態

これをアニメーションで変化させていきます。

const progress = useValue(0)
const linePath = useComputedValue(() => {
  const path = Skia.Path.Make()
  if (progress.current === 0) {
    return path
  }
  path.moveTo(checkMarkPoints[0].x, checkMarkPoints[0].y)
  const vector = getVector(checkMarkPoints[0], checkMarkPoints[1])
  path.rLineTo(vector.x * progress.current, vector.y * progress.current)
  return path
}, [progress])

const runAnimation = useCallback(() => {
  runTiming(progress, { loop: true, yoyo: false }, { duration: 2000 })
}, [])

useEffect(() => {
  runAnimation()
}, [runAnimation])
Loading Skia..

2本目の線も同様に実装し順番にアニメーションを実行していけば、チェックマークが描かれる部分が完成します。

Loading Skia..

アニメーションを少し調整してみます。

runTiming(
  progress1,
  1,
  { duration: 300, easing: Easing.out(Easing.sin) },
  async () => {
    await wait(100)
    runTiming(progress2, 1, {
      duration: 500,
      easing: Easing.out(Easing.sin),
    })
  },
)

Easing関数を指定するのと、1本目の線と2本目の線の間にわずかにインターバルを空けています。微妙に変化が異なるのが分かると思います。

Loading Skia..

完成形

CircularProgressとチェックマークを組み合わせると完成です。

Loading Skia..

処理待ちのときに変化するボタン等に使ったりもできそうです。同時にHapticsフィードバックを組み合わせると更によさそうです。

コードはこちらで確認できます。

最終更新: 2022-09-07 03:13
筆者: @gaishimo 主にReact Nativeでのアプリ開発を行っています。
© 2021 Omoidasu, Inc. All rights reserved.