-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
[iOS] Gestures don't work on child transformed out of the parent bounds when it originally fit inside them #6832
Labels
Maintainer issue
Issue created by a maintainer
Comments
I always knew Gesture Handler wouldn't do anything like this. Adding Case closed. |
I've been informed I can transfer issues between repos 🤯 |
Where you able to solve this @j-piasecki ? |
Unfortunately, I didn't have time to look into this 😞 |
This comment has been minimized.
This comment has been minimized.
Should be fixed with #7014. |
github-merge-queue bot
pushed a commit
that referenced
this issue
Feb 25, 2025
…ePropsOnUIThread` (#7014) ## Motivation Currently, there are two ways to update native view props and styles in Reanimated. The default path (so-called slow path) is to apply all props changes to the ShadowTree via C++ API and let React Native mount the changes. However, if all props updated in given batch are non-layout props (i.e. those that don't require layout recalculation, like background color or opacity) we use a fast path that calls `synchronouslyUpdatePropsOnUIThread` from React Native and applies the changes directly to platform views, without making changes to ShadowTree in C++. Turns out, some features like view measurement or touch detection system use C++ ShadowTree which is not consistent with what's currently on the screen. Because of that, we're removing the fast path (turns out it's not that fast, especially on iOS) to restore the correctness of view measurement and touch detection for animated components. ## Benchmarks * Performance monitor example → Bokeh Example * Android emulator / iPhone 14 Pro real device * Debug mode * Animating `transform` prop using `useAnimatedStyle` | Platform | Before (main) | After (this PR) | |:-:|:-:|:-:| | Android (count=200) | 20 fps | 15 fps | | iOS (count=500) | 22 fps | 22 fps | <details> <summary>App.tsx</summary> ```tsx import React, { useState } from 'react'; import { Dimensions, StyleSheet, View } from 'react-native'; import Animated, { Easing, useAnimatedStyle, useReducedMotion, useSharedValue, withTiming, } from 'react-native-reanimated'; const dimensions = Dimensions.get('window'); function randBetween(min: number, max: number) { return min + Math.random() * (max - min); } function Circle() { const shouldReduceMotion = useReducedMotion(); const [power] = useState(randBetween(0, 1)); const [duration] = useState(randBetween(2000, 3000)); const size = 100 + power * 250; const width = size; const height = size; const hue = randBetween(100, 200); const backgroundColor = `hsl(${hue},100%,50%)`; const opacity = 0.1 + (1 - power) * 0.1; const config = { duration, easing: Easing.linear }; const left = useSharedValue(randBetween(0, dimensions.width) - size / 2); const top = useSharedValue(randBetween(0, dimensions.height) - size / 2); const update = () => { left.value = withTiming(left.value + randBetween(-100, 100), config); top.value = withTiming(top.value + randBetween(-100, 100), config); }; React.useEffect(() => { update(); if (shouldReduceMotion) { return; } const id = setInterval(update, duration); return () => clearInterval(id); }); const animatedStyle = useAnimatedStyle( () => ({ transform: [{ translateX: left.value }, { translateY: top.value }], }), [] ); return ( <Animated.View style={[ styles.circle, { width, height, backgroundColor, opacity }, animatedStyle, ]} /> ); } interface BokehProps { count: number; } function Bokeh({ count }: BokehProps) { return ( <> {[...Array(count)].map((_, i) => ( <Circle key={i} /> ))} </> ); } export default function App() { return ( <View style={styles.container}> <Bokeh count={200} /> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: 'black', overflow: 'hidden', }, circle: { position: 'absolute', borderRadius: 999, }, }); ``` </details> ## Summary * Fixes #6832. * Fixes #6676. ## Test plan
tomekzaw
added a commit
that referenced
this issue
Mar 27, 2025
…ePropsOnUIThread` (#7014) Currently, there are two ways to update native view props and styles in Reanimated. The default path (so-called slow path) is to apply all props changes to the ShadowTree via C++ API and let React Native mount the changes. However, if all props updated in given batch are non-layout props (i.e. those that don't require layout recalculation, like background color or opacity) we use a fast path that calls `synchronouslyUpdatePropsOnUIThread` from React Native and applies the changes directly to platform views, without making changes to ShadowTree in C++. Turns out, some features like view measurement or touch detection system use C++ ShadowTree which is not consistent with what's currently on the screen. Because of that, we're removing the fast path (turns out it's not that fast, especially on iOS) to restore the correctness of view measurement and touch detection for animated components. * Performance monitor example → Bokeh Example * Android emulator / iPhone 14 Pro real device * Debug mode * Animating `transform` prop using `useAnimatedStyle` | Platform | Before (main) | After (this PR) | |:-:|:-:|:-:| | Android (count=200) | 20 fps | 15 fps | | iOS (count=500) | 22 fps | 22 fps | <details> <summary>App.tsx</summary> ```tsx import React, { useState } from 'react'; import { Dimensions, StyleSheet, View } from 'react-native'; import Animated, { Easing, useAnimatedStyle, useReducedMotion, useSharedValue, withTiming, } from 'react-native-reanimated'; const dimensions = Dimensions.get('window'); function randBetween(min: number, max: number) { return min + Math.random() * (max - min); } function Circle() { const shouldReduceMotion = useReducedMotion(); const [power] = useState(randBetween(0, 1)); const [duration] = useState(randBetween(2000, 3000)); const size = 100 + power * 250; const width = size; const height = size; const hue = randBetween(100, 200); const backgroundColor = `hsl(${hue},100%,50%)`; const opacity = 0.1 + (1 - power) * 0.1; const config = { duration, easing: Easing.linear }; const left = useSharedValue(randBetween(0, dimensions.width) - size / 2); const top = useSharedValue(randBetween(0, dimensions.height) - size / 2); const update = () => { left.value = withTiming(left.value + randBetween(-100, 100), config); top.value = withTiming(top.value + randBetween(-100, 100), config); }; React.useEffect(() => { update(); if (shouldReduceMotion) { return; } const id = setInterval(update, duration); return () => clearInterval(id); }); const animatedStyle = useAnimatedStyle( () => ({ transform: [{ translateX: left.value }, { translateY: top.value }], }), [] ); return ( <Animated.View style={[ styles.circle, { width, height, backgroundColor, opacity }, animatedStyle, ]} /> ); } interface BokehProps { count: number; } function Bokeh({ count }: BokehProps) { return ( <> {[...Array(count)].map((_, i) => ( <Circle key={i} /> ))} </> ); } export default function App() { return ( <View style={styles.container}> <Bokeh count={200} /> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: 'black', overflow: 'hidden', }, circle: { position: 'absolute', borderRadius: 999, }, }); ``` </details> * Fixes #6832. * Fixes #6676.
tomekzaw
added a commit
that referenced
this issue
Mar 28, 2025
…ePropsOnUIThread` (#7014) Currently, there are two ways to update native view props and styles in Reanimated. The default path (so-called slow path) is to apply all props changes to the ShadowTree via C++ API and let React Native mount the changes. However, if all props updated in given batch are non-layout props (i.e. those that don't require layout recalculation, like background color or opacity) we use a fast path that calls `synchronouslyUpdatePropsOnUIThread` from React Native and applies the changes directly to platform views, without making changes to ShadowTree in C++. Turns out, some features like view measurement or touch detection system use C++ ShadowTree which is not consistent with what's currently on the screen. Because of that, we're removing the fast path (turns out it's not that fast, especially on iOS) to restore the correctness of view measurement and touch detection for animated components. * Performance monitor example → Bokeh Example * Android emulator / iPhone 14 Pro real device * Debug mode * Animating `transform` prop using `useAnimatedStyle` | Platform | Before (main) | After (this PR) | |:-:|:-:|:-:| | Android (count=200) | 20 fps | 15 fps | | iOS (count=500) | 22 fps | 22 fps | <details> <summary>App.tsx</summary> ```tsx import React, { useState } from 'react'; import { Dimensions, StyleSheet, View } from 'react-native'; import Animated, { Easing, useAnimatedStyle, useReducedMotion, useSharedValue, withTiming, } from 'react-native-reanimated'; const dimensions = Dimensions.get('window'); function randBetween(min: number, max: number) { return min + Math.random() * (max - min); } function Circle() { const shouldReduceMotion = useReducedMotion(); const [power] = useState(randBetween(0, 1)); const [duration] = useState(randBetween(2000, 3000)); const size = 100 + power * 250; const width = size; const height = size; const hue = randBetween(100, 200); const backgroundColor = `hsl(${hue},100%,50%)`; const opacity = 0.1 + (1 - power) * 0.1; const config = { duration, easing: Easing.linear }; const left = useSharedValue(randBetween(0, dimensions.width) - size / 2); const top = useSharedValue(randBetween(0, dimensions.height) - size / 2); const update = () => { left.value = withTiming(left.value + randBetween(-100, 100), config); top.value = withTiming(top.value + randBetween(-100, 100), config); }; React.useEffect(() => { update(); if (shouldReduceMotion) { return; } const id = setInterval(update, duration); return () => clearInterval(id); }); const animatedStyle = useAnimatedStyle( () => ({ transform: [{ translateX: left.value }, { translateY: top.value }], }), [] ); return ( <Animated.View style={[ styles.circle, { width, height, backgroundColor, opacity }, animatedStyle, ]} /> ); } interface BokehProps { count: number; } function Bokeh({ count }: BokehProps) { return ( <> {[...Array(count)].map((_, i) => ( <Circle key={i} /> ))} </> ); } export default function App() { return ( <View style={styles.container}> <Bokeh count={200} /> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: 'black', overflow: 'hidden', }, circle: { position: 'absolute', borderRadius: 999, }, }); ``` </details> * Fixes #6832. * Fixes #6676.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Description
Reanimated not committing non-layout changes to the shadow tree may break gestures on components outside parents (among other things). Here: https://github.com/facebook/react-native/blob/b8f3f919cc9ebbd086d9ac79c93fffd532c55b09/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm#L620 the insets wouldn't be updated since layout will be skipped.
Content from the original RNGH issue:
Description
When gesture detector is attached to a view A which has a child B which may be transformed, if the child starts fully inside the bounds of its parent, the gestures will not work on the child when it's moved outside the bounds. When the child doesn't fit inside the parent's bounds, the gesture will work on it when moved further away.
Child fits fully inside parent's bounds:
Screen.Recording.2024-12-18.at.13.37.00.mov
Child extends out of the parent's bounds
Screen.Recording.2024-12-18.at.13.37.18.mov
Steps to reproduce
See the videos, Reanimated used in repro:
3.16.1
Snack or a link to a repository
https://gist.github.com/j-piasecki/102a4d821059ae2bb4313207425d09f1
Gesture Handler version
2.20.2
React Native version
0.76.5
Platforms
iOS
JavaScript runtime
Hermes
Workflow
Expo bare workflow
Architecture
Fabric (New Architecture)
Build type
Debug mode
Device
iOS simulator
Device model
No response
Acknowledgements
Yes
The text was updated successfully, but these errors were encountered: