iOSで購入完了時等に表示される、チェックマークが描かれるアニメーションを実装してみたいと思います。以下は完成形です。
まず周囲を回る円(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])
次にチェックマークを描きます。前提として事前に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])
2本目の線も同様に実装し順番にアニメーションを実行していけば、チェックマークが描かれる部分が完成します。
アニメーションを少し調整してみます。
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本目の線の間にわずかにインターバルを空けています。微妙に変化が異なるのが分かると思います。
CircularProgressとチェックマークを組み合わせると完成です。
処理待ちのときに変化するボタン等に使ったりもできそうです。同時にHapticsフィードバックを組み合わせると更によさそうです。
コードはこちらで確認できます。