Text#onResponderMove
主な特徴と用途
- パフォーマンス
非常に頻繁に発火する可能性があるため、onResponderMove
内で重い処理を行うとパフォーマンスに影響が出る可能性があります。最適化された処理を心がけるか、requestAnimationFrame
などを利用して滑らかなアニメーションを実現するのが良いでしょう。 - ジェスチャー認識
ドラッグ、スワイプ、ピンチなどのジェスチャーを実装する際に利用されます。onResponderMove
のイベントデータを利用して、指の移動量や方向を計算し、それに応じてUIを更新することができます。 - タッチレスポンダーシステム
onResponderMove
が発火するには、Textコンポーネントがタッチレスポンダーとしてアクティブになっている必要があります。これは通常、onStartShouldSetResponder
やonMoveShouldSetResponder
などの他の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
を使用しています。onStartShouldSetResponder
とonMoveShouldSetResponder
をtrue
にすることで、Text
コンポーネントがタッチイベントのレスポンダーとなることを許可しています。
onResponderMove が発火しない
原因
最も一般的な原因は、Text
コンポーネントがタッチレスポンダーとして認識されていないことです。onResponderMove
は、コンポーネントがタッチレスポンダーになった後にのみ発火します。
トラブルシューティング
-
他のコンポーネントがイベントを消費している
Text
コンポーネントの上にある透明なViewや、同じ位置に重なっている別のコンポーネントがタッチイベントを捕捉している可能性があります。- 解決策
z-index
を調整して、Text
コンポーネントが他の要素より前面に来るようにします。- 問題のコンポーネントのレイアウトを確認し、不要なオーバーレイがないか確認します。
pointerEvents
プロパティをnone
に設定することで、特定の要素がタッチイベントを無視するようにできます。
-
親コンポーネントがレスポンダーを奪っている
- 親コンポーネントが
onStartShouldSetResponderCapture
やonMoveShouldSetResponderCapture
をtrue
にしている場合、子コンポーネント(Text
)はレスポンダーになれないことがあります。 - 解決策
親コンポーネントのジェスチャーハンドリングを見直し、必要に応じて子コンポーネントがイベントを受け取れるように設定を変更します。例えば、親のonStartShouldSetResponderCapture
をfalse
にするか、子でonResponderTerminationRequest
を適切に処理します。
- 親コンポーネントが
-
Text
コンポーネントをタッチレスポンダーにするためには、最低限onStartShouldSetResponder
またはonMoveShouldSetResponder
のどちらかがtrue
を返す必要があります。- 解決策
Text
コンポーネントにonStartShouldSetResponder={() => true}
を追加します。指を動かし始めたときにレスポンダーになりたい場合はonMoveShouldSetResponder={() => true}
も検討してください。
<Text onStartShouldSetResponder={() => true} // これが重要 onResponderGrant={handleResponderGrant} onResponderMove={handleResponderMove} onResponderRelease={handleResponderRelease} > 動くテキスト </Text>
onResponderMove が意図せず発火する / 期待通りの挙動ではない
原因
意図しないコンポーネントがレスポンダーになっているか、イベント伝播が適切に制御されていないことが考えられます。
トラブルシューティング
-
イベントのバブリング/キャプチャフェーズ
- タッチイベントはバブリングフェーズ(一番深いコンポーネントから親へ)とキャプチャフェーズ(親から一番深いコンポーネントへ)を持ちます。意図しないコンポーネントがキャプチャフェーズでイベントを捕捉している可能性があります。
- 解決策
onStartShouldSetResponderCapture
やonMoveShouldSetResponderCapture
を確認し、不要なキャプチャがないか確認します。- イベントオブジェクトの
event.stopPropagation()
を使って、特定の地点でイベントの伝播を停止させることができますが、これは慎重に使うべきです。
-
不適切なレスポンダー設定
- 複数のコンポーネントが同時にレスポンダーになろうとしている場合、React Native のジェスチャーレスポンダーシステムが優先順位を決定します。意図しないコンポーネントがレスポンダーを獲得している可能性があります。
- 解決策
- どのコンポーネントが実際にレスポンダーになっているかを確認するために、
onResponderGrant
でログを出力してみます。 onMoveShouldSetResponder
のロジックをより厳密にし、特定の条件を満たす場合のみtrue
を返すようにします。例えば、特定の領域内でのみドラッグを許可するなど。- PanResponder の利用検討
複雑なジェスチャーや複数の要素のインタラクションを扱う場合、PanResponder
の方がより柔軟で制御しやすいAPIを提供します。PanResponder
はタッチイベントをより高レベルで抽象化し、ジェスチャーの状態(gestureState
)を追跡するのに役立ちます。
- どのコンポーネントが実際にレスポンダーになっているかを確認するために、
パフォーマンスの問題
原因
onResponderMove
は非常に頻繁に発火するため、その中に重い処理があるとUIのガタつきや遅延が発生することがあります。
トラブルシューティング
-
不必要なレンダリング
onResponderMove
の度に親コンポーネント全体が再レンダリングされると、パフォーマンスに影響します。- 解決策
- 状態を管理するコンポーネントを特定し、その状態変更が影響する範囲を最小限に抑えます。
React.memo
を使用して、子コンポーネントがpropsが変更された場合にのみ再レンダリングされるようにします。
-
重い計算処理の最適化
onResponderMove
内での複雑な計算や状態更新を最小限に抑えます。- 解決策
- 状態更新の頻度を制限するために、
requestAnimationFrame
を利用してアニメーションを滑らかにします。例えば、移動量がある程度蓄積されたら初めて状態を更新するなど。 useCallback
やuseMemo
を使用して、不要な再レンダリングや計算を防ぎます。
- 状態更新の頻度を制限するために、
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,
// CSSのtransformプロパティを使って位置を適用
{ 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の値を管理します。sliderTrack
とsliderThumb
(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
など)を統合し、ジェスチャーの状態を追跡するための強力なシステムです。ドラッグ、スワイプ、ピンチなどの複雑なジェスチャーを扱うのに最適です。
特徴
- 柔軟性
複数のコンポーネントがジェスチャーに関与する場合や、異なるジェスチャー(例: タップとドラッグ)を区別する場合に特に便利です。 - レスポンダーシステムの抽象化
onStartShouldSetResponder
やonMoveShouldSetResponder
といった低レベルなレスポンダープロパティを気にすることなく、ジェスチャーを定義できます。 - ジェスチャー状態 (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-flatlist
やreact-native-sortable-list
など、ドラッグ&ドロップでアイテムを並べ替えられるリストコンポーネントがあります。これらは内部でジェスチャーハンドリングを処理してくれます。 - スライダー
react-native-slider
など、既にドラッグ機能が組み込まれたスライダーコンポーネントが多数存在します。
- 特定のユースケースに最適化された機能が提供されます。
- テスト済みの安定したコードベースを利用できます。
- 開発時間を大幅に短縮できます。
方法 | 特徴 | 最適なケース |
---|---|---|
Text#onResponderMove | 低レベルのイベントハンドラ | シンプルな単一のドラッグ、カスタムジェスチャーの試作 |
PanResponder | React Native 内蔵、ジェスチャー状態を追跡、Animated と統合可能 | 中程度の複雑さのドラッグ、スワイプ、ピンチジェスチャー |
react-native-gesture-handler | ネイティブベース、高性能、豊富なジェスチャー | 複雑なジェスチャー、高いパフォーマンスが求められる場合、プロダクションアプリ |
特定のライブラリ | 事前に構築された特定の機能 | スライダー、ソート可能なリスト、画像ズームなど、特定のUIパターン |