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

blocksExternalGesture doesn't work together with activeOffsetX #3321

Open
gaearon opened this issue Jan 9, 2025 · 5 comments · May be fixed by #3322
Open

blocksExternalGesture doesn't work together with activeOffsetX #3321

gaearon opened this issue Jan 9, 2025 · 5 comments · May be fixed by #3322
Labels
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 9, 2025

Description

I'm trying to get scroll views and pan gestures to coordinate and I'm just not able to do that.

This time it seems (maybe?) specific to using .activeOffsetX.

Here's my code:

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

export default function App() {
  const val = useSharedValue(0)
  const pan = Gesture.Pan()
    .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)
    })
  // .activeOffsetX([-5, 5])
  // ^^^ THIS TRIGGERS THE BUG ^^^

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

function InnerScrollView({pan}) {
  const native = Gesture.Native().blocksExternalGesture(pan)
  return (
    <View
      style={{
        paddingTop: 200,
        alignItems: 'center',
      }}>
      <GestureDetector gesture={native}>
        <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>
  )
}

So there's a pan handler that translates the outer view, and there's a scroll view nested inside.

The desired behavior is that dragging the inner scroll view should fail the pan handler.

It's supposedly achieved by:

  const native = Gesture.Native().blocksExternalGesture(pan)
  // ...
      <GestureDetector gesture={native}>
        <ScrollView

(I did verify this is the correct ScrollView.)

However, if I uncomment this line:

    .onFinalize(() => {
      'worklet'
      console.log('finalize')
      val.set(0)
    })
  // .activeOffsetX([-5, 5])
  // ^^^ THIS TRIGGERS THE BUG ^^^

Then the behavior seems to become nondeterministic. Sometimes it'll scroll the scroll view, sometimes it'll activate the pan handler. I can't spot any specific pattern to it.

Tried this both with 2.22.0-rc.1 but I think it's present on earlier versions too.

I haven't checked Android.

nondet.mov

Again, the intended behavior (as specified in code) is that the scroll view gesture should block the pan handler (and therefore the pan handler's activation conditions should have nothing to do with whether the scroll view activates).

Steps to reproduce

See above

Snack or a link to a repository

https://snack.expo.dev/71UGWTSsSo5UO9XeO7-7F

Gesture Handler version

2.22.0-rc.1

React Native version

0.76.3

Platforms

iOS

JavaScript runtime

Hermes

Workflow

Expo bare workflow

Architecture

Paper (Old Architecture)

Build type

Debug mode

Device

iOS simulator

Device model

No response

Acknowledgements

Yes

@github-actions github-actions bot added Repro provided A reproduction with a snack or repo is provided Platform: iOS This issue is specific to iOS labels Jan 9, 2025
@gaearon
Copy link
Author

gaearon commented Jan 9, 2025

It seems like inverting the relationship and using requireExternalGestureToFail doesn't seem to suffer from the same problem. This seems to work:

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

export default function App() {
  const val = useSharedValue(0)

  const native = Gesture.Native()
  const pan = Gesture.Pan()
    .requireExternalGestureToFail(native)
    .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)
    })
    .activeOffsetX([-10, 10])

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

function InnerScrollView({native}) {
  return (
    <View
      style={{
        paddingTop: 200,
        alignItems: 'center',
      }}>
      <GestureDetector gesture={native}>
        <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>
  )
}

@gaearon
Copy link
Author

gaearon commented Jan 10, 2025

For context, here's the actual bug we're trying to fix: bluesky-social/social-app#7417

@j-piasecki
Copy link
Member

Could you check if the above PR fixes the problem for you?

@gaearon
Copy link
Author

gaearon commented Jan 10, 2025

It does!

@gaearon
Copy link
Author

gaearon commented Jan 10, 2025

Found another problem though. #3326

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

Successfully merging a pull request may close this issue.

2 participants