Text#onResponderMove

2025-06-06

主な特徴と用途

  • パフォーマンス
    非常に頻繁に発火する可能性があるため、onResponderMove内で重い処理を行うとパフォーマンスに影響が出る可能性があります。最適化された処理を心がけるか、requestAnimationFrameなどを利用して滑らかなアニメーションを実現するのが良いでしょう。
  • ジェスチャー認識
    ドラッグ、スワイプ、ピンチなどのジェスチャーを実装する際に利用されます。onResponderMoveのイベントデータを利用して、指の移動量や方向を計算し、それに応じてUIを更新することができます。
  • タッチレスポンダーシステム
    onResponderMoveが発火するには、Textコンポーネントがタッチレスポンダーとしてアクティブになっている必要があります。これは通常、onStartShouldSetResponderonMoveShouldSetResponderなどの他のResponder propsがtrueを返した場合に起こります。
  • イベントデータ (event.nativeEvent)
    発火時にイベントオブジェクトが渡され、そのnativeEventプロパティには以下のような情報が含まれています。
    • locationX, locationY: イベントが発生したTextコンポーネ内でのローカル座標。
    • pageX, pageY: スクリーン全体での絶対座標。
    • touches: 現在画面に触れているすべてのタッチに関する情報(複数の指が触れている場合など)。
    • identifier: 各タッチの一意のID。
    • target: イベントを受け取ったコンポーネントのノードID。
    • timestamp: イベントが発生した時間。

一般的な使用例

  • カスタムジェスチャーを認識して、特定の操作を実行する。
  • スライダーの値を指の動きに合わせて変更する。
  • 画面上の要素をドラッグして移動させる。

例 (概念的なコード)

import React, { useState } from 'react';
import { View, Text, StyleSheet } from 'react-native';

const DraggableText = () => {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [startTouch, setStartTouch] = useState(null);

  const handleResponderGrant = (event) => {
    // レスポンダーになったときに、初期のタッチ位置を記録
    setStartTouch({
      x: event.nativeEvent.pageX,
      y: event.nativeEvent.pageY,
    });
  };

  const handleResponderMove = (event) => {
    if (startTouch) {
      // 指の移動量に基づいてテキストの位置を更新
      const deltaX = event.nativeEvent.pageX - startTouch.x;
      const deltaY = event.nativeEvent.pageY - startTouch.y;

      setPosition({
        x: position.x + deltaX,
        y: position.y + deltaY,
      });

      // 新しい開始タッチ位置を更新 (連続的な移動のため)
      setStartTouch({
        x: event.nativeEvent.pageX,
        y: event.nativeEvent.pageY,
      });
    }
  };

  const handleResponderRelease = () => {
    // 指を離したら初期タッチをリセット
    setStartTouch(null);
  };

  return (
    <View style={styles.container}>
      <Text
        style={[styles.draggableText, { transform: [{ translateX: position.x }, { translateY: position.y }] }]}
        onStartShouldSetResponder={() => true} // これがないとonResponderGrantが発火しない
        onMoveShouldSetResponder={() => true} // これがないとonResponderMoveが発火しない
        onResponderGrant={handleResponderGrant}
        onResponderMove={handleResponderMove}
        onResponderRelease={handleResponderRelease}
      >
        ドラッグできるテキスト
      </Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  draggableText: {
    fontSize: 20,
    padding: 20,
    backgroundColor: 'lightblue',
    borderWidth: 1,
    borderColor: 'blue',
  },
});

export default DraggableText;

この例では、Textコンポーネントをドラッグ可能にするためにonResponderMoveを使用しています。onStartShouldSetResponderonMoveShouldSetRespondertrueにすることで、Textコンポーネントがタッチイベントのレスポンダーとなることを許可しています。



onResponderMove が発火しない

原因
最も一般的な原因は、Textコンポーネントがタッチレスポンダーとして認識されていないことです。onResponderMove は、コンポーネントがタッチレスポンダーになった後にのみ発火します。

