😏

【React Native + Expo】ドラッグ&ドロップで座標を取得する

2023/03/18に公開

どうも〜つぶあんです。
React Native+Expoでドラッグ&ドロップできて、移動した場所の座標を取得する方法の自分用メモ書きです。

準備

準備として下記のライブラリをインストールします。

$ npx expo install react-native-reanimated
$ npx expo install react-native-gesture-handler

次にbabel.config.jsを修正します。

module.exports = function (api) {
  api.cache(true);
  return {
    presets: ["babel-preset-expo"],
    plugins: [
      require.resolve("expo-router/babel"),
      'react-native-reanimated/plugin', //ここを追加
    ],
  };
};

いきなりの記述例

import { StyleSheet, Text, View } from "react-native";
import React, { useState } from "react";
import { PanGestureHandler } from "react-native-gesture-handler";
import Animated, {
  runOnJS,
  useAnimatedGestureHandler,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
} from "react-native-reanimated";

const BOXSIZE = 200;

const dragScreen = () => {

  // State
  const [x, setX] = useState(0);
  const [y, setY] = useState(0);
  const [viewWidth, setViewWidth] = useState(0);
  const [viewHeight, setViewHeight] = useState(0);

  // 初期の位置
  const positionX = useSharedValue(x);
  const positionY = useSharedValue(y);

  // 
  const gestureHandler = useAnimatedGestureHandler({
    // 開始時
    onStart: (event, context) => {
      context.x = positionX.value;
      context.y = positionY.value;
    },
    // 移動中
    onActive: (event, context) => {
      positionX.value = event.translationX + context.x;
      positionY.value = event.translationY + context.y;

      // 画面外にでないようにする
      if (positionX.value < 0) {
        positionX.value = 0;
      }
      if (positionX.value > viewWidth - BOXSIZE) {
        positionX.value = viewWidth - BOXSIZE;
      }
      if (positionY.value < 0) {
        positionY.value = 0;
      }
      if (positionY.value > viewHeight - BOXSIZE) {
        positionY.value = viewHeight - BOXSIZE;
      }
      runOnJS(setX)(positionX.value);
      runOnJS(setY)(positionY.value);
    },
    // 移動後
    onEnd: (event, context) => {
      positionX.value = withSpring(x);
      positionY.value = withSpring(y);
      console.log("X座標:", x);
      console.log("Y座標:", y);
    },
  });

  const AStyle = useAnimatedStyle(() => {
    return {
      transform: [
        {
          translateX: positionX.value,
        },
        {
          translateY: positionY.value,
        },
      ],
    };
  });

  // Viewのサイズを取得
  const getWindowSize = (e) => {
    setViewWidth(e.nativeEvent.layout.width);
    setViewHeight(e.nativeEvent.layout.height);
  }

  return (
    <View style={styles.container} onLayout={getWindowSize}>
      <PanGestureHandler onGestureEvent={gestureHandler}>
        <Animated.View style={[styles.box, AStyle]}>
          <Text style={styles.text}>X座標:</Text>
          <Text style={styles.text}>{x}</Text>
          <Text style={styles.text}>Y座標:</Text>
          <Text style={styles.text}>{y}</Text>
        </Animated.View>
      </PanGestureHandler>
    </View>
  );
};

export default dragScreen;

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  box: {
    width: BOXSIZE,
    height: BOXSIZE,
    backgroundColor: "red",
    padding: 10,
  },
  text: {
    color: "#fff",
  },
});

解説

開始位置を指定。

useSharedValue(x)
useSharedValue(y)

useAnimatedGestureHandlerで開始時、移動中、移動後のそれぞれのコールバックを指定します。

useAnimatedGestureHandler({
    onStart: (event, context) => {
        // 開始時
    },
    onActive: (event, context) => {
        // 移動中
    },
    onEnd: (event, context) => {
        // 移動後
        // ここで移動後の座標を取得
    },
  });

ドラッグし終わって再度ドラッグしようとすると開始位置に戻ってしまう為、
onActiveの時に開始位置の座標を足しています。

positionX.value = event.translationX + context.x;
positionY.value = event.translationY + context.y;

useAnimatedStyleでスタイルを定義。

useAnimatedStyle(() => {
    return {
      transform: [
        {
          translateX: positionX.value,
        },
        {
          translateY: positionY.value,
        },
      ],
    };
  })

PanGestureHandlerで全体を囲いジェスチャーイベントを指定して、Animated.Viewのスタイルに上記で定義したスタイルを指定します。

      <PanGestureHandler onGestureEvent={gestureHandler}>
        <Animated.View style={[styles.box, AStyle]}>
          <Text style={styles.text}>X座標:</Text>
          <Text style={styles.text}>{x}</Text>
          <Text style={styles.text}>Y座標:</Text>
          <Text style={styles.text}>{y}</Text>
        </Animated.View>
      </PanGestureHandler>

画面外にドラッグできてしまうので、画面外に出ないようにします。
まずは画面の幅を取得します。

  const getWindowSize = (e) => {
    setViewWidth(e.nativeEvent.layout.width);
    setViewHeight(e.nativeEvent.layout.height);
  }

そしてViewのonLayoutプロパティのコールバック関数から取得します。

    <View style={styles.container} onLayout={getWindowSize}>

取得したサイズからはみ出さないように記述します。

      // 左端にハミ出さないように
      if (positionX.value < 0) {
        positionX.value = 0;
      }
      // 右端にハミ出さないように
      if (positionX.value > viewWidth - BOXSIZE) {
        positionX.value = viewWidth - BOXSIZE;
      }
      // 上端にハミ出さないように
      if (positionY.value < 0) {
        positionY.value = 0;
      }
      // 下端にハミ出さないように
      if (positionY.value > viewHeight - BOXSIZE) {
        positionY.value = viewHeight - BOXSIZE;
      }

以上で、ボックスをドラッグ&ドロップして座標を取得する記述例でした。
なるほどね〜。

Discussion