1

I currently have a horizontal ScrollView with a few pink boxes in it. These boxes are built using Animated.View and a GestureDetector to manage dragging of the box. I want to be able to drag the pink box outside of the scroll view area (the blue box), so that it becomes "detached". Currently when I drag a box "outside" of the scroll view, it's still technically a child of the scroll view, so scrolling the scroll view cuases the pink box to be scrolled also.

I've tried to create a minimal reproducible example of my issue. Here is a GIF of my problem:

An animated GIF showing two pink boxes in a scroll view. When the pink box is dragged outside of the scroll view, and the scroll view is then scrolled, the pink box previously dragged outside of the scroll view still scrolls

Above you can see that when I drag the pink box outside of the scroll view area, the pink box is still scrolled even when "outside" of the scroll view/blue scroll area. Additionally, the pink box can't be dragged/panned once moved to the white area outside of the scroll view which isn't desired either.

I have created this expo snack to show my issue and so you can try it for yourself. This is best tested on iOS (within the Expo Go app).

Below is my code for achieving the above example. I'm using overflow: visible on the scroll view to allow the pink box to be visible once dragged outside of the scroll view, which I think is part of the problem:

App.js

export default function App() {
  const boxes = useState([{id: 0}, {id: 1}]);

  return (
    <SafeAreaView>
      <GestureHandlerRootView style={styles.container}>
        <View style={styles.scrollViewContainer}>
          <ScrollView style={styles.scrollViewStyles} horizontal={true}>
            {boxes.map(({id}) => <Box key={id} />)}
          </ScrollView>
        </View>
        <StatusBar style="auto" />
      </GestureHandlerRootView>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
  },
  scrollViewContainer: {
    width: '100%',
    backgroundColor: "lightblue",
    height: 150,
  },
  scrollViewStyles: {
    overflow: "visible"
  }
});

And here is my Box component that is being rendered above in App:

const SIZE = 100;
export default function Box(props) {
  const translateX = useSharedValue(0);
  const translateY = useSharedValue(0);
  const context = useSharedValue({x: 0, y: 0});

  const panGesture = Gesture.Pan()
    .onBegin(() => {
      context.value = {x: translateX.value, y: translateY.value};
    })
    .onUpdate((event) => {
      translateX.value = event.translationX + context.value.x;
      translateY.value = event.translationY + context.value.y;
    });

  const panStyle = useAnimatedStyle(() => ({
    transform: [
      {translateX: withSpring(translateX.value)},
      {translateY: withSpring(translateY.value)},
    ]
  }));

  return (<GestureDetector gesture={panGesture}>
    <Animated.View style={[styles.box, panStyle]} />
  </GestureDetector>);
}

const styles = StyleSheet.create({
  box: {
    width: SIZE,
    height: SIZE,
    marginHorizontal: 10,
    borderRadius: 20,
    backgroundColor: "pink",
    shadowColor: "#000",
    shadowOffset: {width: 0, height: 0},
    shadowOpacity: 0.5,
  },
});
Shnick
  • 1,199
  • 1
  • 13
  • 32
  • after dragging an item from scrollview is it possible drag it anywhere else? if not we can stop it moving after you drag it out from the first view. – Ushan Fernando Jul 23 '22 at 13:54
  • @UshanFernando Hi, ideally it should still be draggable once th pink box is moved to the white area outside of the scroll view. – Shnick Jul 23 '22 at 14:01

1 Answers1

1

Only possible way i was able to come up with to achieve this is by removing the item from scrollview when the user move it out from the scroll view and re-rendering it outside the scrollview.

You can detect wether the object is moved outside of the scrollview by listening to onEnd method on pan gesture.

 .onEnd((event) => {
      if (translateY.value > 90) {
        runOnJS(props.moveItem)(
          event.translationX ,
          event.translationY + context.value.y,
          props.id
        );
      }

And Based on that you can remove that item from the array rendered inside scrollview and add it to the array rendered outside the scroll view.

  const moveItem = (x, y, id) => {
    const boxesF = boxes.filter((e) => e.id != id);
    setBoxes(boxesF);
    movedItems.push();
    setMovedItems([...movedItems,{ x: x, y: y, id:id }]);
    console.log('called'+id);
    console.log(x);
    console.log(y);
  };

and after that you can render the items separately

    <View style={styles.scrollViewContainer}>
      <ScrollView style={styles.scrollViewStyles} horizontal={true}>
        {boxes?.map(({ id }) => (
          <Box key={id} moveItem={moveItem} id={id} />
        ))}
      </ScrollView>
    </View>
    <View>
      {movedItems?.map((e) => {
        return <Box translateX={e.x} translateY={e.y} key={e.id}/>;
      })}
    </View>

I followed this strategy and implemented a sample snack project. even though i was able to separate the items from scrollview. positioning of items are not perfect you may have to find a way to position them exactly where user dragged them. (You may have to do some calculations based on translation values)

Snack Url

Ushan Fernando
  • 622
  • 4
  • 19
  • Thanks Ushan, I've tried to implement this into my project and it somewhat works. The main issue I have is that I only want to swap the Box to the movedItems state when the spring animation is complete to avoid jolty behaviour, so I've movd the logic for `props.moveItem` into a `withSpring()` callback. The issue is that it takes a bit of time for the animation to settle and complete, and if the view is scrolled during that time, the element still scrolls with the scroll view. I'm thinking that creating a custom scroll view & only moving certain children within the view might be an option – Shnick Jul 28 '22 at 12:25