トラブルシューティング

  • 他のコンポーネントがイベントを消費している

    • Textコンポーネントの上にある透明なViewや、同じ位置に重なっている別のコンポーネントがタッチイベントを捕捉している可能性があります。
    • 解決策
      • z-index を調整して、Textコンポーネントが他の要素より前面に来るようにします。
      • 問題のコンポーネントのレイアウトを確認し、不要なオーバーレイがないか確認します。
      • pointerEvents プロパティを none に設定することで、特定の要素がタッチイベントを無視するようにできます。
  • 親コンポーネントがレスポンダーを奪っている

    • 親コンポーネントが onStartShouldSetResponderCaptureonMoveShouldSetResponderCapturetrue にしている場合、子コンポーネント(Text)はレスポンダーになれないことがあります。
    • 解決策
      親コンポーネントのジェスチャーハンドリングを見直し、必要に応じて子コンポーネントがイベントを受け取れるように設定を変更します。例えば、親のonStartShouldSetResponderCapturefalseにするか、子でonResponderTerminationRequestを適切に処理します。
    • Textコンポーネントをタッチレスポンダーにするためには、最低限 onStartShouldSetResponder または onMoveShouldSetResponder のどちらかが true を返す必要があります。
    • 解決策
      Textコンポーネントに onStartShouldSetResponder={() => true} を追加します。指を動かし始めたときにレスポンダーになりたい場合は onMoveShouldSetResponder={() => true} も検討してください。
    <Text
      onStartShouldSetResponder={() => true} // これが重要
      onResponderGrant={handleResponderGrant}
      onResponderMove={handleResponderMove}
      onResponderRelease={handleResponderRelease}
    >
      動くテキスト
    </Text>
    

onResponderMove が意図せず発火する / 期待通りの挙動ではない

原因
意図しないコンポーネントがレスポンダーになっているか、イベント伝播が適切に制御されていないことが考えられます。

トラブルシューティング

  • イベントのバブリング/キャプチャフェーズ

    • タッチイベントはバブリングフェーズ(一番深いコンポーネントから親へ)とキャプチャフェーズ(親から一番深いコンポーネントへ)を持ちます。意図しないコンポーネントがキャプチャフェーズでイベントを捕捉している可能性があります。
    • 解決策
      • onStartShouldSetResponderCaptureonMoveShouldSetResponderCapture を確認し、不要なキャプチャがないか確認します。
      • イベントオブジェクトの event.stopPropagation() を使って、特定の地点でイベントの伝播を停止させることができますが、これは慎重に使うべきです。
  • 不適切なレスポンダー設定

    • 複数のコンポーネントが同時にレスポンダーになろうとしている場合、React Native のジェスチャーレスポンダーシステムが優先順位を決定します。意図しないコンポーネントがレスポンダーを獲得している可能性があります。
    • 解決策
      • どのコンポーネントが実際にレスポンダーになっているかを確認するために、onResponderGrant でログを出力してみます。
      • onMoveShouldSetResponder のロジックをより厳密にし、特定の条件を満たす場合のみ true を返すようにします。例えば、特定の領域内でのみドラッグを許可するなど。
      • PanResponder の利用検討
        複雑なジェスチャーや複数の要素のインタラクションを扱う場合、PanResponder の方がより柔軟で制御しやすいAPIを提供します。PanResponder はタッチイベントをより高レベルで抽象化し、ジェスチャーの状態(gestureState)を追跡するのに役立ちます。

パフォーマンスの問題

原因
onResponderMove は非常に頻繁に発火するため、その中に重い処理があるとUIのガタつきや遅延が発生することがあります。

トラブルシューティング

  • 不必要なレンダリング

    • onResponderMove の度に親コンポーネント全体が再レンダリングされると、パフォーマンスに影響します。
    • 解決策
      • 状態を管理するコンポーネントを特定し、その状態変更が影響する範囲を最小限に抑えます。
      • React.memo を使用して、子コンポーネントがpropsが変更された場合にのみ再レンダリングされるようにします。
  • 重い計算処理の最適化

    • onResponderMove 内での複雑な計算や状態更新を最小限に抑えます。
    • 解決策
      • 状態更新の頻度を制限するために、requestAnimationFrame を利用してアニメーションを滑らかにします。例えば、移動量がある程度蓄積されたら初めて状態を更新するなど。
      • useCallbackuseMemo を使用して、不要な再レンダリングや計算を防ぎます。

Android/iOS での挙動の違い

原因
プラットフォーム固有のタッチ処理やジェスチャー認識の仕組みの違いにより、同じコードでも挙動が異なることがあります。

