software-mansion / react-native-gesture-handler

Declarative API exposing platform native touch and gesture system to React Native.
https://docs.swmansion.com/react-native-gesture-handler/
MIT License
5.85k stars 954 forks source link

ScrollView nested within a PanGestureHandler #420

Closed dutzi closed 2 years ago

dutzi commented 5 years ago

I'd like to have a ScrollView within a PanGestureHandler where the ScrollView is scrollable but whenever scrollY <= 0 (and its not a momentum scroll) the PanGestureHandler should handle the gesture.

It's kind of hard to explain, so here is a video: https://streamable.com/hplw6

How could this be achieved?

origamih commented 5 years ago

This is my implementation:

import React from "react";
import { PanGestureHandler, State, ScrollView } from 'react-native-gesture-handler';

class Example extends React.Component {
  ref = React.createRef();
  scrollRef = React.createRef();
  state = {
    enable: true
  };
  _onScrollDown = (event) => {
    if (!this.state.enable) return;
    const {translationY} = event.nativeEvent;
    // handle PanGesture event here
  };

  _onScroll = ({nativeEvent}) => {
    if (nativeEvent.contentOffset.y <= 0 && !this.state.enable) {
      this.setState({enable: true });
    }
    if (nativeEvent.contentOffset.y > 0 && this.state.enable) {
      this.setState({enable: false});
    }
  };

  render () {
    const { enable } = this.state;
    return (
      <ScrollView
        ref={this.scrollRef}
        waitFor={enable ? this.ref : this.scrollRef}
        scrollEventThrottle={40}
        onScroll={this._onScroll}
      >
        <PanGestureHandler
          enabled={enable}
          ref={this.ref}
          activeOffsetY={5}
          failOffsetY={-5}
          onGestureEvent={this._onScrollDown}
        >

        </PanGestureHandler>
      </ScrollView>
    );
  }
}
hssrrw commented 5 years ago

I am trying to achieve the same effect as in the video above. Controlling enabled prop is too slow, because requires component update. It would be better if the feature would be implemented on the native side using PanGestureHandler.

Judging by the amount of apps with this kind of bottom panels with scrollable content, I'd say it's quite a popular case. Maybe the lib already allows to implement that somehow. Would really appreciate any feedback from the library authors.

pribeh commented 5 years ago

Using scrollview/flatlist from the gesture library permits this to work well on iOS but doesn't seem to work in Android. @hssrrw is correct that controlling enabled is too slow and the pangesturehandler responds to quick before its disabled. If we could define a region within a pangesturehandler that won't respond that could work in many if not most use cases.

I achieved this in another way by using interpolate and transform on an outer view to track the pangesturehandler transY position. Then only wrap the upper region above the scrollview with the pangesturehandler.

I've switched to using the interactable object now and I can't quite grasp how to achieve the same just yet but I'll work on it.

pribeh commented 5 years ago

I know this isn't ideal for everyone but this should cover various use cases. I've modified the render output of Interactable.js to something like this:

  render() {
    return (
      <Animated.View style={[
        this.props.style,
        {
          transform: [
            {
              translateX: this.props.verticalOnly ? 0 : this._transX,
              translateY: this.props.horizontalOnly ? 0 : this._transY,
            },
          ],
        },
      ]}>
        <PanGestureHandler
          maxPointers={1}
          enabled={this.props.dragEnabled}
          onGestureEvent={this._onGestureEvent}
          onHandlerStateChange={this._onGestureEvent}
        >
          <Animated.View>
            {this.props.header}
          </Animated.View>
        </PanGestureHandler>
        <Animated.View>
          {this.props.body}
        </Animated.View>
      </Animated.View>
    );
  }

The above is just an inversion of the pangesturehandler and the transforming view with the body separated out of the pangesturehandler. The idea is that you take the header section of your draggable object, which includes the handler bar, and insert it as your this.props.header and the rest of the contents, including scrollview, likewise into this.props.body of your component like so:

<Interactable.View
    header={this.renderHeader}
    body={this.renderBody}
    verticalOnly={true}
    style={[ styles.interactableContainer ]}
    snapPoints={[ { y:  this.topSnap }, { y: this.bottomSnap }, ]}
    boundaries={{ top: this.topSnap }}
    initialPosition={{ y: this.bottomSnap }}
    animatedValueY={this._deltaY} />

