react-native-gesture-handler
react-native-gesture-handler copied to clipboard
Fix `GestureDetector` not working when the underlying view changes
Description
GestureDetector was not reattaching gestures if the underlying view has changed, which was especially noticeable when using layout animations.
This PR updates GestureDetector to keep track of the tag of the view it's attached to and to reattach gestures it the tag changes.
The second commit also fixes gestures not reattaching when manually changing the underlying view (at the expense of forcing another render), but only when Reanimated is not used. Applying the following patch:
Expand
diff --git a/node_modules/react-native-reanimated/src/createAnimatedComponent.tsx b/node_modules/react-native-reanimated/src/createAnimatedComponent.tsx
index 1cf0c3f..3f22437 100644
--- a/node_modules/react-native-reanimated/src/createAnimatedComponent.tsx
+++ b/node_modules/react-native-reanimated/src/createAnimatedComponent.tsx
@@ -294,19 +294,12 @@ export default function createAnimatedComponent(
const node = this._getEventViewRef();
const attached = new Set();
const nextEvts = new Set();
- let viewTag: number | undefined;
+ let viewTag: number | undefined = RNRenderer.findHostInstance_DEPRECATED(this)._nativeTag;
for (const key in this.props) {
const prop = this.props[key];
if (prop instanceof AnimatedEvent) {
nextEvts.add((prop as AnimatedEvent).__nodeID);
- } else if (
- has('current', prop) &&
- prop.current instanceof WorkletEventHandler
- ) {
- if (viewTag === undefined) {
- viewTag = prop.current.viewTag;
- }
}
}
for (const key in prevProps) {
Test plan
Tested on the Example app and on the following code:
Expand
import React, { useState } from 'react';
import { Text, View } from 'react-native';
import {
FlatList,
Gesture,
GestureDetector,
} from 'react-native-gesture-handler';
import Animated, { BounceIn } from 'react-native-reanimated';
const items = [
{ name: 'Item A' },
{ name: 'Item B' },
{ name: 'Item C' },
{ name: 'Item D' },
{ name: 'Item A' },
{ name: 'Item B' },
{ name: 'Item C' },
{ name: 'Item D' },
{ name: 'Item A' },
{ name: 'Item B' },
{ name: 'Item C' },
{ name: 'Item D' },
{ name: 'Item A' },
{ name: 'Item B' },
{ name: 'Item C' },
{ name: 'Item D' },
{ name: 'Item A' },
{ name: 'Item B' },
{ name: 'Item C' },
{ name: 'Item D' },
];
function Item() {
const [faved, setFaved] = useState(false);
const color = faved ? '#900' : '#aaa';
const tap = Gesture.Tap()
.onEnd(() => {
setFaved(!faved);
})
.runOnJS(true);
return (
<GestureDetector gesture={tap}>
<Animated.View
key={color}
entering={BounceIn}
style={{ backgroundColor: color, width: 30, height: 30 }}
/>
</GestureDetector>
);
}
function renderItem({ item }: { item: { name: string } }) {
return (
<View
style={{
width: '100%',
height: 50,
backgroundColor: 'red',
flexDirection: 'row',
justifyContent: 'space-between',
padding: 10,
alignItems: 'center',
}}>
<Text>{item.name}</Text>
<Item />
</View>
);
}
export default function Example() {
return (
<View style={{ flex: 1 }}>
<FlatList style={{ flex: 1 }} data={items} renderItem={renderItem} />
</View>
);
}
Code to test the second commit:
Expand
import React from 'react';
import { View } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated from 'react-native-reanimated';
function Item() {
console.log('render item');
return (
<Animated.View
style={{
alignSelf: 'center',
width: 200,
height: 200,
backgroundColor: 'red',
}}
/>
);
}
export default function Example() {
const gesture = Gesture.Tap()
.onStart(() => {
console.log('a', _WORKLET);
})
.runOnJS(true);
console.log('render parent');
return (
<View style={{ flex: 1 }}>
<GestureDetector gesture={gesture}>
<Item />
</GestureDetector>
</View>
);
}
Change between View and Animated.View while the app is running and check if the tap still works. Remove .runOnJS(true) to test using Reanimated.