トラブルシューティング

  • プラットフォームごとのテストと調整
    • 常に両方のプラットフォーム(AndroidとiOS)でテストを行い、挙動の違いを特定します。
    • 解決策
      • Platform API を使用して、プラットフォームごとに異なるロジックを適用します。
      import { Platform } from 'react-native';
      
      // ...
      if (Platform.OS === 'ios') {
        // iOS 固有の処理
      } else {
        // Android 固有の処理
      }
      
      • 特定のプラットフォームで既知のバグがないか、React Native のGitHub IssueやStack Overflowで検索します。
  • シンプルな再現コード
    問題が発生した場合、可能な限り問題を再現できる最小限のコードスニペットを作成します。これにより、原因の特定が容易になります。
  • React DevTools の活用
    React Native Debugger や Flipper などのツールを使用して、コンポーネントツリー、props、state の変化をリアルタイムで確認します。これにより、予期せぬ状態更新やレンダリングの問題を特定できます。
  • ログ出力
    onResponderGrant, onResponderMove, onResponderRelease など、各イベントハンドラでログを出力し、いつ、どのようなデータで発火しているかを確認します。console.log(event.nativeEvent) は非常に役立ちます。


例1:基本的なドラッグ可能なテキスト

この例では、テキストを指でドラッグして画面上を移動させます。

import React, { useState } from 'react';
import { View, Text, StyleSheet } from 'react-native';

const BasicDraggableText = () => {
  // テキストの現在の位置を管理するステート
  const [position, setPosition] = useState({ x: 0, y: 0 });
  // ドラッグ開始時のタッチ位置を管理するステート
  const [startTouch, setStartTouch] = useState(null);

  // onResponderGrant: コンポーネントがタッチレスポンダーになったときに発火
  const handleResponderGrant = (event) => {
    // ドラッグ開始時の指のスクリーン上の絶対位置を記録
    setStartTouch({
      x: event.nativeEvent.pageX,
      y: event.nativeEvent.pageY,
    });
  };

  // onResponderMove: 指が画面上を動いているときに発火
  const handleResponderMove = (event) => {
    if (startTouch) {
      // 現在の指の位置とドラッグ開始時の位置から移動量を計算
      const deltaX = event.nativeEvent.pageX - startTouch.x;
      const deltaY = event.nativeEvent.pageY - startTouch.y;

      // テキストの新しい位置を計算(既存の位置に移動量を加算)
      setPosition({
        x: position.x + deltaX,
        y: position.y + deltaY,
      });

      // 連続的なドラッグのために、次の移動量計算の基準となる開始タッチ位置を更新
      setStartTouch({
        x: event.nativeEvent.pageX,
        y: event.nativeEvent.pageY,
      });
    }
  };

  // onResponderRelease: 指が画面から離れたときに発火
  const handleResponderRelease = () => {
    // ドラッグが終了したので、開始タッチ情報をリセット
    setStartTouch(null);
  };

  return (
    <View style={styles.container}>
      <Text
        style={[
          styles.draggableText,
          // CSStransformプロパティを使って位置を適用
          { transform: [{ translateX: position.x }, { translateY: position.y }] },
        ]}
        // onStartShouldSetResponder: タッチが開始されたときにこのコンポーネントがレスポンダーになるべきか
        onStartShouldSetResponder={() => true}
        // onMoveShouldSetResponder: 指が動いたときにこのコンポーネントがレスポンダーになるべきか
        // (ドラッグ中に他のコンポーネントにレスポンダーを奪われたくない場合に重要)
        onMoveShouldSetResponder={() => true}
        onResponderGrant={handleResponderGrant}
        onResponderMove={handleResponderMove}
        onResponderRelease={handleResponderRelease}
      >
        ドラッグできるテキスト
      </Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#f0f0f0',
  },
  draggableText: {
    fontSize: 20,
    padding: 20,
    backgroundColor: 'lightblue',
    borderWidth: 1,
    borderColor: 'blue',
    borderRadius: 10,
    position: 'absolute', // 位置を自由に動かすためにabsoluteにする
  },
});

export default BasicDraggableText;

