Skip to content
React Native Skia ガイドブック
Esc
navigateopen⌘Jpreview
On this page

ジェスチャーと組み合わせる

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.changeXe.changeYには直前のイベントからの移動量が入っています。指を離したonEndではwithDecayを使い、そのときの速度(velocityXvelocityY)を引き継いで慣性のある動きを付け、clampで移動範囲を制限しています。ここでのcxcyの書き換えも、前章と同じく共有値の.valueへの直接代入です。Reactのイベントハンドラーではなく、UIスレッド上で完結する形でジェスチャーが処理されます。

Skiaを読み込み中…

要素ごとに追跡する

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>
  );
};

円の位置を決めるxyと、透明ビューを動かすuseAnimatedStyletranslateXtranslateYが同じ共有値を参照しているのがポイントです。Canvas上の見た目とジェスチャーの当たり判定が、常に同じ値で連動します。

下のデモは、3つの円それぞれに専用のGestureDetectorと透明なAnimated.Viewを重ねています。それぞれを個別につまんでドラッグしてみてください。

Skiaを読み込み中…

注意点

GestureDetectorGestureHandlerRootViewでラップされたツリーの中でしか動作しません。実際のアプリでは、ルートコンポーネントを一度GestureHandlerRootViewで囲っておけば、その配下のどこでGestureDetectorを使っても問題ありません。

パンのほかにもGesture.Pinch()(ピンチ)やGesture.Tap()(タップ)、Gesture.Simultaneous()(複数ジェスチャーの同時認識)などが用意されています。組み合わせ次第で表現の幅が広がりますが、複雑なジェスチャーほど実機とシミュレーター/ブラウザで挙動の細部が変わることがあるため、実装したら実機でも一度動きを確認することをおすすめします。

公式ドキュメントのGestures、およびreact-native-gesture-handlerのドキュメントも参照してください。