ジェスチャーと組み合わせる
react-native-gesture-handlerのGestureDetectorとReanimatedの共有値(Shared Value)を組み合わせ、指で触って動かせるSkiaの図形を作ります。
前章で、共有値(Shared Value)はSkiaコンポーネントのpropsに直接渡せることを見ました。この仕組みは、ジェスチャーによる操作と組み合わせたときに真価を発揮します。指を動かすたびに共有値を書き換えれば、その変化がUIスレッド上でそのままCanvasに反映されるからです。
React Native Skiaの公式ドキュメントでも、ジェスチャーの実装にはreact-native-gesture-handlerが推奨されています。この章では、そのGestureDetectorを使います。
GestureDetectorの基本
Gesture.Pan()でパン(ドラッグ)ジェスチャーを作り、GestureDetectorで対象のビューを包みます。
import { Canvas, Circle, Fill } from "@shopify/react-native-skia";
import { GestureDetector, Gesture } from "react-native-gesture-handler";
import { useSharedValue, withDecay } from "react-native-reanimated";
const radius = 26;
const size = 240;
const DragDemo = () => {
const cx = useSharedValue(size / 2);
const cy = useSharedValue(size / 2);
const gesture = Gesture.Pan()
.onChange((e) => {
cx.value += e.changeX;
cy.value += e.changeY;
})
.onEnd((e) => {
cx.value = withDecay({ velocity: e.velocityX, clamp: [radius, size - radius] });
cy.value = withDecay({ velocity: e.velocityY, clamp: [radius, size - radius] });
});
return (
<GestureDetector gesture={gesture}>
<Canvas style={{ flex: 1 }}>
<Fill color="white" />
<Circle cx={cx} cy={cy} r={radius} color="cyan" />
</Canvas>
</GestureDetector>
);
};
onChangeは指が動くたびに呼ばれ、e.changeX・e.changeYには直前のイベントからの移動量が入っています。指を離したonEndではwithDecayを使い、そのときの速度(velocityX・velocityY)を引き継いで慣性のある動きを付け、clampで移動範囲を制限しています。ここでのcx・cyの書き換えも、前章と同じく共有値の.valueへの直接代入です。Reactのイベントハンドラーではなく、UIスレッド上で完結する形でジェスチャーが処理されます。
要素ごとに追跡する
Canvasの中に複数の図形があり、それぞれを別々にドラッグしたい場合は、GestureDetectorを図形の数だけ用意します。ただしGestureDetectorはReact Nativeの通常のビュー階層に対して当たり判定を持つため、Skiaの図形そのものを直接ラップすることはできません。そこで、図形と同じ位置・同じ変形を持つ透明なAnimated.Viewを重ね、そちらにジェスチャーを設定します。
import { View } from "react-native";
import { Canvas, Circle, Fill } from "@shopify/react-native-skia";
import { GestureDetector, Gesture } from "react-native-gesture-handler";
import Animated, { useSharedValue, useAnimatedStyle } from "react-native-reanimated";
const radius = 30;
const ElementTracking = () => {
const x = useSharedValue(100);
const y = useSharedValue(100);
// Skiaの円と同じ位置・サイズを持つ「見えないビュー」に当たり判定を持たせる
const style = useAnimatedStyle(() => ({
position: "absolute",
top: -radius,
left: -radius,
width: radius * 2,
height: radius * 2,
transform: [{ translateX: x.value }, { translateY: y.value }],
}));
const gesture = Gesture.Pan().onChange((e) => {
x.value += e.x;
y.value += e.y;
});
return (
<View style={{ flex: 1 }}>
<Canvas style={{ flex: 1 }}>
<Fill color="white" />
<Circle cx={x} cy={y} r={radius} color="cyan" />
</Canvas>
<GestureDetector gesture={gesture}>
<Animated.View style={style} />
</GestureDetector>
</View>
);
};
円の位置を決めるx・yと、透明ビューを動かすuseAnimatedStyleのtranslateX・translateYが同じ共有値を参照しているのがポイントです。Canvas上の見た目とジェスチャーの当たり判定が、常に同じ値で連動します。
下のデモは、3つの円それぞれに専用のGestureDetectorと透明なAnimated.Viewを重ねています。それぞれを個別につまんでドラッグしてみてください。
注意点
GestureDetectorはGestureHandlerRootViewでラップされたツリーの中でしか動作しません。実際のアプリでは、ルートコンポーネントを一度GestureHandlerRootViewで囲っておけば、その配下のどこでGestureDetectorを使っても問題ありません。
パンのほかにもGesture.Pinch()(ピンチ)やGesture.Tap()(タップ)、Gesture.Simultaneous()(複数ジェスチャーの同時認識)などが用意されています。組み合わせ次第で表現の幅が広がりますが、複雑なジェスチャーほど実機とシミュレーター/ブラウザで挙動の細部が変わることがあるため、実装したら実機でも一度動きを確認することをおすすめします。
公式ドキュメントのGestures、およびreact-native-gesture-handlerのドキュメントも参照してください。