解説

  • onStartShouldSetResponder={() => true} と onMoveShouldSetResponder={() => true}
    これらは非常に重要です。これらがないと、Textコンポーネントはタッチイベントのレスポンダーになることができず、onResponderMoveを含む他のonResponder...系のコールバックは発火しません。
  • handleResponderRelease
    指が画面から離れたときに呼び出されます。startTouchをリセットします。
  • handleResponderMove
    指が画面上を移動するたびに繰り返し呼び出されます。startTouchを使って現在の指の位置からの相対的な移動量を計算し、positionステートを更新します。startTouchを常に現在のpageX/pageYで更新することで、連続的なドラッグに対応します。
  • handleResponderGrant
    指がTextコンポーネントに触れて、このコンポーネントがレスポンダーになるときに呼び出されます。ここでstartTouchを初期化します。
  • startTouch ステート
    ドラッグが開始された瞬間の指の絶対座標を保持します。これにより、指の移動量(deltaX, deltaY)を正確に計算できます。
  • position ステート
    テキストコンポーネントの現在のX, Y座標を保持します。

例2:onResponderMove を使ったシンプルなスライダー

この例では、Textコンポーネントをスライダーのつまみとして扱い、その横方向の移動に応じて値を変更します。

import React, { useState } from 'react';
import { View, Text, StyleSheet, Dimensions } from 'react-native';

const { width } = Dimensions.get('window');
const SLIDER_WIDTH = width * 0.8; // スライダーの幅を画面幅の80%に設定

const SliderWithText = () => {
  const [sliderValue, setSliderValue] = useState(50); // スライダーの現在値 (0-100)
  const [textX, setTextX] = useState(0); // テキストのX座標 (0-SLIDER_WIDTH)
  const [initialTouchX, setInitialTouchX] = useState(0); // タッチ開始時のX座標
  const [initialTextX, setInitialTextX] = useState(0); // タッチ開始時のテキストのX座標

  // onResponderGrant: スライダーつまみを掴んだとき
  const handleResponderGrant = (event) => {
    setInitialTouchX(event.nativeEvent.pageX);
    setInitialTextX(textX); // 現在のテキストのX位置を保存
  };

  // onResponderMove: 指が動いたとき
  const handleResponderMove = (event) => {
    const deltaX = event.nativeEvent.pageX - initialTouchX; // 指の移動量

    // テキストの新しいX位置を計算し、スライダーの範囲内に制限
    let newTextX = initialTextX + deltaX;
    if (newTextX < 0) {
      newTextX = 0;
    } else if (newTextX > SLIDER_WIDTH) {
      newTextX = SLIDER_WIDTH;
    }
    setTextX(newTextX);

    // スライダーの値を更新 (0-100)
    const newValue = Math.round((newTextX / SLIDER_WIDTH) * 100);
    setSliderValue(newValue);
  };

  // onResponderRelease: 指を離したとき
  const handleResponderRelease = () => {
    // 必要であればここで値を確定するなどの処理
  };

  return (
    <View style={styles.container}>
      <Text style={styles.valueText}>Current Value: {sliderValue}</Text>
      <View style={styles.sliderTrack}>
        <Text
          style={[
            styles.sliderThumb,
            { transform: [{ translateX: textX }] }, // つまみを移動
          ]}
          onStartShouldSetResponder={() => true}
          onMoveShouldSetResponder={() => true}
          onResponderGrant={handleResponderGrant}
          onResponderMove={handleResponderMove}
          onResponderRelease={handleResponderRelease}
        >
          ●
        </Text>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#f9f9f9',
  },
  valueText: {
    fontSize: 24,
    marginBottom: 30,
    fontWeight: 'bold',
  },
  sliderTrack: {
    width: SLIDER_WIDTH,
    height: 10,
    backgroundColor: '#ddd',
    borderRadius: 5,
    justifyContent: 'center',
  },
  sliderThumb: {
    position: 'absolute', // トラック内での相対位置を可能にする
    fontSize: 20,
    color: 'red',
    alignSelf: 'flex-start', // 親の左端を基準にする
  },
});

export default SliderWithText;

解説

  • textXからsliderValueへの変換(newValue = Math.round((newTextX / SLIDER_WIDTH) * 100);)を行い、値を表示します。
  • handleResponderMove では、指の移動量に応じてtextXを更新し、それがSLIDER_WIDTHの範囲内に収まるように制限します。
  • textX ステートでつまみのX座標を管理し、sliderValue で0-100の値を管理します。
  • sliderTracksliderThumb (Textコンポーネント) を使ってスライダーを構成します。

Text#onResponderMoveはシングルタッチだけでなく、複数の指の動き(event.nativeEvent.touches)も検出できます。これは、ピンチ(拡大縮小)などのジェスチャーを実装する際の基本的なデータとなります。

