Skip to content

[Android] An active gesture is cancelled when Fabric moves its GestureDetector's view #4608

Closed

Description

@roidayan1

Description

On Android, a v3 gesture in a GestureDetector is cancelled when a re-render moves the detector's native view while the gesture is active, though the finger is still down. A drag-to-reorder list does exactly that: it moves the held row to its new place as the finger moves, and Fabric moves the row's view by removing it and adding it back. The gesture gets onTouchesCancelled and onDeactivate with canceled: true within a frame of the move.

Expected: the gesture stays active through the move and ends on release.
Actual: it is cancelled right after the move.

Cause: RNGestureHandlerDetectorView.onDetachedFromWindow calls detachAllHandlers(), and RNGestureHandlerRegistry.detachHandlerInternal cancels a handler whose view is set. A Fabric move removes the view and adds it back within one mount, so the detector is off the window only for that moment, but the cancel has already happened; on re-attach the handlers come back idle. The same screen with the v2 builder (Gesture.Pan()) keeps the drag, as v2 handlers aren't detached on a window detach. This is still the code on main.

We hit it in an app's board: cards in a FlatList, dragged by a handle with usePanGesture to reorder them live. Logcat from the pan's callbacks, three drags, each cancelled right after the first re-render that touched the held row:

# pick-up re-renders the held card dimmed, the list's scrolling off
03:33.663  onActivate
03:33.664  onUpdate y=716.1
03:33.680  onUpdate y=706.5
03:33.706  onTouchesCancelled
03:33.706  onDeactivate canceled=true

# pick-up collapses the list around a held section header
13:05.147  onActivate
13:05.147  onUpdate y=588.4
13:05.165  onUpdate y=581.7
13:05.183  onUpdate y=576.4
13:05.210  onTouchesCancelled
13:05.211  onDeactivate canceled=true

# nothing re-rendered on pick-up: the pan runs until the list first moves the held card
13:29.315  onActivate
13:29.315  onUpdate y=715.5
   ...     (22 more updates, finger still moving)
13:29.697  card moved in the list
13:29.738  onTouchesCancelled
13:29.738  onDeactivate canceled=true

With the callbacks only logging (nothing re-rendered), the same drag is never cancelled.

iOS: not checked. RNGestureHandlerDetector also detaches its handlers in willMoveToWindow: with a nil window, so it may behave the same.

Minimal code:

function Row({ id, onGrab, onRelease }) {
  const pan = usePanGesture({
    runOnJS: true,
    onActivate: () => onGrab(id),
    onDeactivate: (e) => onRelease(e.canceled),
  });
  return (
    <GestureDetector gesture={pan}>
      <View style={{ height: 80, marginBottom: 12, backgroundColor: "navy" }} />
    </GestureDetector>
  );
}

export default function Repro() {
  const [rows, setRows] = useState(["A", "B", "C", "D"]);
  const onGrab = (id) => setRows((r) => [id, ...r.filter((x) => x !== id)]);
  const onRelease = (canceled) => console.log("canceled:", canceled); // true on Android
  return (
    <FlatList
      data={rows}
      keyExtractor={(id) => id}
      renderItem={({ item }) => (
        <Row id={item} onGrab={onGrab} onRelease={onRelease} />
      )}
    />
  );
}

Steps to reproduce

  1. Render the code above on Android (New Architecture).
  2. Drag a row that isn't at the top, sideways so the list doesn't scroll, and keep moving.
  3. The row moves to the top as the pan activates.
  4. onDeactivate fires with canceled: true right after, while the finger is still down.

A link to a Gist, an Expo Snack or a link to a repository based on this template that reproduces the bug.

https://gist.github.com/roidayan1/6c9e3ebf4b5056d91806acd888a7ebca

Gesture Handler version

3.3.0 (the code is unchanged on main after 3.4.0)

React Native version

0.88.0

Platforms

Android

JavaScript runtime

Hermes

Workflow

Using Expo Prebuild or an Expo development build

Build type

Release mode

Device

Android emulator

Device model

Pixel emulator, Android 16 (API 36)

Acknowledgements

Yes

Activity

  1. m-bert commented on Oct 20, 2026

    @m-bert
    Collaborator

    Hi @roidayan1! Thank you for reporting this issue!

    This is expected in Gesture Handler 3. GestureDetector is now a host component (unless you're on the builder API), so when Fabric moves its view, the detector gets detached and its gestures are cancelled.

    Your case is exactly what VirtualGestureDetector is for - have you tried it? Wrap the list in InterceptingGestureDetector and use VirtualGestureDetector per row. This should let Pan survive reordering on Android.

  2. roidayan1 commented on Oct 20, 2026

    @roidayan1
    Author

    Thanks @m-bert, that's what we ended up doing: InterceptingGestureDetector around the list and VirtualGestureDetector per row, and the pan now survives the moves on Android. I'm closing #4609.

    A suggestion for the docs: the detector page doesn't say that a plain GestureDetector cancels an active gesture when Fabric moves its view, so a drag-to-reorder list built the obvious way breaks only on Android. A short note there, pointing to VirtualGestureDetector for rows that move, would save the next person the search.

  3. m-bert commented on Oct 21, 2026

    @m-bert
    Collaborator

    Good to hear that it works. What do you think about #4623? Is it something that you had in mind?

  4. roidayan1 commented on Oct 21, 2026

    @roidayan1
    Author

    @m-bert Yes, that's exactly what I had in mind, thanks! The example matches our case one to one.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions