Skip to content

Commit

Permalink
Fix handlers coming back from being cancelled (#2704)
Browse files Browse the repository at this point in the history
## Description

This is loosely related to
#2693,
as debugging it allowed to find this bug.

Currently when a handler finishes while it's waiting, it only sends
events if it fails or when it gets canceled. If the gesture finishes
successfully, the event is not sent as it's waiting for another one. If
the gesture it's waiting for fails, it goes through all awaiting
handlers and tries to activate them if criteria are met.

Since the gesture it tries to activate is already finished, synthetic
events need to be sent to the JS side to achieve the correct behavior.
Because of that, if the gesture is cancelled while waiting and then the
gesture it waited for fails, GH will try to activate the cancelled
gesture and will send the synthetic events. So the events would look
like this:
1. `UNDETERMINED` -> `BEGAN`
2. `BEGAN` -> `CANCELLED`
3. `BEGAN` -> `ACTIVE`
4. `ACTIVE` -> `ENDED`
5. `ENDED` -> `UNDETERMINED`

I.e. in certain conditions, a gesture could be activated after it was
canceled, this PR simply adds a condition that the gesture cannot be in
`FAILED` or `CANCELLED` state before sending the synthetic events.

## Test plan

<details>
<summary>Tested on the following code in the Example app.</summary>

```jsx
import React, { useRef } from 'react';
import { StyleSheet, Text, ScrollView, View } from 'react-native';
import {
  GestureDetector,
  Gesture,
  RectButton,
  GestureHandlerRootView,
} from 'react-native-gesture-handler';

export const DATA = new Array(100).fill(0).map((_, index) => `Item ${index}`);

function Button({ text, panRef }: any) {
  const [selected, setSelected] = React.useState(false);

  return (
    <RectButton
      waitFor={panRef}
      style={[styles.selectableItem, selected && styles.selectedItem]}
      onHandlerStateChange={(e) => {
        console.log(
          'State change',
          e.nativeEvent.oldState,
          e.nativeEvent.state
        );
      }}
      onPress={() => {
        setSelected(!selected);
        console.log('Pressed', text);
      }}>
      <Text style={[styles.genreText, styles.horizontalMargin]}>{text}</Text>
    </RectButton>
  );
}

export default function App() {
  const panRef = useRef<any>(null);

  const scrollGesture = Gesture.Native();
  const pan = Gesture.Pan().manualActivation(true).withRef(panRef);

  return (
    <GestureDetector gesture={pan}>
      <View style={{ flex: 1 }}>
        <GestureDetector gesture={scrollGesture}>
          <ScrollView>
            <GestureHandlerRootView>
              {DATA.map((text, index) => (
                <Button key={index} text={text} panRef={panRef} />
              ))}
            </GestureHandlerRootView>
          </ScrollView>
        </GestureDetector>
      </View>
    </GestureDetector>
  );
}

export const styles = StyleSheet.create({
  genreText: {
    fontSize: 18,
    fontWeight: '600',
    marginVertical: 12,
  },
  selectableItem: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingVertical: 8,
  },
  horizontalMargin: {
    marginHorizontal: 16,
  },
  selectedItem: {
    backgroundColor: '#aaa',
  }
});

```
</details>

Before:


https://github.com/software-mansion/react-native-gesture-handler/assets/21055725/35211c91-544b-4612-9e80-ee8abed9b646

After:


https://github.com/software-mansion/react-native-gesture-handler/assets/21055725/c8b8da6e-aae8-4b09-acca-1286dab6b4fa

---------

Co-authored-by: Michał Bert <[email protected]>
  • Loading branch information
j-piasecki and m-bert authored Feb 5, 2024
1 parent 60258e2 commit 8ea9177
Showing 1 changed file with 12 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,19 @@ class GestureHandlerOrchestrator(
}
cleanupAwaitingHandlers()

// Dispatch state change event if handler is no longer in the active state we should also
// trigger END state change and UNDETERMINED state change if necessary
// At this point the waiting handler is allowed to activate, so we need to send BEGAN -> ACTIVE event
// as it wasn't sent before. If handler has finished recognizing the gesture before it was allowed to
// activate, we also need to send ACTIVE -> END and END -> UNDETERMINED events, as it was blocked from
// sending events while waiting.
// There is one catch though - if the handler failed or was cancelled while waiting, relevant event has
// already been sent. The following chain would result in artificially activating that handler after the
// failure logic was ran and we don't want to do that.
if (currentState == GestureHandler.STATE_FAILED || currentState == GestureHandler.STATE_CANCELLED) {
return
}

handler.dispatchStateChange(GestureHandler.STATE_ACTIVE, GestureHandler.STATE_BEGAN)

if (currentState != GestureHandler.STATE_ACTIVE) {
handler.dispatchStateChange(GestureHandler.STATE_END, GestureHandler.STATE_ACTIVE)
if (currentState != GestureHandler.STATE_END) {
Expand Down

0 comments on commit 8ea9177

Please sign in to comment.