import React, { useState } from 'react';
import { View, Text, StyleSheet } from 'react-native';

const PinchableText = () => {
  const [scale, setScale] = useState(1); // 拡大率
  const [initialDistance, setInitialDistance] = useState(0); // 初期指間距離

  const getDistance = (touches) => {
    if (touches.length < 2) return 0;
    const touch1 = touches[0];
    const touch2 = touches[1];
    const dx = touch1.pageX - touch2.pageX;
    const dy = touch1.pageY - touch2.pageY;
    return Math.sqrt(dx * dx + dy * dy); // 2点間の距離を計算
  };

  const handleResponderGrant = (event) => {
    // 2本の指が触れた場合に初期距離を記録
    if (event.nativeEvent.touches.length >= 2) {
      setInitialDistance(getDistance(event.nativeEvent.touches));
    } else {
      setInitialDistance(0); // 1本指ならリセット
    }
  };

  const handleResponderMove = (event) => {
    if (event.nativeEvent.touches.length >= 2 && initialDistance > 0) {
      const currentDistance = getDistance(event.nativeEvent.touches);
      // 距離の変化に基づいてスケールを計算
      const newScale = (currentDistance / initialDistance) * scale;

      // 極端な拡大縮小を防ぐために範囲を制限
      if (newScale > 0.5 && newScale < 3) {
        setScale(newScale);
        // ここでinitialDistanceを更新すると、連続的なピンチが可能になりますが、
        // 単純なピンチジェスチャーの認識では最初の距離を基準にすることも多いです。
        // setInitialDistance(currentDistance); // 連続ピンチの場合はこの行をアンコメント
      }
    }
  };

  const handleResponderRelease = () => {
    setInitialDistance(0); // 指を離したらリセット
  };

  return (
    <View style={styles.container}>
      <Text
        style={[
          styles.pinchableText,
          { transform: [{ scale: scale }] }, // scaleプロパティで拡大縮小
        ]}
        onStartShouldSetResponder={() => true}
        onMoveShouldSetResponder={() => true}
        onResponderGrant={handleResponderGrant}
        onResponderMove={handleResponderMove}
        onResponderRelease={handleResponderRelease}
      >
        ピンチできるテキスト
      </Text>
      <Text style={styles.scaleValue}>Scale: {scale.toFixed(2)}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#e8e8e8',
  },
  pinchableText: {
    fontSize: 30,
    padding: 30,
    backgroundColor: '#b0e0e6',
    borderWidth: 2,
    borderColor: '#4682b4',
    borderRadius: 15,
    textAlign: 'center',
  },
  scaleValue: {
    marginTop: 20,
    fontSize: 18,
    color: '#555',
  },
});

export default PinchableText;
  • handleResponderMove
    • event.nativeEvent.touches には現在画面に触れているすべての指の情報が含まれます。
    • 2本以上の指が触れていて、initialDistanceが設定されている場合にのみ処理を実行します。
    • 現在の指間距離と初期距離の比率から新しいスケールを計算し、scaleステートを更新します。
  • handleResponderGrant
    2本指が触れた場合にのみ、initialDistanceをセットします。
  • getDistance 関数
    2つのタッチポイントオブジェクトから、ユークリッド距離を計算します。
  • initialDistance ステート
    ピンチジェスチャー開始時の2本の指間の距離を保持します。
  • scale ステート
    テキストの現在の拡大率を管理します。
  • パフォーマンス
    onResponderMoveは非常に頻繁に発火するため、重い計算や過度な状態更新はUIのガタつきを引き起こす可能性があります。
    • 複雑なアニメーションでは、Animated APIやreact-native-gesture-handlerなどのライブラリの使用を検討してください。これらはネイティブ側でイベントを処理し、JSスレッドへの負荷を軽減します。
  • onStartShouldSetResponder / onMoveShouldSetResponder
    これらのコールバックでtrueを返さない限り、onResponderMoveは発火しません。これが最も一般的な落とし穴です。


PanResponder API の利用

React Native に組み込まれている PanResponder は、複数のタッチイベント(onResponderGrant, onResponderMove, onResponderRelease など)を統合し、ジェスチャーの状態を追跡するための強力なシステムです。ドラッグ、スワイプ、ピンチなどの複雑なジェスチャーを扱うのに最適です。

