Currently, I am working on a unique feature where there is a horizontal ScrollView
containing pink boxes. Each box is constructed using Animated.View
along with a GestureDetector
to handle the dragging functionality. My goal is to enable the user to drag a pink box outside of the scroll view area (represented by the blue box) so that it becomes "detached". However, the challenge arises when the dragged box remains connected to the scroll view even when it's moved beyond its boundaries.
To illustrate this issue, I have created a simplified example which demonstrates the problem through a GIF:
The GIF above showcases the behavior where the pink box continues to be scrolled along with the scroll view despite being moved outside of the designated area. Moreover, once the box is positioned in the white space outside of the scroll view, it loses the ability to be further dragged or panned, which is not the intended behavior.
I have prepared an Expo Snack for you to test and experience the problem firsthand. For the best results, I recommend testing it on an iOS device within the Expo Go app.
Below is the code snippet responsible for creating the scenario described above. The key point to note is the usage of overflow: visible
property in the scroll view, as this may play a role in causing the observed behavior:
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"
}
});
Additionally, here is the Box
component mentioned above that is rendered within the 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,
},
});