-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Logic Detector #3689
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
Logic Detector #3689
Conversation
5b65a19 to
81bf405
Compare
j-piasecki
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did a fist pass, but some stuff will likely change due to #3682 :/
Anyway, you have the hardest part figured out, now it's just a matter of polishing it up.
...ler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorView.kt
Outdated
Show resolved
Hide resolved
...roid/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorViewManager.kt
Outdated
Show resolved
Hide resolved
packages/react-native-gesture-handler/src/specs/RNGestureHandlerDetectorNativeComponent.ts
Outdated
Show resolved
Hide resolved
packages/react-native-gesture-handler/apple/RNGestureHandlerNativeEventUtils.h
Outdated
Show resolved
Hide resolved
packages/react-native-gesture-handler/apple/RNGestureHandlerManager.mm
Outdated
Show resolved
Hide resolved
packages/react-native-gesture-handler/src/v3/HostGestureDetector.web.tsx
Outdated
Show resolved
Hide resolved
packages/react-native-gesture-handler/src/v3/LogicDetector.web.tsx
Outdated
Show resolved
Hide resolved
94984b2 to
4e718b3
Compare
...ler/android/src/main/java/com/swmansion/gesturehandler/react/events/RNGestureHandlerEvent.kt
Show resolved
Hide resolved
packages/react-native-gesture-handler/apple/RNGestureHandlerManager.mm
Outdated
Show resolved
Hide resolved
packages/react-native-gesture-handler/src/v3/NativeDetector/HostGestureDetector.web.tsx
Outdated
Show resolved
Hide resolved
packages/react-native-gesture-handler/src/v3/NativeDetector/HostGestureDetector.web.tsx
Outdated
Show resolved
Hide resolved
I'll be on vacation and I don't want to block this PR - I believe it will be good to go when @j-piasecki approves it :D
| attachedHandlers.current = attachedHandlers.current.difference( | ||
| attachedNativeHandlers.current | ||
| ); | ||
| detachHandlers(attachedHandlers.current, attachedNativeHandlers.current); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we doing that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have to reattach all native handlers when child changes, it might be obsolete after #3714 is merged
|
Could you also check if these changes have an impact on render time compared to the |
| ); | ||
| const reanimatedTouchEventHandler = Reanimated?.useComposedEventHandler( | ||
| getHandlers('onReanimatedTouchEvent') | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For some reason, the reanimated handlers don't work on web. I spend too much time trying to fix it, I'd appreciate any ideas why is that the case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible that invokeNullableMethod isn't able to trigger whatever is returned by useComposedEventHandler?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really, as the gesture asigned to NativeDetector seems to work, which also goes through this path. Only the gestures from LogicDetector don't work. I think there is an issue with updating the handlers, which I struggle to solve as useComposedEventHandler is a hook thus I can't simply rerun it in register. Also as @m-bert noticed, similar issue occurs on android in some cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For some reason it works on web, once reanimated and worklets are bumped to 4.1. The issue persists for some cases in android
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I checked and rechecked, everything seems to work on all platforms after bumping reanimated to 4.1
I tested, compared this branch with the next branch using DevTools profiler and this base example. Small examples were within margin of error. Difference emerges only for a lot of detectors, here are the result for 3000 detectors on iOS 16 Plus Simulator. I'm comparing load times for EDIT: added a test series with this base example, where each detector has its own gesture |
| const logicProps = | ||
| Platform.OS === 'web' | ||
| ? { | ||
| viewRef, | ||
| viewTag, | ||
| handlerTags: isComposedGesture(props.gesture) | ||
| ? props.gesture.tags | ||
| : [props.gesture.tag], | ||
| } | ||
| : { | ||
| viewTag, | ||
| handlerTags: isComposedGesture(props.gesture) | ||
| ? props.gesture.tags | ||
| : [props.gesture.tag], | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about
const logicProps = {
viewTag,
handlerTags: isComposedGesture(props.gesture)
? props.gesture.tags
: [props.gesture.tag],
};
if (Platform.OS === 'web') {
Object.assign(logicProps, { viewRef });
}?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I like it. Thanks 153385c
j-piasecki
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's good to go 🚢🇮🇹.
Please also open a PR that will separate the logic-related logic to a separate detector component.
We also may want to rethink the names at this point.
## Description To simplify migration the `GestureDetector` will check the type of the gesture it receives and render the detector accordingly it: * Checks if the gesture it received is V2 or V2: * If it's V2, it renders the old `GestureDetector` component * If it's V3, it checks for the `InterceptingGestureDetector` context: * If context is there, it's inside boundary and renders `LogicDetector` * If the context is not there, it renders `NativeDetector` ### Why separate `NativeDetector` and `InterceptingGestureDetector`? We did some [performance tests](#3689 (comment)) on the `NativeDetector` after adding changes that handle attaching `LogicDetector`, which revealed that the new logic adds quite a bit of overhead, but only on the JS side. New logic on the native side does not seem to have a significant effect. We concluded that the best solution is to create a separate component that will have all functionalities of `NativeDetector` and also allow `LogicDetector` attachment, while NativeDetector will be reverted to how it had been before implementing `LogicDetector`. ## Test plan Rendering `LogicDetector`: <details> ```tsx import React from 'react'; import { Text, View, StyleSheet } from 'react-native'; import { GestureDetectorBoundary, GestureDetector, useTap, GestureHandlerRootView } from 'react-native-gesture-handler'; import Svg, { Circle, Rect } from 'react-native-svg'; export default function SvgExample() { const circleElementTap = useTap({ onStart: () => { 'worklet'; console.log('RNGH: clicked circle') }, }); const rectElementTap = useTap({ onStart: () => { 'worklet'; console.log('RNGH: clicked parallelogram') }, }); return ( <GestureHandlerRootView> <View style={styles.container}> <Text style={styles.header}> Overlapping SVGs with gesture detectors </Text> <View style={{ backgroundColor: 'tomato' }}> <GestureDetectorBoundary> <Svg height="250" width="250" onPress={() => console.log('SVG: clicked container')}> <GestureDetector gesture={circleElementTap}> <Circle cx="125" cy="125" r="125" fill="green" onPress={() => console.log('SVG: clicked circle')} /> </GestureDetector> <GestureDetector gesture={rectElementTap}> <Rect skewX="45" width="125" height="250" fill="yellow" onPress={() => console.log('SVG: clicked parallelogram')} /> </GestureDetector> </Svg> </GestureDetectorBoundary> </View> <Text> Tapping each color should read to a different console.log output </Text> </View> </GestureHandlerRootView> ); } const styles = StyleSheet.create({ container: { alignItems: 'center', justifyContent: 'center', marginBottom: 48, }, header: { fontSize: 18, fontWeight: 'bold', margin: 10, }, }); ``` </details Rendering `NativeDetector`: <details> ```tsx import React from 'react'; import { StyleSheet, View } from 'react-native'; import { GestureDetector, useTap, } from 'react-native-gesture-handler'; import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing, interpolateColor, } from 'react-native-reanimated'; export default function SimpleTap() { const colorValue = useSharedValue(0); const tapGesture = useTap({ onStart: () => { 'worklet'; colorValue.value = withTiming(colorValue.value === 0 ? 1 : 0, { duration: 400, easing: Easing.inOut(Easing.ease), }); }, }); const animatedStyle = useAnimatedStyle(() => { const backgroundColor = interpolateColor( colorValue.value, [0, 1], ['#b58df1', '#ff7f50'] // purple → coral ); return { backgroundColor, }; }); return ( <View style={styles.centerView}> <GestureDetector gesture={tapGesture}> <Animated.View style={[styles.box, animatedStyle]} /> </GestureDetector> </View> ); } const styles = StyleSheet.create({ centerView: { flex: 1, justifyContent: 'center', alignItems: 'center', }, box: { height: 120, width: 120, backgroundColor: '#b58df1', marginBottom: 30, borderRadius: 12, }, }); ``` </details Rendering old v2 `GestureDetector`: <details> ```tsx import React from 'react'; import { StyleSheet, View } from 'react-native'; import { Gesture, GestureDetector, } from 'react-native-gesture-handler'; import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing, interpolateColor, } from 'react-native-reanimated'; export default function SimpleTap() { const colorValue = useSharedValue(0); const tapGesture = Gesture.Tap() .onStart(() => { 'worklet'; colorValue.value = withTiming(colorValue.value === 0 ? 1 : 0, { duration: 400, easing: Easing.inOut(Easing.ease), }); }); const animatedStyle = useAnimatedStyle(() => { const backgroundColor = interpolateColor( colorValue.value, [0, 1], ['#b58df1', '#ff7f50'] // purple → coral ); return { backgroundColor, }; }); return ( <View style={styles.centerView}> <GestureDetector gesture={tapGesture}> <Animated.View style={[styles.box, animatedStyle]} /> </GestureDetector> </View> ); } const styles = StyleSheet.create({ centerView: { flex: 1, justifyContent: 'center', alignItems: 'center', }, box: { height: 120, width: 120, backgroundColor: '#b58df1', marginBottom: 30, borderRadius: 12, }, }); ``` </details
## Description This PR makes `VirtualDetector` compatible with native gestures. Back in #3689 I decided to handle `VirtualDetector` compatibility with native gestures for later, as it is a extremely niche feature and it seemed to require a lot of work. Turns out it doesn't, beacause we use wrap in VirtualDetector we can handle them as normal gestures. ## Test plan Tested on: <details> ```tsx import * as React from 'react'; import { StyleSheet, Text, View, ScrollView, Button } from 'react-native'; import { GestureHandlerRootView, GestureDetector, useNative, useTap, InterceptingGestureDetector } from 'react-native-gesture-handler'; export default function App() { const items = Array.from({ length: 100 }, (_, index) => `Item ${index + 1}`); const [enabled, setEnabled] = React.useState(true); const gesture = useNative({ onStart: (e) => { 'worklet'; console.log('onStart'); } }); const outerGesture = useTap({ onStart: (e) => { 'worklet' console.log('onOuterStart'); } }); const SV1 = () => ( <ScrollView style={styles.scrollView1}> {items.map((item, index) => ( <View key={index} style={styles.item}> <Text style={styles.text}>{item}</Text> </View> ))} </ScrollView> ); const SV2 = () => ( <ScrollView style={styles.scrollView2}> {items.map((item, index) => ( <View key={index} style={styles.item}> <Text style={styles.text}>{item}</Text> </View> ))} </ScrollView> ); return ( <GestureHandlerRootView style={styles.root}> <View style={styles.buttonContainer}> <Button title="Swap the child" onPress={() => setEnabled(!enabled)} color="#4a90e2" /> </View> <InterceptingGestureDetector gesture={outerGesture}> <View style={styles.outerContainer}> <View style={styles.frame}> <GestureDetector gesture={gesture}> {enabled ? <SV1 /> : <SV2 />} </GestureDetector> </View> </View> </InterceptingGestureDetector> </GestureHandlerRootView> ); } const styles = StyleSheet.create({ scrollView1: { backgroundColor: 'pink', marginHorizontal: 20, }, scrollView2: { backgroundColor: 'lightblue', marginHorizontal: 20, }, item: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', padding: 20, margin: 2, backgroundColor: 'white', borderRadius: 10, }, text: { fontSize: 20, color: 'black', }, root: { flex: 1, backgroundColor: '#fafafa', paddingTop: 60, alignItems: 'center', }, buttonContainer: { marginBottom: 20, width: '80%', }, outerContainer: { padding: 14, backgroundColor: '#fff', borderRadius: 18, borderWidth: 1, borderColor: '#e0e0e0', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.12, shadowRadius: 8, elevation: 5, }, frame: { borderRadius: 14, borderWidth: 1, borderColor: '#d6d6d6', backgroundColor: '#fdfdfd', overflow: 'hidden', }, innerContainer: { paddingVertical: 24, paddingHorizontal: 20, borderRadius: 12, alignItems: 'center', justifyContent: 'center', }, active: { backgroundColor: '#e9f7ef', borderColor: '#4caf50', borderWidth: 1.5, }, inactive: { backgroundColor: '#fff8e1', borderColor: '#ffb300', borderWidth: 1, }, }); ``` </details>
Description
This PR implements new component ->
LogicDetector. It resolves the issue of attaching gestures to inner SVG components.LogicDetectorcommunicates with aNativeDetectorhigher in the hierarchy, which will be responsible for attaching gestures.Note: attaching
Nativegestures toLogicDetectorwill be a added in a follow up, as it is a niche feature - thus not a priority - and we don't want to block this PR.Note 2: Reanimated handlers currently only work on web for reanimated: ^4.1
Test plan
tested on the following code