Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

requireExternalGestureToFail and blocksExternalGesture don't work reliably #3326

Open
gaearon opened this issue Jan 10, 2025 · 0 comments
Open
Labels
Platform: Android This issue is specific to Android Platform: iOS This issue is specific to iOS Repro provided A reproduction with a snack or repo is provided

Comments

@gaearon
Copy link

gaearon commented Jan 10, 2025

Description

Continuing my adventures with these APIs.

I have three gestures:

  • A top-level Pan handler for a swipeable drawer.
  • A Pager. The Pager has multiple pages.
  • A nested ScrollView on Pager's pages.

Here's how I want them to work:

  • The nested ScrollView should take precedence over anything else.
  • Then, the Pager.
  • Then (only if we're on the first page), the Drawer.

This is proving quite tricky.

Example 1: Broken (requireExternalGestureToFail in parent)

Let's start with this baseline code:

import {useState} from 'react'
import {ScrollView, Text, View} from 'react-native'
import {
  Gesture,
  GestureDetector,
  GestureHandlerRootView,
} from 'react-native-gesture-handler'
import PagerView from 'react-native-pager-view'
import Animated, {
  useAnimatedStyle,
  useSharedValue,
} from 'react-native-reanimated'

export default function App() {
  const val = useSharedValue(0)
  const [swipeEnabled, setSwipeEnabled] = useState(true)

  const innerNative = Gesture.Native()

  let pan = Gesture.Pan()
    .requireExternalGestureToFail(innerNative)
    .onBegin(() => {
      'worklet'
      console.log('begin')
      val.set(0)
    })
    .onUpdate(e => {
      'worklet'
      console.log('update')
      val.set(e.translationX)
    })
    .onEnd(() => {
      'worklet'
      console.log('end')
      val.set(0)
    })
    .onFinalize(() => {
      'worklet'
      console.log('finalize')
      val.set(0)
    })

  if (swipeEnabled) {
    pan = pan.failOffsetX(-1).activeOffsetX(5)
  } else {
    pan = pan.failOffsetX([0, 0]).failOffsetY([0, 0])
  }

  const style = useAnimatedStyle(() => {
    return {
      flex: 1,
      transform: [
        {
          translateX: val.value,
        },
      ],
    }
  })
  return (
    <GestureHandlerRootView>
      <GestureDetector gesture={pan}>
        <Animated.View style={style}>
          <Pager pan={pan} setSwipeEnabled={setSwipeEnabled}>
            <InnerScrollView pan={pan} innerNative={innerNative} />
          </Pager>
        </Animated.View>
      </GestureDetector>
    </GestureHandlerRootView>
  )
}

function Pager({children, pan, setSwipeEnabled}) {
  const native = Gesture.Native().requireExternalGestureToFail(pan)
  return (
    <GestureDetector gesture={native}>
      <PagerView
        overdrag={true}
        initialPage={0}
        style={{
          flex: 1,
          backgroundColor: 'green',
        }}
        onPageSelected={e => {
          setSwipeEnabled(e.nativeEvent.position === 0)
        }}>
        {children}
        {children}
        {children}
      </PagerView>
    </GestureDetector>
  )
}

function InnerScrollView({pan, innerNative}) {
  return (
    <View
      style={{
        paddingTop: 200,
        alignItems: 'center',
      }}>
      <GestureDetector gesture={innerNative}>
        <ScrollView
          horizontal
          pagingEnabled
          style={{
            width: 300,
            backgroundColor: 'yellow',
            height: 200,
          }}>
          <Text
            style={{
              width: 1000,
            }}>
            1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
            27 28 29 30 31 32 33 34 35 36 37 38 39 40
          </Text>
        </ScrollView>
      </GestureDetector>
    </View>
  )
}

Here, I'm declaring that the inner ScrollView should take precedence over Pan:

  const innerNative = Gesture.Native()

  let pan = Gesture.Pan()
    .requireExternalGestureToFail(innerNative)

However, this doesn't work. Neither on iOS nor on Android:

bug-1.mov

Example 2: Broken (blocksExternalGesture in parent)

Okay, now let's try to express the relationship in the reverse:

  let pan = Gesture.Pan()
    // ...

  const innerNative = Gesture.Native().blocksExternalGesture(pan)

(Full snippet)

This also doesn't work, neither on iOS nor on Android:

bug-2.mov

Example 3: Works (blocksExternalGesture in child)

Now let's move the innerNative declaration down the component chain.

We'll remove it here:

  let pan = Gesture.Pan()
    // ...

  // ------- REMOVED IT HERE: -------
  // const innerNative = Gesture.Native().blocksExternalGesture(pan)

and add it here:

function InnerScrollView({pan}) {
  // -------  ADDED IT HERE: -------
  const innerNative = Gesture.Native().blocksExternalGesture(pan)

(Full snippet)

Now it works!

finally.mov

Conclusions

To sum up:

  • requireExternalGestureToFail in the parent didn't work (both platforms)
  • blocksExternalGesture in the parent didn't work either (both platforms)
  • blocksExternalGesture in the child did work (both platforms)

This is with 2.20.2 + #3322 applied.

I wanted to test this with 2.22.0 but I couldn't get even the most basic view running locally because of some error about "unknown view tag" while rendering GestureDetector.

That said, this Snack (Example 1) is running 2.22.0-rc.1 and does reproduce the first issue, at least on Android. And this Snack (Example 2) reproduces the second issue. So I think, at least for Android, the bug is present in the latest.

Steps to reproduce

  1. See above

Snack or a link to a repository

https://snack.expo.dev/mzHvgIje0N9z2MuCjQfqW?platform=android

Gesture Handler version

2.22.0-rc.1

React Native version

0.76.3

Platforms

Android, iOS

JavaScript runtime

Hermes

Workflow

Expo bare workflow

Architecture

Paper (Old Architecture)

Build type

Debug mode

Device

Android emulator

Device model

No response

Acknowledgements

Yes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Platform: Android This issue is specific to Android Platform: iOS This issue is specific to iOS Repro provided A reproduction with a snack or repo is provided
Projects
None yet
Development

No branches or pull requests

1 participant