Text#onResponderTerminationRequest
詳しく説明します。
ジェスチャーレスポンダーシステムとは?
React Nativeでは、ユーザーのタップ、スワイプ、ドラッグといった様々なタッチイベントをどのように処理するかを管理するために「ジェスチャーレスポンダーシステム」が導入されています。これは、どのコンポーネントが現在タッチイベントの「レスポンダー」(応答者)であるかを決定し、そのライフサイクルを管理する仕組みです。
onResponderTerminationRequest
の役割
ある View
(または Text
などのTouchableなコンポーネント)が現在タッチイベントのレスポンダーであるとします。つまり、ユーザーがそのコンポーネントを触っていて、そのコンポーネントがそのタッチイベントを処理している状態です。
このとき、他のコンポーネント(例えば、親の ScrollView
や別のTouchableな要素など)が「私にレスポンダーの役割を譲ってほしい」と要求してくることがあります。
onResponderTerminationRequest
は、現在のレスポンダーであるコンポーネントに対して、「レスポンダーの役割を終了して、他のコンポーネントに明け渡しても良いか?」と尋ねるコールバック関数です。
- この関数が
false
を返すと、現在のコンポーネントはレスポンダーの役割を保持し続け、他のコンポーネントに明け渡しません。 - この関数が
true
を返すと、現在のコンポーネントはレスポンダーの役割を解放し、他のコンポーネントがレスポンダーになることを許可します。
具体的な使用例とシナリオ
例えば、ScrollView
の中に Text
コンポーネントがあり、その Text
コンポーネントに onPress
などのタッチイベントハンドラーが設定されている場合を考えます。
- ユーザーが
Text
をタップし始め、Text
がレスポンダーになります。 - ユーザーが指を動かし、
ScrollView
がスクロールしたいと判断します。 ScrollView
はText
に対してonResponderTerminationRequest
を呼び出します。-
- もし
Text
がonResponderTerminationRequest
でtrue
を返すと、Text
はレスポンダーを終了し、ScrollView
がスクロールを開始します。 - もし
Text
がonResponderTerminationRequest
でfalse
を返すと、Text
はレスポンダーを保持し、ScrollView
はスクロールを開始できません(これは一般的にはユーザー体験として望ましくない場合が多いです)。
- もし
Text
コンポーネントはデフォルトで onPress
などのタッチハンドラーを持つことができます。onResponderTerminationRequest
は、Text
がタッチイベントを処理している最中に、他の要素がタッチイベントの制御を要求してきた場合に、その要求を受け入れるかどうかを制御するのに役立ちます。
よくあるエラーと問題点
-
- 問題
ScrollView
内のText
コンポーネントがタッチイベントを処理している場合(例えばonPress
が設定されている場合)、ユーザーがText
の上で指を動かしてもScrollView
がスクロールを開始しないことがあります。これは、Text
がレスポンダーの役割を手放さないために起こります。 - 原因
Text
のonResponderTerminationRequest
がfalse
を返しているか、デフォルトの挙動が期待通りでないため、親のScrollView
がレスポンダーになることを許可していない。
- 問題
-
ボタンやインタラクティブ要素のタップが効かない
- 問題
複数のタッチ可能なコンポーネントが重なっている場合(または隣接しているがタッチ領域が重複している場合)に、一部のコンポーネントのタップイベントが発火しないことがあります。 - 原因
あるコンポーネントがレスポンダーの役割を保持し続けてしまい、他のコンポーネントがレスポンダーになる機会を得られないため。特に、onResponderTerminationRequest
がfalse
を返しているコンポーネントがある場合に顕著です。
- 問題
-
複雑なジェスチャーが意図通りに動作しない
- 問題
ピンチズームやスワイプなどの複雑なジェスチャーを実装しようとしたときに、一部のジェスチャーが他のコンポーネントによって「奪われて」しまい、予期しない動作になることがあります。 - 原因
ジェスチャーハンドリングライブラリ(例:react-native-gesture-handler
)を使用している場合でも、基盤となるジェスチャーレスポンダーシステムとの相互作用が正しく設定されていないため。onResponderTerminationRequest
の挙動が、ジェスチャーハンドラーの意図と異なる場合に発生します。
- 問題
-
デバッグが難しい
- 問題
どのコンポーネントが現在レスポンダーになっているのか、なぜレスポンダーが切り替わらないのかを特定するのが難しい。 - 原因
ジェスチャーレスポンダーシステムは内部的な仕組みであり、その状態を直接視覚的に確認するツールが限られているため。
- 問題
-
onResponderTerminationRequest の返り値を確認する
- ほとんどの場合、
onResponderTerminationRequest
はtrue
を返すようにしておくと、他のコンポーネントがレスポンダーになることを許可するため、柔軟なインタラクションが可能です。 - 明示的に
false
を返している箇所がないか、コードを確認してください。特定の理由がない限り、false
を返すのは避けるべきです。 - 例
<Text onPress={() => console.log('Text pressed')} onResponderTerminationRequest={() => true} // これがデフォルトで、ほとんどの場合問題ない > タップ可能なテキスト </Text>
- ほとんどの場合、
-
タッチイベントのライフサイクルを理解する
onStartShouldSetResponder
やonMoveShouldSetResponder
など、レスポンダーになるための他のプロパティとの関連性を理解することが重要です。onResponderTerminationRequest
は、既にレスポンダーになっているコンポーネントが、その役割を終了して良いかを問われた際に呼び出されるという点を再確認してください。
-
console.log を活用してデバッグする
- 各タッチイベントハンドラー(
onPressIn
,onPressOut
,onResponderGrant
,onResponderRelease
,onResponderTerminationRequest
など)の中にconsole.log
を仕込み、イベントの発生順序やonResponderTerminationRequest
の返り値を確認します。 - これにより、どのコンポーネントがいつレスポンダーになり、いつその役割を放棄しようとしているのかを把握できます。
- 各タッチイベントハンドラー(
-
コンポーネントの階層とz-indexを確認する
- 重なっているコンポーネントがある場合、z-indexや描画順がタッチイベントの処理に影響を与えることがあります。
onResponderTerminationRequest
は直接的な原因ではないかもしれませんが、コンポーネントの配置が原因で予期せぬタッチの奪い合いが起きている可能性も考慮します。
- 重なっているコンポーネントがある場合、z-indexや描画順がタッチイベントの処理に影響を与えることがあります。
-
react-native-gesture-handler の利用を検討する
- 複雑なジェスチャー(ピンチ、スワイプ、パンなど)を扱う場合や、コンポーネント間のタッチイベントの競合をより細かく制御したい場合は、
react-native-gesture-handler
ライブラリの導入を強く推奨します。 - このライブラリは、React Nativeのネイティブなジェスチャーシステムをより高度に抽象化し、競合解決の仕組みを提供します。
onResponderTerminationRequest
のような低レベルなプロパティを直接操作するよりも、このライブラリが提供するコンポーネント(PanGestureHandler
など)を使う方が、多くのケースで問題が解決しやすくなります。
- 複雑なジェスチャー(ピンチ、スワイプ、パンなど)を扱う場合や、コンポーネント間のタッチイベントの競合をより細かく制御したい場合は、
-
簡素化されたテストケースを作成する
- 問題が発生している画面全体ではなく、問題の原因となっていると思われるコンポーネントとその周辺の最小限のコードで新しいコンポーネントを作成し、挙動を再現してみてください。これにより、問題を切り分けやすくなります。
具体的なプログラミング例を通じて、その挙動を理解しましょう。
基本的な例:Text
がレスポンダーの役割を譲る場合(デフォルトの挙動)
ほとんどの場合、onResponderTerminationRequest
はtrue
を返すように設定されており、これは他のコンポーネント(特に親のScrollView
など)がタッチイベントの制御を求めたときに、現在のText
コンポーネントがそれを快く譲ることを意味します。
import React, { useState } from 'react';
import { View, Text, ScrollView, StyleSheet } from 'react-native';
const TerminationRequestExample = () => {
const [log, setLog] = useState([]);
const addLog = (message) => {
setLog(prevLog => [...prevLog, message]);
};
return (
<View style={styles.container}>
<ScrollView style={styles.scrollView}>
<Text style={styles.header}>スクロール可能な領域</Text>
<Text
style={styles.touchableText}
onPressIn={() => addLog('Text: onPressIn (レスポンダーになることを要求)')}
onResponderGrant={() => addLog('Text: onResponderGrant (レスポンダーになりました)')}
onResponderRelease={() => addLog('Text: onResponderRelease (タップ終了)')}
// デフォルトの挙動: trueを返す
onResponderTerminationRequest={() => {
addLog('Text: onResponderTerminationRequest (他の要素がレスポンダーを要求)');
return true; // レスポンダーを譲ることを許可
}}
onResponderTerminate={() => addLog('Text: onResponderTerminate (レスポンダーが奪われました)')}
>
このテキストをタップして指を上下に動かしてください。
通常、ScrollViewがスクロールを開始します。
{'\n\n'}
長いテキストを置いて、スクロールとタップの競合を試します。
長いテキストを置いて、スクロールとタップの競合を試します。
長いテキストを置いて、スクロールとタップの競合を試します。
長いテキストを置いて、スクロールとタップの競合を試します。
長いテキストを置いて、スクロールとタップの競合を試します。
長いテキストを置いて、スクロールとタップの競合を試します。
長いテキストを置いて、スクロールとタップの競合を試します。
長いテキストを置いて、スクロールとタップの競合を試します。
長いテキストを置いて、スクロールとタップの競合を試します。
長いテキストを置いて、スクロールとタップの競合を試します。
長いテキストを置いて、スクロールとタップの競合を試します。
</Text>
{Array.from({ length: 20 }).map((_, i) => (
<Text key={i} style={styles.fillerText}>
追加のスクロール可能なコンテンツ {i + 1}
</Text>
))}
</ScrollView>
<View style={styles.logContainer}>
<Text style={styles.logHeader}>イベントログ:</Text>
<ScrollView style={styles.logScrollView}>
{log.map((entry, index) => (
<Text key={index} style={styles.logText}>{entry}</Text>
))}
</ScrollView>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 50,
},
scrollView: {
flex: 1,
backgroundColor: '#f0f0f0',
padding: 10,
},
header: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10,
},
touchableText: {
fontSize: 16,
padding: 15,
backgroundColor: '#e0ffe0',
borderWidth: 1,
borderColor: 'green',
marginBottom: 20,
},
fillerText: {
fontSize: 14,
padding: 5,
marginVertical: 2,
backgroundColor: '#ffffff',
borderBottomWidth: 1,
borderBottomColor: '#cccccc',
},
logContainer: {
height: 200,
backgroundColor: '#333',
padding: 10,
},
logHeader: {
fontSize: 16,
fontWeight: 'bold',
color: '#fff',
marginBottom: 5,
},
logScrollView: {
flex: 1,
},
logText: {
fontSize: 12,
color: '#eee',
},
});
export default TerminationRequestExample;
解説
- touchableText をタップ
onPressIn
が発火し、Text
がレスポンダーになることを要求します。onResponderGrant
が発火し、Text
がレスポンダーになります。
- 指を上下にドラッグ
ScrollView
がスクロールの意思を検知し、Text
コンポーネントに「レスポンダーを譲ってほしい」と要求します。Text
のonResponderTerminationRequest
が呼び出されます。この例ではtrue
を返すため、Text
はレスポンダーの役割を解放します。onResponderTerminate
が発火し、Text
がレスポンダーではなくなったことを示します。ScrollView
がレスポンダーになり、スクロールが開始されます。
ここでは、onResponderTerminationRequest
が false
を返すように設定し、Text
がレスポンダーの役割を他のコンポーネント(ScrollView
)に譲ることを拒否するケースを示します。
import React, { useState } from 'react';
import { View, Text, ScrollView, StyleSheet, Alert } from 'react-native';
const RejectTerminationExample = () => {
const [log, setLog] = useState([]);
const addLog = (message) => {
setLog(prevLog => [...prevLog, message]);
};
return (
<View style={styles.container}>
<ScrollView style={styles.scrollView}>
<Text style={styles.header}>レスポンダー拒否の例</Text>
<Text
style={[styles.touchableText, { backgroundColor: '#ffffe0', borderColor: 'orange' }]}
onPressIn={() => addLog('Text: onPressIn')}
onResponderGrant={() => addLog('Text: onResponderGrant (レスポンダーになりました)')}
onResponderRelease={() => addLog('Text: onResponderRelease (タップ終了)')}
// !!! レスポンダーを譲ることを拒否する !!!
onResponderTerminationRequest={() => {
addLog('Text: onResponderTerminationRequest (他の要素がレスポンダーを要求) -> falseを返します');
Alert.alert('拒否', 'このテキストがレスポンダーを保持しています。');
return false; // レスポンダーを譲ることを拒否
}}
onResponderTerminate={() => addLog('Text: onResponderTerminate (レスポンダーが奪われました - これは呼ばれません)')}
>
このテキストをタップして指を上下に動かしてください。
ScrollViewはスクロールを開始しないはずです。
{'\n\n'}
長いテキストを置いて、スクロールとタップの競合を試します。
長いテキストを置いて、スクロールとタップの競合を試します。
長いテキストを置いて、スクロールとタップの競合を試します。
長いテキストを置いて、スクロールとタップの競合を試します。
長いテキストを置いて、スクロールとタップの競合を試します。
長いテキストを置いて、スクロールとタップの競合を試します。
長いテキストを置いて、スクロールとタップの競合を試します。
長いテキストを置いて、スクロールとタップの競合を試します。
長いテキストを置いて、スクロールとタップの競合を試します。
長いテキストを置いて、スクロールとタップの競合を試します。
長いテキストを置いて、スクロールとタップの競合を試します。
</Text>
{Array.from({ length: 20 }).map((_, i) => (
<Text key={i} style={styles.fillerText}>
追加のスクロール可能なコンテンツ {i + 1}
</Text>
))}
</ScrollView>
<View style={styles.logContainer}>
<Text style={styles.logHeader}>イベントログ:</Text>
<ScrollView style={styles.logScrollView}>
{log.map((entry, index) => (
<Text key={index} style={styles.logText}>{entry}</Text>
))}
</ScrollView>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 50,
},
scrollView: {
flex: 1,
backgroundColor: '#f0f0f0',
padding: 10,
},
header: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10,
},
touchableText: {
fontSize: 16,
padding: 15,
backgroundColor: '#e0ffe0',
borderWidth: 1,
borderColor: 'green',
marginBottom: 20,
},
fillerText: {
fontSize: 14,
padding: 5,
marginVertical: 2,
backgroundColor: '#ffffff',
borderBottomWidth: 1,
borderBottomColor: '#cccccc',
},
logContainer: {
height: 200,
backgroundColor: '#333',
padding: 10,
},
logHeader: {
fontSize: 16,
fontWeight: 'bold',
color: '#fff',
marginBottom: 5,
},
logScrollView: {
flex: 1,
},
logText: {
fontSize: 12,
color: '#eee',
},
});
export default RejectTerminationExample;
- touchableText をタップ
onPressIn
が発火し、Text
がレスポンダーになることを要求します。onResponderGrant
が発火し、Text
がレスポンダーになります。
- 指を上下にドラッグ
ScrollView
がスクロールの意思を検知し、Text
コンポーネントに「レスポンダーを譲ってほしい」と要求します。Text
のonResponderTerminationRequest
が呼び出されますが、この例ではfalse
を返すため、Text
はレスポンダーの役割を保持し続けます。- 結果として、
ScrollView
はスクロールを開始できず、onResponderTerminate
は呼び出されません(レスポンダーが奪われないため)。
- PanResponder の利用
複雑なジェスチャーや複数の要素間でのタッチイベントの競合をより細かく制御したい場合は、PanResponder
APIを検討してください。PanResponder
はonResponderTerminationRequest
を含む、より包括的なジェスチャーレスポンダーのライフサイクルを提供します。 - false を返すのは慎重に
false
を返すことは、そのコンポーネントが「絶対にタッチイベントの制御を譲らない」ことを意味します。これは、特定のドラッグ操作など、コンポーネントがタッチイベントを完全に制御し続ける必要がある場合にのみ使用すべきです。そうしないと、ユーザー体験を損なう可能性があります。 - デフォルトは true
ほとんどのTouchable
コンポーネントやText
コンポーネントは、onResponderTerminationRequest
が内部的にtrue
を返すように設定されています。これにより、ScrollView
などの親コンポーネントがスムーズにタッチイベントを奪うことができます。
onResponderTerminationRequest
の代替方法というよりは、ジェスチャーレスポンダーシステムやタッチイベントの制御をより効果的に行うための、より高レベルな方法や推奨されるアプローチとして以下のものがあります。
Pressable コンポーネント (推奨)
React Native 0.63 から導入された Pressable
は、Touchable
系のコンポーネント(TouchableOpacity
, TouchableHighlight
など)に代わる、より柔軟で高機能なコンポーネントです。onResponderTerminationRequest
のような低レベルなイベントを直接扱う必要なく、より豊かなインタラクションを実現できます。
特徴
- より安定した挙動
ジェスチャーレスポンダーシステムとの連携が最適化されています。 - hitSlop
タップ可能な領域を視覚的なサイズとは別に拡張できます。 - カスタムフィードバック
Touchable
系のコンポーネントのような組み込みのフィードバック(透明度の変化、背景色の変化など)だけでなく、より複雑なアニメーションや視覚効果を自由に実装できます。 - より詳細な状態
pressed
状態をコールバックで受け取れるため、押されている間とそうでない間のスタイルを簡単に切り替えられます。
Text コンポーネントにタッチイベントを付与する代替として
Text
コンポーネント自体には onPress
などの基本的なタッチハンドラーがありますが、より高度なインタラクションが必要な場合は、Text
を Pressable
で囲むのが一般的です。
例
import React, { useState } from 'react';
import { View, Text, Pressable, StyleSheet } from 'react-native';
const PressableExample = () => {
const [timesPressed, setTimesPressed] = useState(0);
return (
<View style={styles.container}>
<Pressable
onPress={() => {
setTimesPressed((current) => current + 1);
}}
// 押されている間のスタイルを動的に変更
style={({ pressed }) => [
styles.pressableText,
{
backgroundColor: pressed ? '#d0f0d0' : '#e0ffe0',
borderColor: pressed ? 'darkgreen' : 'green',
},
]}
>
<Text style={styles.textInsidePressable}>
Pressable なテキストをタップしてください!
</Text>
<Text style={styles.pressCount}>
タップ回数: {timesPressed}
</Text>
</Pressable>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
pressableText: {
padding: 15,
borderRadius: 8,
borderWidth: 1,
alignItems: 'center',
justifyContent: 'center',
},
textInsidePressable: {
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
},
pressCount: {
marginTop: 10,
fontSize: 14,
color: '#666',
},
});
export default PressableExample;
react-native-gesture-handler (複雑なジェスチャーや競合解決に最適)
ピンチ、パン、スワイプ、長押しなど、より複雑なジェスチャーを実装したり、複数のジェスチャーが競合する状況を管理したりする場合、React Nativeに組み込まれているジェスチャーレスポンダーシステムよりも、 ライブラリの使用が強く推奨されます。
特徴
- 豊富なジェスチャータイプ
PanGestureHandler
(ドラッグ),TapGestureHandler
(タップ),LongPressGestureHandler
(長押し),PinchGestureHandler
(ピンチ),RotationGestureHandler
(回転) など、多くのプリセットジェスチャーが用意されています。 - ジェスチャー間の関係性
複数のジェスチャーハンドラー間で優先順位や相互排他を簡単に設定できます(例: 「スクロール中にタップは無効にする」など)。 - 宣言的なAPI
ジェスチャーの定義と競合解決をより宣言的に記述できます。 - ネイティブスレッドでの処理
ジェスチャー処理をJavaScriptスレッドではなくネイティブUIスレッドで行うため、パフォーマンスが向上し、UIがより滑らかに感じられます。
Text をドラッグ可能にする例(PanGestureHandler を使用)
この場合、Text
コンポーネント自体に直接 onResponderTerminationRequest
を設定する代わりに、Text
を PanGestureHandler
でラップし、そのハンドラーのイベントを処理します。
import React, { useRef } from 'react';
import { View, Text, StyleSheet, Animated } from 'react-native';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
const GestureHandlerExample = () => {
const translateX = useRef(new Animated.Value(0)).current;
const translateY = useRef(new Animated.Value(0)).current;
const onGestureEvent = Animated.event(
[{
nativeEvent: {
translationX: translateX,
translationY: translateY,
},
}],
{ useNativeDriver: true }
);
const onHandlerStateChange = ({ nativeEvent }) => {
if (nativeEvent.state === State.END) {
// ジェスチャーが終了したら、現在の位置をオフセットとして保持
translateX.extractOffset();
translateY.extractOffset();
}
};
return (
<View style={styles.container}>
<PanGestureHandler
onGestureEvent={onGestureEvent}
onHandlerStateChange={onHandlerStateChange}
>
<Animated.View
style={[
styles.draggableTextContainer,
{
transform: [
{ translateX: translateX },
{ translateY: translateY },
],
},
]}
>
<Text style={styles.draggableText}>
このテキストをドラッグしてください
</Text>
</Animated.View>
</PanGestureHandler>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
draggableTextContainer: {
padding: 20,
backgroundColor: '#add8e6', // 薄い水色
borderRadius: 10,
borderWidth: 1,
borderColor: 'blue',
},
draggableText: {
fontSize: 20,
fontWeight: 'bold',
color: '#333',
},
});
export default GestureHandlerExample;
解説
この例では、PanGestureHandler
がタッチイベントを直接処理し、Animated.Value
を使用してText
コンポーネントの表示位置をネイティブスレッドで更新しています。onResponderTerminationRequest
のような低レベルなAPIは意識せずに、複雑なドラッグジェスチャーを実装できます。
PanResponder
は、React Nativeに組み込まれているジェスチャーレスポンダーシステムを直接操作するためのAPIです。onResponderTerminationRequest
も含め、タッチイベントのライフサイクル全体を自分で管理したい場合に利用します。これは非常に強力ですが、その分複雑さも増します。
特徴
- 柔軟性
あらゆるカスタムジェスチャーをゼロから作成できます。 - シングルタッチとマルチタッチ
基本的なマルチタッチジェスチャーも自力で実装できます。 - 低レベルな制御
onStartShouldSetResponder
,onMoveShouldSetResponder
,onResponderGrant
,onResponderMove
,onResponderRelease
,onResponderTerminationRequest
,onResponderTerminate
など、レスポンダーの全ライフサイクルイベントをハンドリングできます。
使用場面
- 既存の
Touchable
コンポーネントやScrollView
との競合を厳密に制御したい場合。 react-native-gesture-handler
では対応できない、非常に特殊なジェスチャーロジックが必要な場合。
import React, { useRef } from 'react';
import { View, Text, PanResponder, StyleSheet, Animated } from 'react-native';
const PanResponderExample = () => {
const pan = useRef(new Animated.ValueXY()).current; // ドラッグ位置を保持
const panResponder = useRef(
PanResponder.create({
// ユーザーがタッチを開始したときに、このコンポーネントがレスポンダーになるべきか?
onStartShouldSetPanResponder: () => true,
// ユーザーが指を動かしたときに、このコンポーネントがレスポンダーになるべきか?
onMoveShouldSetPanResponder: () => true,
// レスポンダーになったときに呼び出される
onPanResponderGrant: (evt, gestureState) => {
console.log('PanResponder: レスポンダーになりました');
// 現在のレイアウトオフセットを保持し、アニメーションを開始
pan.setOffset({ x: pan.x._value, y: pan.y._value });
pan.setValue({ x: 0, y: 0 }); // オフセットを0にリセットして、相対的な移動を計算
},
// 指が動いている間呼び出される
onPanResponderMove: Animated.event(
[
null,
{ dx: pan.x, dy: pan.y } // x, y の値を更新
],
{ useNativeDriver: false } // ここでは例としてfalseにしています
),
// 他のコンポーネントがレスポンダーを要求したときに呼び出される
onPanResponderTerminationRequest: (evt, gestureState) => {
console.log('PanResponder: レスポンダー終了要求を受け取りました');
// 必要に応じてtrue/falseを返す
return true; // デフォルトでは譲ることを許可
},
// レスポンダーが他のコンポーネントに奪われたときに呼び出される
onPanResponderTerminate: (evt, gestureState) => {
console.log('PanResponder: レスポンダーが奪われました');
// オフセットをクリア
pan.extractOffset();
},
// タッチが終了したときに呼び出される
onPanResponderRelease: (evt, gestureState) => {
console.log('PanResponder: タッチが終了しました');
// オフセットをクリア
pan.extractOffset();
},
})
).current;
return (
<View style={styles.container}>
<Animated.View
style={{
transform: [{ translateX: pan.x }, { translateY: pan.y }],
...styles.draggableBox,
}}
{...panResponder.panHandlers} // PanResponderのハンドラーを適用
>
<Text style={styles.draggableText}>
PanResponder でドラッグ
</Text>
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
draggableBox: {
width: 150,
height: 100,
backgroundColor: 'lightblue',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 8,
borderWidth: 1,
borderColor: 'blue',
},
draggableText: {
fontSize: 16,
fontWeight: 'bold',
color: '#333',
},
});
export default PanResponderExample;