-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Add cancelsJSResponder prop to the gesture handlers
#4094
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 13 commits
4897ae0
8fd0c11
1f123b1
b939be1
dc9270f
aae857b
986a56a
1197ee7
da47e89
a97742e
fca5a98
f9b2835
ea95543
2e97530
c4b49ec
68c31bb
ca4c6d3
fd57422
fff3181
758cea7
803ee7c
92fecd7
770b410
eea0192
d8ff02f
9f24c36
4d509d3
0c13b44
bc8f207
7795b1a
403a750
3f7c16e
d419464
6eac6c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| import React, { useCallback, useRef, useState } from 'react'; | ||
| import { StyleSheet, Switch, Text, View } from 'react-native'; | ||
| import { GestureDetector, usePanGesture } from 'react-native-gesture-handler'; | ||
| import { | ||
| COLORS, | ||
| Feedback, | ||
| FeedbackHandle, | ||
| commonStyles, | ||
| } from '../../../common'; | ||
|
|
||
| const MAX_EVENTS = 8; | ||
|
|
||
| export default function RNResponderCancellationExample() { | ||
| const feedbackRef = useRef<FeedbackHandle>(null); | ||
| const sequenceRef = useRef(0); | ||
| const [events, setEvents] = useState<string[]>([]); | ||
| const [preventRecognizers, setPreventRecognizers] = useState(true); | ||
|
|
||
| const pushEvent = useCallback((label: string) => { | ||
| sequenceRef.current += 1; | ||
| const event = `${sequenceRef.current}. ${label}`; | ||
|
|
||
| console.log(event); | ||
| feedbackRef.current?.showMessage(label); | ||
| setEvents((prev) => [event, ...prev].slice(0, MAX_EVENTS)); | ||
| }, []); | ||
|
|
||
| const panGesture = usePanGesture({ | ||
| minDistance: 12, | ||
| runOnJS: true, | ||
| preventRecognizers, | ||
| onActivate: () => { | ||
| pushEvent('GH pan ACTIVE'); | ||
| }, | ||
| onFinalize: (_event, success) => { | ||
| pushEvent(`GH pan finalize (${success ? 'success' : 'cancel/fail'})`); | ||
| }, | ||
| }); | ||
|
|
||
| return ( | ||
| <View style={styles.container}> | ||
| <Text style={commonStyles.header}>RN responder cancellation</Text> | ||
| <Text style={commonStyles.instructions}> | ||
| Toggle preventRecognizers and drag inside the box to compare behavior. | ||
| </Text> | ||
| <View style={styles.settingsRow}> | ||
| <Text style={styles.settingsLabel}>preventRecognizers</Text> | ||
| <Switch | ||
| value={preventRecognizers} | ||
| onValueChange={setPreventRecognizers} | ||
| /> | ||
| </View> | ||
|
|
||
| <GestureDetector gesture={panGesture}> | ||
| <View | ||
| style={styles.touchArea} | ||
| onStartShouldSetResponder={() => { | ||
| pushEvent('RN onStartShouldSetResponder -> true'); | ||
| return true; | ||
| }} | ||
| onMoveShouldSetResponder={() => { | ||
| pushEvent('RN onMoveShouldSetResponder -> true'); | ||
| return true; | ||
| }} | ||
| onResponderGrant={() => { | ||
| pushEvent('RN onResponderGrant'); | ||
| }} | ||
| onResponderMove={() => { | ||
| pushEvent('RN onResponderMove'); | ||
| }} | ||
| onResponderRelease={() => { | ||
| pushEvent('RN onResponderRelease'); | ||
| }} | ||
| onResponderTerminate={() => { | ||
| pushEvent('RN onResponderTerminate'); | ||
| }} | ||
| onResponderTerminationRequest={() => { | ||
| pushEvent('RN onResponderTerminationRequest -> true'); | ||
| return true; | ||
| }}> | ||
| <Text style={styles.touchAreaLabel}>Drag me</Text> | ||
| </View> | ||
| </GestureDetector> | ||
|
|
||
| <Feedback ref={feedbackRef} duration={1300} /> | ||
| <View style={styles.logContainer}> | ||
| {events.map((item) => ( | ||
| <Text | ||
| key={item} | ||
| style={[ | ||
| styles.logLine, | ||
| item.includes('GH pan ACTIVE') && styles.logLineActive, | ||
| ]}> | ||
| {item} | ||
| </Text> | ||
| ))} | ||
| </View> | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| container: { | ||
| flex: 1, | ||
| paddingHorizontal: 16, | ||
| paddingVertical: 24, | ||
| gap: 12, | ||
| alignItems: 'center', | ||
| backgroundColor: COLORS.offWhite, | ||
| }, | ||
| touchArea: { | ||
| width: '100%', | ||
| maxWidth: 340, | ||
| minHeight: 220, | ||
| borderRadius: 20, | ||
| borderWidth: 2, | ||
| borderColor: COLORS.NAVY, | ||
| backgroundColor: '#d8ebff', | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| }, | ||
| touchAreaLabel: { | ||
| color: COLORS.NAVY, | ||
| fontWeight: '700', | ||
| fontSize: 18, | ||
| }, | ||
| settingsRow: { | ||
| width: '100%', | ||
| maxWidth: 340, | ||
| flexDirection: 'row', | ||
| alignItems: 'center', | ||
| justifyContent: 'space-between', | ||
| }, | ||
| settingsLabel: { | ||
| color: COLORS.NAVY, | ||
| fontSize: 14, | ||
| fontWeight: '600', | ||
| }, | ||
| logContainer: { | ||
| width: '100%', | ||
| maxWidth: 380, | ||
| minHeight: 170, | ||
| borderRadius: 12, | ||
| padding: 12, | ||
| backgroundColor: '#ffffff', | ||
| borderWidth: 1, | ||
| borderColor: '#d5dbe6', | ||
| gap: 2, | ||
| }, | ||
| logLine: { | ||
| fontSize: 13, | ||
| color: '#2c3a4f', | ||
| fontFamily: 'Courier', | ||
| }, | ||
| logLineActive: { | ||
| color: '#1565c0', | ||
| fontWeight: 'bold', | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,18 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView: | |
| rootView, | ||
| ).apply { | ||
| minimumAlphaForTraversal = MIN_ALPHA_FOR_TOUCH | ||
| onPreventRecognizersRequested = { _ -> | ||
| shouldIntercept = true | ||
|
coado marked this conversation as resolved.
Outdated
|
||
| val time = SystemClock.uptimeMillis() | ||
| val event = MotionEvent.obtain(time, time, MotionEvent.ACTION_CANCEL, 0f, 0f, 0) | ||
| if (rootView is RootView) { | ||
| rootView.onChildStartedNativeGesture(rootView, event) | ||
| } | ||
| event.recycle() | ||
| } | ||
| onPreventRecognizersReleased = { _ -> | ||
| shouldIntercept = false | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need to reset
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the last active handler with
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you check how this behaves with iOS system recognizers? i.e., two simultaneous gestures: A with I think this is the case where this would matter on Android, but it would also allow native components to start handling touch in addition to the JS responder. Ideally, both platforms would be aligned on this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The JS responder doesn't work when the A is activated and starts working when A deactivates (regardless if B is activated or not). |
||
| } | ||
| } | ||
| jsGestureHandler = RootViewGestureHandler(handlerTag = -wrappedViewTag) | ||
| registry.registerHandler(jsGestureHandler) | ||
|
|
@@ -92,18 +104,6 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView: | |
| override fun onHandle(event: MotionEvent, sourceEvent: MotionEvent) = handleEvent(event) | ||
|
|
||
| override fun onHandleHover(event: MotionEvent, sourceEvent: MotionEvent) = handleEvent(event) | ||
|
|
||
| override fun onCancel() { | ||
| shouldIntercept = true | ||
| val time = SystemClock.uptimeMillis() | ||
| val event = MotionEvent.obtain(time, time, MotionEvent.ACTION_CANCEL, 0f, 0f, 0).apply { | ||
| action = MotionEvent.ACTION_CANCEL | ||
| } | ||
| if (rootView is RootView) { | ||
| rootView.onChildStartedNativeGesture(rootView, event) | ||
| } | ||
| event.recycle() | ||
| } | ||
| } | ||
|
|
||
| fun requestDisallowInterceptTouchEvent() { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.