Skip to content

Conversation

@j-piasecki
Copy link
Member

Description

Supersedes #2283

On Android, onTouchesDown was delayed to be dispatched after onBegin so that the handlers had the time to initialize themselves (which happens in onHandle) so that the non-touch events would have correct data.

The old approach was saving the event, which triggered the touch event, and using that event to initialize the handler during imperative state change. The issues with that approach were:

  1. Storing the event for the duration of the event handling
  2. It wouldn't work with asynchronous state changes, which are now possible

The new approach is to add an option to force gestures to initialize regardless of the state they are in. This way, when the state is updated and the current state of the handler is UNDETERMINED, the flag is set on the handler, and when it starts handling the event, it will first initialize itself.

Test plan

Tested on the following snippet, with affected gestures

import React from 'react';
import { StyleSheet, View } from 'react-native';
import { usePan, NativeDetector } from 'react-native-gesture-handler';

export default function EmptyExample() {
  const pan = usePan({
    onTouchesDown: () => {
      'worklet';
      console.log('Touch down');
      globalThis._setGestureStateSync(57, 4);
    },
    onTouchesMove: () => {
      'worklet';
      console.log('Touch move');
    },
    onTouchesUp: () => {
      'worklet';
      console.log('Touch up');
    },
    onTouchesCancelled: () => {
      'worklet';
      console.log('Touch cancel');
    },
    onBegin: () => {
      'worklet';
      console.log('Gesture begin');
    },
    onStart: (e) => {
      'worklet';
      console.log('Gesture start');
    },
    onUpdate: (e) => {
      'worklet';
      console.log('Gesture update', e.handlerData.translationX, e.handlerData.translationY);
    },
    onEnd: () => {
      'worklet';
      console.log('Gesture end');
    },
    onFinalize: () => {
      'worklet';
      console.log('Gesture finalize');
    },
  });

  console.log('Rendering EmptyExamplee', pan.tag);

  return (
    <View style={styles.container}>
      <NativeDetector gesture={pan}>
        <View style={{ width: 300, height: 300, backgroundColor: 'green' }} />
      </NativeDetector>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

@j-piasecki j-piasecki merged commit 99daffa into next Nov 3, 2025
3 checks passed
@j-piasecki j-piasecki deleted the @jpiasecki/android-callback-ordering branch November 3, 2025 10:42
j-piasecki added a commit that referenced this pull request Nov 12, 2025
… soon (#3794)

## Description

Ordering of touch events on the web was mostly aligned with Android. To
unify the ordering across all platforms (along
#3793),
this PR changes it so that `onTouchesDown` is called before `onBegin`
and `onTouchesMove` is called before `onUpdate`.

Another change is enforcing going through `BEGAN` when imperatively
changing the state from `UNDETERMINED` to `ACTIVE`.

And a fix for an issue that came out due to the above changes - when the
discrete gestures were imperatively activated inside `onTouchesDown`,
the order was:
```
Touch down
Gesture begin
Gesture start
Touch cancel
Gesture end
Gesture finalize
Gesture begin
Gesture finalize
```

This was because those gestures transition to the `END` state
immediately after `ACTIVE`, and get reset immediately. Then, the normal
flow resumes, and they go to the `BEGAN` as a result of touch input. To
prevent that, I changed `cleanupFinishedHandlers` to be queued as a
microtask instead of being executed immediately. This should ensure that
it will be called in the same run loop, but after the gesture handles
its state.

## Test plan

Tested on the following code:
```
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';

export default function EmptyExample() {
  const pan = Gesture.Pan()
    .onTouchesDown((e, stateManager) => {
      'worklet';
      console.log('Touch down');
      stateManager.activate();
    })
    .onTouchesMove(() => {
      'worklet';
      console.log('Touch move');
    })
    .onTouchesUp(() => {
      'worklet';
      console.log('Touch up');
    })
    .onTouchesCancelled(() => {
      'worklet';
      console.log('Touch cancel');
    })
    .onBegin(() => {
      'worklet';
      console.log('Gesture begin');
    })
    .onStart((e) => {
      'worklet';
      console.log('Gesture start');
    })
    .onEnd(() => {
      'worklet';
      console.log('Gesture end');
    })
    .onFinalize(() => {
      'worklet';
      console.log('Gesture finalize');
    });

  return (
    <View style={styles.container}>
      <GestureDetector gesture={pan}>
        <View style={{ width: 300, height: 300, backgroundColor: 'green' }} />
      </GestureDetector>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants