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