Material UIでも使われているRipple Effectのボタンをreact-native-skiaで作成してみます。
以下は早速ですがこの記事の成果物です。タップ(クリック)すると波紋が拡がります。
仕組みとしてはボタン上でタップした座標を取得し、そこを中心として円を描画し拡大していけばよさそうです。
Canvas内でタップした座標はuseTouchHandler
で取得できます。取得した座標をSkiaValueに保持しその座標を中心に円を描画してみます。
const cx = useValue<number>(canvasSize.width / 2)
const cy = useValue<number>(canvasSize.height / 2 + buttonSize.height / 3)
const touchHandler = useTouchHandler({
onStart: ({ x, y }) => {
cx.current = x
cy.current = y
},
})
return (
<Canvas style={[canvasSize]} onTouch={touchHandler}>
<RoundedRect
{...buttonSize}
{...buttonPos}
r={buttonRadius}
color="lightblue"
style="fill"
/>
<Circle
cx={cx || 0}
cy={cy || 0}
r={30}
opacity={0.2}
color={rippleColor}
/>
</Canvas>
)
すると以下のようにタップで円が表示されますが、隅をタップした時にボタンの領域をはみ出して表示されてしまいます。
ボタンの領域内にしか表示されないようにするには対象を<Group>
で囲みclipします。
Group: Clip Rounded Rectangle | React Native Skia
<Group
clip={rrect(
{ ...buttonPos, ...buttonSize },
buttonRadius,
buttonRadius,
)}
>
...
</Group>
これでボタンの外側には描画されなくなります。
次は波紋の円が拡がっていくアニメーションを作成します。タップ後に半径のサイズをアニメーションで大きくしていきます。
const rippleProgress = useValue<number>(0)
const touchHandler = useTouchHandler({
onStart: ({ x, y }) => {
...
cy.current = y
rippleProgress.animation?.cancel()
rippleProgress.current = 0
runTiming(
rippleProgress,
1,
{ duration: 1000 },
() => {
setTimeout(() => {
rippleProgress.current = 0
}, 0)
},
)
},
})
const rippleRadius = useComputedValue(() => {
return interpolate(rippleProgress.current, [0, 1], [0, 300])
}, [rippleProgress])
<Circle
cx={cx}
cy={cy}
r={rippleRadius}
opacity={0.2}
color={rippleColor}
/>
これでタップすると波紋が拡がっていくようになりました。
より自然なアニメーションになるようにイージング関数を変更してみます。skiaのrunTiming
(またはuseTiming
)のオプションでeasing関数を指定できます(何も指定していない場合はlinear
が使われます)。
runTiming(
rippleProgress,
1,
{ duration: 1000, easing: Easing.out(Easing.quad) }
)
以下はeaseOutQuad
を指定した場合です。
以下はeaseInOutQuad
を指定した場合です。
Linearを指定するよりも大分自然な感じになったと思います。微妙な違いですが、easeInOutを指定した時の方が、easeOutより出だしが少し緩慢な印象です。この辺りはアニメーションの長さによっても印象が変わりそうです。
Easing関数の種類については以下が参考になります。
こちら で確認できます。