This way, you can still have as large an area to let the user drag your object up or down but the pangesturehandler won't interfere with the user's scrolling of the contents of that object. The only disadvantage is that the user can't drag the object up or down directly in the scrollview area. In many use cases, like ours, this is exactly what you don't want to happen so this works.

Tested on both Android and iOS and works great.

Noitidart commented 5 years ago

I'm also stuck on Android. I have a PanGestureHandler which has a descendent which is a ScrollView, but I cannot scroll in this ScrollView, the pan always is taking precedence. I even created a ref and passed it deeply down and then did:

<PanGestureHandler waitFor={ref}>
    ....
    <ScrollView ref={ref} />
    ....
</PanGestureHandler>

I'm on Android, using RN 0.59.5.

jmacindoe commented 5 years ago

Take a look at the bottom sheet example, it seems to be what you want https://github.com/kmagiera/react-native-gesture-handler/blob/master/Example/bottomSheet/index.js

The behaviour is also implemented by this library https://github.com/osdnk/react-native-reanimated-bottom-sheet

Although the bottom sheet example seemed a bit buggy when I tested it (jumped around if you scrolled it up and down really fast) and reanimated-bottom-sheet doesn't support nesting a ScrollView. The nested view will scroll, but it has to be a plain View without its own scrolling behaviour.

PierreCapo commented 4 years ago

am trying to achieve the same effect as in the video above. Controlling enabled prop is too slow, because requires component update. It would be better if the feature would be implemented on the native side using PanGestureHandler.

Judging by the amount of apps with this kind of bottom panels with scrollable content, I'd say it's quite a popular case. Maybe the lib already allows to implement that somehow. Would really appreciate any feedback from the library authors.

I totally agree with @hssrrw , I think there are really common usecases where you want to toggle a gesture handler and right now you need to come back to the JavaScript thread to do that. And even by doing that, I have to admit it was really buggy when I needed to do a setState while doing a gesture. Moreover, because of those setState I've needed to use use-memo-one or move to class components to memoize all the animated values.

pkf1994 commented 4 years ago

I'm also stuck on Android. I have a PanGestureHandler which has a descendent which is a ScrollView, but I cannot scroll in this ScrollView, the pan always is taking precedence. I even created a ref and passed it deeply down and then did:

<PanGestureHandler waitFor={ref}>
    ....
    <ScrollView ref={ref} />
    ....
</PanGestureHandler>

I'm on Android, using RN 0.59.5.

Did you fix it ? same issue on android

Fantasim commented 4 years ago

Any update about this case ?

pribeh commented 4 years ago

Our results are confusing. On a Samsung S9 and S10, the scrollview from RN works. But on all other phones we tested including Pixel, scrollview from Gesture Handler works. The S9 doesn't work with scrollview when imported from Gesture Handler though.

mayconmesquita commented 4 years ago

@pribeh i got similar results.

SudoPlz commented 4 years ago

All I did was change my import from import { ScrollView } from 'react-native'; to import { ScrollView } from 'react-native-gesture-handler';

and it worked.

Edit:

For people experiencing issues on Android, make sure the hitbox of your ScrollView content is where you expect it to be, explaining:

Sometimes eventhough you can see a view, the hitbox of that view (where the user can click/tap on) is not the same as what you're actually seeing. To troubleshoot, make sure you pass overflow: 'hidden' on all the child views of your ScrollView and check wether after you do that, a view that should respond to the gesture you're invoking becomes hidden.

In our case we had a View inside a LongPressGestureHandler that when passed overflow: 'hidden' became invisible. All we had to do was re-arrange our view structure, pass flexGrow: 1 to that view, and pretty much make it visible even with overflow: 'hidden' turned on. As soon as we did that the hitbox was the same as the visible area of that view, and gestures started working fine again.

Bigood commented 4 years ago

Can confirm that what @SudoPlz suggested work as intended, my nested scrollview now captures touches. Thanks!

pribeh commented 4 years ago

@SudoPlz we tried the same but it doesn't work for Samsung S9 and S10s. Let us know if your seeing different results.

JEGardner commented 4 years ago

I've managed to get this working with a mixture of NativeViewGestureHandler (more info here: https://github.com/software-mansion/react-native-gesture-handler/issues/492) and not directly nesting gesture handlers (more info here: https://github.com/software-mansion/react-native-gesture-handler/issues/71).

My eventual structure (with stuff removed) to nest a ScrollView with nested FlatLists inside a PanGestureHandler was:

<PanGestureHandler
  ref={this.panGestureHandlerRef}
  simultaneousHandlers={this.nativeHandlerRef}
>
    <Animated.View style={{ flex: 1 }}>
        <NativeViewGestureHandler
           ref={this.nativeHandlerRef}
           simultaneousHandlers={this.panGestureHandlerRef}
        >
            <Animated.ScrollView
              ref={this.wrapperRef}
              horizontal
            >
                // Array of flatlists also here
            </Animated.ScrollView>
        </NativeViewGestureHandler>
    </Animated.View>
</PanGestureHandler>

Hope this helps!

SudoPlz commented 4 years ago

@pribeh check my edited post, it may help you.

RBrNx commented 4 years ago

@SudoPlz Do you have a working example of this nested scrollview that you could share with us?

zabojad commented 4 years ago

All I did was change my import from import { ScrollView } from 'react-native'; to import { ScrollView } from 'react-native-gesture-handler';

and it worked.

In my case too...

neiker commented 3 years ago

This can be solved if ScrollView component gives access to pan gesture by implementing onGestureEvent prop.

TypeScript definitions says this is posible but in practice is not supported

a-eid commented 3 years ago

I'm gonna leave this here, it might help some one...

<PanGestureHandler 
  activeOffsetX={[-10, 10]}
  /* or */
  activeOffsetY={[-10, 10]}
>
zeabdelkhalek commented 3 years ago

@a-eid Works !!!!!

ShaMan123 commented 3 years ago

1034 ?

jakub-gonet commented 3 years ago

I think this can be closed now.

xilin commented 3 years ago

https://github.com/software-mansion/react-native-gesture-handler/blob/master/examples/Example/bottomSheet/index.js#L55 The key here is to add compensation of _reverseLastScrollY to translateY

kneza23 commented 3 years ago

I don't think that issue can be regarded as closed @jakub-gonet. The question was to make it work in a way that the scroll is disabled and enabled when on top or not.

Anyone found the solution?

levibuzolic commented 3 years ago