特徴

  • 柔軟性
    複数のコンポーネントがジェスチャーに関与する場合や、異なるジェスチャー(例: タップとドラッグ)を区別する場合に特に便利です。
  • レスポンダーシステムの抽象化
    onStartShouldSetResponderonMoveShouldSetResponder といった低レベルなレスポンダープロパティを気にすることなく、ジェスチャーを定義できます。
  • ジェスチャー状態 (gestureState)
    dx, dy (累積移動距離), vx, vy (現在の速度), x0, y0 (ジェスチャー開始位置) などの情報を提供します。これにより、自分でこれらの値を計算する必要がなくなります。

Text#onResponderMove との比較

  • PanResponder はより構造化された方法でジェスチャーを扱えるため、コードの可読性と保守性が向上します。
  • onResponderMove は個々のムーブイベントを処理しますが、PanResponder はジェスチャー全体の状態を追跡します。

使用例 (概念)

import React, { useRef, useState } from 'react';
import { View, Text, StyleSheet, PanResponder, Animated } from 'react-native';

const PanResponderExample = () => {
  const pan = useRef(new Animated.ValueXY()).current; // アニメーション値を管理

  const panResponder = useRef(
    PanResponder.create({
      // ユーザーがタップし始めたときに、このビューがレスポンダーになるべきか
      onStartShouldSetPanResponder: () => true,
      // ユーザーが移動し始めたときに、このビューがレスポンダーになるべきか
      onMoveShouldSetPanResponder: () => true,

      // ジェスチャーが開始されたとき (onResponderGrant に相当)
      onPanResponderGrant: (evt, gestureState) => {
        // 現在のオフセットを保存しておき、新しい動きの基準にする
        pan.setOffset({
          x: pan.x._value,
          y: pan.y._value,
        });
        pan.setValue({ x: 0, y: 0 }); // オフセットを設定したら、現在の値をリセット
      },

      // ユーザーが動いているとき (onResponderMove に相当)
      onPanResponderMove: Animated.event(
        [
          null, // event オブジェクトは無視
          { dx: pan.x, dy: pan.y } // gestureState.dx/dy を pan.x/y に直接マッピング
        ],
        { useNativeDriver: false } // ネイティブドライバを使用する場合は true。ここでは Animated.ValueXY を使うので false にする
      ),

      // ユーザーが指を離したとき (onResponderRelease に相当)
      onPanResponderRelease: (evt, gestureState) => {
        // オフセットと値を結合して、新しい最終位置とする
        pan.flattenOffset();
      },
    })
  ).current;

  return (
    <View style={styles.container}>
      <Animated.View
        style={{
          transform: [{ translateX: pan.x }, { translateY: pan.y }],
        }}
        {...panResponder.panHandlers} // ここでPanResponderのハンドラを渡す
      >
        <Text style={styles.draggableText}>
          PanResponderでドラッグ
        </Text>
      </Animated.View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  draggableText: {
    fontSize: 20,
    padding: 20,
    backgroundColor: '#ffb3ba',
    borderRadius: 10,
    textAlign: 'center',
  },
});

export default PanResponderExample;

なぜこれがより良いのか?

  • gestureState オブジェクトから、ドラッグの方向、速度、総移動量など、ジェスチャーに関するより多くの情報を簡単に取得できます。
  • Animated API と統合することで、UIの更新がJavaScriptスレッドではなくネイティブスレッドで実行され、非常に滑らかなアニメーションを実現できます(useNativeDriver: true の場合)。

このライブラリは、React Native のネイティブジェスチャーシステムを直接利用することで、非常に高性能で信頼性の高いジェスチャーハンドリングを提供します。PanResponder のさらに上位の抽象化と考えることができます。

特徴

  • コンポーネントのラップ
    既存のコンポーネント(Textを含む)をこれらのジェスチャーハンドラでラップして使用します。
  • 宣言的なAPI
    JSXを使ってジェスチャーハンドラを宣言的に定義できます。
  • 豊富なジェスチャーコンポーネント
    PanGestureHandler, TapGestureHandler, PinchGestureHandler, RotationGestureHandler, FlingGestureHandler など、多様なジェスチャーに対応する専用のコンポーネントが用意されています。
  • ネイティブのジェスチャー認識
    ジェスチャー処理をネイティブモジュールにオフロードするため、JSスレッドのブロックによるUIのガタつきが大幅に減少します。

