react-native-gesture-handler icon indicating copy to clipboard operation
react-native-gesture-handler copied to clipboard

Fix `GestureDetector` not working when the underlying view changes

Open j-piasecki opened this issue 3 years ago • 0 comments

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) {

also makes it work when using Reanimated, but I'm not sure whether it's fine to change it this way upstream. This needs to be discussed.

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.

j-piasecki avatar Jun 06 '22 09:06 j-piasecki