@kneza23 TapGestureHandler (or any gesture handler's ref) with a waitFor set on the NativeViewGestureHandler is the key to preventing the ScrollView from enabling under certain conditions.

The control is very limited, and once the ScrollView has activated and started scrolling there aren't any particularaly nice ways of cancelling it outside of setNativeProps({scrollEnabled: false}).

The bottom sheet example in the repo should give you the basic pattern: https://github.com/software-mansion/react-native-gesture-handler/blob/13053b92ac030e340099cdfee648623408bc8021/examples/Example/src/bottomSheet/index.tsx#L129-L132

But essentially the ScrollView will wait until the TapGestureHandler has either failed or activated, so by using a prop like maxDeltaY the gesture state will stay in BEGAN (which prevents the scrolling) until you exceed the maxDeltaY value, after which it'll switch to FAILED and the scroll will start running like normal. You'll need to make sure you set maxDeltaY to 0 or disable that gesture handler before the next scroll gesture when you want the scroll to take precedence and behave like normal.

I'm really hoping one day we get some better controls over the activation/deactivation of ScrollView -- being able to turn it on/off directly with a react-native-reanimated value would be an absolute game changer for complex gestures.

ehsan6sha commented 2 years ago

This combination was the only that worked for me for Reanimated.ScrollView as external scrollView of a RecyclerListView:

PanGestureHandler (simref to nativeView) > Reanimated.View > NativeViewGestureHandler(simref to Pan) > Reanimated.ScrollView (RecyclerListView)

l4k5hm4n commented 2 years ago

This combination was the only that worked for me for Reanimated.ScrollView as external scrollView of a RecyclerListView:

PanGestureHandler (simref to nativeView) > Reanimated.View > NativeViewGestureHandler(simref to Pan) > Reanimated.ScrollView (RecyclerListView)

I can't get it to work on android

ehsan6sha commented 2 years ago

This combination was the only that worked for me for Reanimated.ScrollView as external scrollView of a RecyclerListView: PanGestureHandler (simref to nativeView) > Reanimated.View > NativeViewGestureHandler(simref to Pan) > Reanimated.ScrollView (RecyclerListView)

I can't get it to work on android

Take a look at this project in the file components/SingleMedia.tsx https://github.com/functionland/photos

hoangvu12 commented 2 years ago

All I did was change my import from import { ScrollView } from 'react-native'; to import { ScrollView } from 'react-native-gesture-handler';

and it worked.

Edit:

For people experiencing issues on Android, make sure the hitbox of your ScrollView content is where you expect it to be, explaining:

Sometimes eventhough you can see a view, the hitbox of that view (where the user can click/tap on) is not the same as what you're actually seeing. To troubleshoot, make sure you pass overflow: 'hidden' on all the child views of your ScrollView and check wether after you do that, a view that should respond to the gesture you're invoking becomes hidden.

In our case we had a View inside a LongPressGestureHandler that when passed overflow: 'hidden' became invisible. All we had to do was re-arrange our view structure, pass flexGrow: 1 to that view, and pretty much make it visible even with overflow: 'hidden' turned on. As soon as we did that the hitbox was the same as the visible area of that view, and gestures started working fine again.

For anyone who using FlatList, just import FlatList from react-native-gesture-handler then it should scrollable. At least worked for me.

jakub-gonet commented 2 years ago

@levibuzolic did an excellent job explaining the current capabilities. If you can think of a better API we can implement to achieve that, please create a new issue.

LinusU commented 2 years ago

I can recommend using the no-restricted-imports ESLint rule to avoid importing the wrong ScrollView/FlatList:

'no-restricted-imports': [1, {
  paths: [
    {
      importNames: ['DrawerLayoutAndroid', 'FlatList', 'ScrollView', 'Switch', 'TextInput'],
      message: 'Please use `DrawerLayoutAndroid`, `FlatList`, `ScrollView`, `Switch`, `TextInput` from `react-native-gesture-handler` instead',
      name: 'react-native'
    }
  ]
}]
han-steve commented 2 years ago

The best implementation I've seen to date is from https://github.com/gorhom/react-native-bottom-sheet It basically uses 2 simultaneous handlers for the scrollview and the pan gesture, while continuously resetting the scrollview position to lock it in place. I drew this diagram to help me understand it I drew this diagram to help me understand it

han-steve commented 2 years ago

I also considered a way to implement it by using Manual Gestures (https://docs.swmansion.com/react-native-gesture-handler/docs/manual-gestures/manual-gestures) Basically if I can fail my scroll down gesture conditioned on a reanimated value, and have the scrollview activate when the bottomsheet pan gesture fails (by using the wait for), I can implement this behavior. However, I couldn't get the manual gesture to work (possibly due to bugs in the library), so I couldn't achieve it.

WintKhetSan commented 1 year ago

It might helps to anyone who stuck in swipe gesture within a scrollview :)

Reference to @origamih solution :

import { ScrollView, PanGestureHandler } from 'react-native-gesture-handler'; import useSwipe from 'hooks/useSwipe';

const ref = React.createRef(); const scrollRef = useRef(null); const [enable, setEnable] = useState(true);

// swipe feature const { onTouchStart, onTouchEnd } = useSwipe(onSwipeRight, 6);

function onSwipeRight() { navigation.goBack(); }

const onScroll = (event: any) => { if (event.nativeEvent.contentOffset.y <= 0 && !enable) { setEnable(true); } if (event.nativeEvent.contentOffset.y > 0 && enable) { setEnable(false); } };

const onGestureEnd = (event: any) => { if (!enable) return; // handle Gesture Event here onSwipeRight(); onTouchEnd(event); };

<ScrollView ref={scrollRef} onScroll={onScroll} waitFor={enable ? ref : scrollRef} scrollEventThrottle={40}>

<PanGestureHandler enabled={enable} ref={ref} activeOffsetX={5} failOffsetX={-15}

       onBegan={onTouchStart} onGestureEvent={onGestureEnd}>  

      <View>

           // your work is here

      </View>

</PanGestureHandler>

Here is my hook (useSwipe.tsx)

import { Dimensions } from 'react-native'; const windowWidth = Dimensions.get('window').width;

const useSwipe = (onSwipeRight?: any, rangeOffset = 4) => { let firstTouch = 0;

// set user touch start position
function onTouchStart(e: any) {
    firstTouch = e.nativeEvent.pageX;
}

// when touch ends check for swipe directions
function onTouchEnd(e: any) {
    // get touch position and screen size
    const positionX = e.nativeEvent.pageX;
    const range = windowWidth / rangeOffset;

    // check if position is growing positively and has reached specified range
    if (positionX - firstTouch > range) {
        onSwipeRight && onSwipeRight();
    }

    // check if position is growing negatively and has reached specified range
    // if (firstTouch - positionX > range) {
    //  onSwipeLeft && onSwipeLeft();
    // }
}

return { onTouchStart, onTouchEnd };

};

export default useSwipe;

ucheNkadiCode commented 1 year ago

I'm gonna leave this here, it might help some one...

<PanGestureHandler 
  activeOffsetX={[-10, 10]}
  /* or */
  activeOffsetY={[-10, 10]}
>

@a-eid This pretty much works however, when I click and drag something, it doesn't cling directly to my finger IF i go purely vertically up or down. They have to go a bit sideways to get the object to move. I'm using longPress to first make the object movable. Do you know any hack ways to basically overcome the activeOffset as soon as the object is "picked up"?

uwemneku commented 1 year ago

If you would like the PanGestureHandler to takes control only when the scrollOffset of the Scrollview is 0 or at the end, you could do so using shared values from reanimted 2 and onContentSizeChange. You can keep track of the scrollOffset and only take actions when scrollOffset is 0 or at the end of the scrollView. In the sample below, the Animated.View is only moved when this conditions are satisfied

import React, { useRef } from 'react';
import {
  ScrollView,
  PanGestureHandler,
  PanGestureHandlerGestureEvent,
} from 'react-native-gesture-handler';
import Animated, {
  useAnimatedGestureHandler,
  useAnimatedStyle,
  useSharedValue,
} from 'react-native-reanimated';
import { ScrollViewProps, View } from 'react-native';

const SCROLL_VIEW_HEIGHT = 300;
const NestedScrollView = () => {
  const scrollView_ref = useRef<ScrollView>(null);
  const gesture_ref = useRef<PanGestureHandler>(null);
  const scrollOffset = useSharedValue(0);
  const marginTop = useSharedValue(0);
  const scrollViewContentHeight = useSharedValue(0);

  const gestureHandler = useAnimatedGestureHandler<
    PanGestureHandlerGestureEvent,
    { start: number; prev: number }
  >(
    {
      onActive(_, e) {
        const isScrollingDownwards = e.start < _.y;
        const isDownGestureActive =
          isScrollingDownwards && scrollOffset.value === 0;
        const isUpGestureActive =
          !isScrollingDownwards &&
          Math.round(scrollOffset.value) ===
            Math.round(scrollViewContentHeight.value);

        if (isDownGestureActive || isUpGestureActive) {
          marginTop.value += _.translationY - e.prev || 0;
        } else {
          e.prev = _.translationY;
        }

        e.start = _.y;
      },
      onEnd(_, e) {
        e.prev = 0;
      },
    },
    [scrollOffset.value]
  );

  const scrollHandler: ScrollViewProps['onScroll'] = ({ nativeEvent }) => {
    scrollOffset.value = Math.round(nativeEvent.contentOffset.y);
  };
  const setContentSize: ScrollViewProps['onContentSizeChange'] = (_, h) => {
    scrollViewContentHeight.value = h - SCROLL_VIEW_HEIGHT;
  };

  const containerAnimatedStyle = useAnimatedStyle(() => ({
    transform: [{ translateY: marginTop.value }],
  }));

  return (
    <PanGestureHandler
      onGestureEvent={gestureHandler}
      ref={gesture_ref}
      simultaneousHandlers={scrollView_ref}>
      <Animated.View
        style={[
          containerAnimatedStyle,
          { height: SCROLL_VIEW_HEIGHT, backgroundColor: 'red' },
        ]}>
        <ScrollView
          bounces={false}
          onScroll={scrollHandler}
          onContentSizeChange={setContentSize}
          ref={scrollView_ref}
          simultaneousHandlers={gesture_ref}>
          {/* scrollview Content */}
        </ScrollView>
      </Animated.View>
    </PanGestureHandler>
  );
};

export default NestedScrollView;

check out this snack. Note that it works better on android

himanchau commented 1 year ago

I've finally figured this out with new pan gesture handler.

const gesture = Gesture.Pan()
  .onBegin(() => {
    // touching screen
    moving.value = true;
  })
  .onUpdate((e) => {
    // move sheet if top or scrollview or is closed state
    if (scrollY.value === 0 || prevY.value === closed) {
      transY.value = prevY.value + e.translationY - movedY.value;

    // capture movement, but don't move sheet
    } else {
      movedY.value = e.translationY;
    }

    // simulate scroll if user continues touching screen
    if (prevY.value !== open && transY.value < open) {
      runOnJS(scrollRef?.current.scrollTo)({ y: -transY.value + open, animated: false });
    }
  })
  .onEnd((e) => {
    // close sheet if velocity or travel is good
    if ((e.velocityY > 500 || e.translationY > 100) && scrollY.value < 1) {
      transY.value = withTiming(closed, { duration: 200 });
      prevY.value = closed;

    // else open sheet on reverse
    } else if (e.velocityY < -500 || e.translationY < -100) {
      transY.value = withTiming(open, { duration: 200 });
      prevY.value = open;

    // don't do anything
    } else {
      transY.value = withTiming(prevY.value, { duration: 200 });
    }
  })
  .onFinalize((e) => {
    // stopped touching screen
    moving.value = false;
    movedY.value = 0;
  })
  .simultaneousWithExternalGesture(scrollRef)

I've created a snack to show how it works (iOS). https://snack.expo.dev/@himanshu266/bottom-sheet-scrollview

okaybeydanol commented 1 year ago

I'm gonna leave this here, it might help some one...

<PanGestureHandler 
  activeOffsetX={[-10, 10]}
  /* or */
  activeOffsetY={[-10, 10]}
>

Thank you. That's exactly what I was looking for. I add a gesture to the shopify flash list. I was trying to make it deleted when dragging in x minus. I was looking for how to do it for an hour :)

<PanGestureHandler onGestureEvent={onGestureEvent} activeOffsetX={[-30, 0]}>

It works very well

netojose commented 10 months ago

All I did was change my import from import { ScrollView } from 'react-native'; to import { ScrollView } from 'react-native-gesture-handler';

and it worked.

Edit:

For people experiencing issues on Android, make sure the hitbox of your ScrollView content is where you expect it to be, explaining:

Sometimes eventhough you can see a view, the hitbox of that view (where the user can click/tap on) is not the same as what you're actually seeing. To troubleshoot, make sure you pass overflow: 'hidden' on all the child views of your ScrollView and check wether after you do that, a view that should respond to the gesture you're invoking becomes hidden.

In our case we had a View inside a LongPressGestureHandler that when passed overflow: 'hidden' became invisible. All we had to do was re-arrange our view structure, pass flexGrow: 1 to that view, and pretty much make it visible even with overflow: 'hidden' turned on. As soon as we did that the hitbox was the same as the visible area of that view, and gestures started working fine again.

This worked for me. Thanks!

neiker commented 1 month ago

With new Reanimated API roughly this is my solution:

// Replace with your own snap points
const snapPoints = [-400, 0];

function AwesomeContainer({ children }) {
  const scrollViewAnimatedRef = useAnimatedRef<Animated.ScrollView>();
  const translationY = useSharedValue(0);
  const scrollOffsetY = useSharedValue(0);

  const handleContentScroll = useAnimatedScrollHandler(({ contentOffset }) => {
    scrollOffsetY.value = contentOffset.y;
  }, []);

  const gesture = useMemo(() => {
    const initialScrollOffsetY = makeMutable(0);

    return Gesture.Pan()
      .simultaneousWithExternalGesture(scrollViewAnimatedRef)
      .onStart(() => {
        initialScrollOffsetY.value = scrollOffsetY.value;
      })
      .onUpdate(event => {
        translationY.value = clamp(
          event.translationY - initialScrollOffsetY.value,
          snapPoints[0],
          snapPoints[1]
        );
      })
      .onEnd(event => {
        if (translationY.value === snapPoints[0]) {
          return;
        }

        translationY.value = withTiming(snapTo(translationY.value, event.velocityY, snapPoints));
      });
  }, [scrollOffsetY, scrollViewAnimatedRef, translationY]);

  const positionAnimatedStyle = useAnimatedStyle(() => {
    return {
      transform: [{ translateY: translationY.value }]
    };
  }, []);

  return (
    <GestureDetector gesture={gesture}>
      <Animated.View style={positionAnimatedStyle}>
        <Animated.ScrollView
          ref={scrollViewAnimatedRef}
          bounces={false}
          showsVerticalScrollIndicator={false}
          onScroll={handleContentScroll}
          scrollEventThrottle={16}
        >
          {children}
        </Animated.ScrollView>
      </Animated.View>
    </GestureDetector>
  );
}