Text#onResponderMove や PanResponder との比較

  • 特に複雑なインタラクションや同時発生する複数のジェスチャーを扱う場合に真価を発揮します。
  • 学習コストはやや上がりますが、一度慣れれば非常に強力なツールとなります。
  • 最も高いパフォーマンスと複雑なジェスチャーの信頼性が必要な場合に最適です。

使用例 (PanGestureHandler でテキストをドラッグ)

まず、ライブラリをインストールします。 npm install react-native-gesture-handler react-native-reanimated npx pod-install (iOSの場合)

import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  useAnimatedGestureHandler,
} from 'react-native-reanimated';

// Animated.Text は PanGestureHandler の子に直接置けないので、Animated.View でラップ
const AnimatedTextWrapper = Animated.createAnimatedComponent(View);

const GestureHandlerExample = () => {
  const translateX = useSharedValue(0);
  const translateY = useSharedValue(0);

  // useAnimatedGestureHandler は Reanimated V2+ の新しいフック
  const gestureHandler = useAnimatedGestureHandler({
    onStart: (event, ctx) => {
      // ジェスチャー開始時のオフセットを保存
      ctx.startX = translateX.value;
      ctx.startY = translateY.value;
    },
    onActive: (event, ctx) => {
      // 移動中に translateX, translateY を更新
      translateX.value = ctx.startX + event.translationX;
      translateY.value = ctx.startY + event.translationY;
    },
    onEnd: (event, ctx) => {
      // ジェスチャー終了時の処理 (例: スナップバック、物理ベースのアニメーションなど)
    },
  });

  // アニメーションスタイルを定義
  const animatedStyle = useAnimatedStyle(() => {
    return {
      transform: [{ translateX: translateX.value }, { translateY: translateY.value }],
    };
  });

  return (
    <View style={styles.container}>
      <PanGestureHandler onGestureEvent={gestureHandler}>
        <AnimatedTextWrapper style={animatedStyle}>
          <Text style={styles.draggableText}>
            Gesture Handler でドラッグ
          </Text>
        </AnimatedTextWrapper>
      </PanGestureHandler>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  draggableText: {
    fontSize: 20,
    padding: 20,
    backgroundColor: '#baffc9',
    borderRadius: 10,
    textAlign: 'center',
  },
});

export default GestureHandlerExample;

なぜこれがより良いのか?

  • 宣言的
    JSXでジェスチャーを定義できるため、コードがより理解しやすくなります。
  • 信頼性
    ネイティブのジェスチャー認識器を使用するため、プラットフォーム(iOS/Android)間の挙動の差異が少なく、より堅牢なジェスチャーハンドリングが実現できます。
  • パフォーマンス
    ジェスチャー処理がネイティブスレッドで行われるため、JavaScriptスレッドの負荷が大幅に軽減され、非常に滑らかなアニメーションが可能です。

場合によっては、onResponderMove の代替として、特定の機能に特化した既存のライブラリやコンポーネントを使用するのが最も効率的です。

  • 画像ビューア/ズーム
    画像のピンチズームやドラッグ移動などの機能を提供するライブラリ(例: react-native-image-zoom-viewer)もあります。
  • ソート可能なリスト
    react-native-draggable-flatlistreact-native-sortable-list など、ドラッグ&ドロップでアイテムを並べ替えられるリストコンポーネントがあります。これらは内部でジェスチャーハンドリングを処理してくれます。
  • スライダー
    react-native-slider など、既にドラッグ機能が組み込まれたスライダーコンポーネントが多数存在します。
  • 特定のユースケースに最適化された機能が提供されます。
  • テスト済みの安定したコードベースを利用できます。
  • 開発時間を大幅に短縮できます。
方法特徴最適なケース
Text#onResponderMove低レベルのイベントハンドラシンプルな単一のドラッグ、カスタムジェスチャーの試作
PanResponderReact Native 内蔵、ジェスチャー状態を追跡、Animated と統合可能中程度の複雑さのドラッグ、スワイプ、ピンチジェスチャー
react-native-gesture-handlerネイティブベース、高性能、豊富なジェスチャー複雑なジェスチャー、高いパフォーマンスが求められる場合、プロダクションアプリ
特定のライブラリ事前に構築された特定の機能スライダー、ソート可能なリスト、画像ズームなど、特定のUIパターン