I'm building a bottom-sheet that one can swipe up/down to change its height.
I want its inner children component to be scrollable, but it doesnt work as expected.
I'm just guessing the cause of this is that any touch event like scrolling in ScrollView
gets captured and bubbled up to GestureDetector
so scrolling the inner content doesnt work while swiping the sheet itself works.
import { Dimensions, StyleSheet, TouchableOpacity } from 'react-native';
import React, { useCallback, useEffect } from 'react';
import Animated from 'react-native-reanimated';
import {
Gesture,
GestureDetector,
ScrollView,
} from 'react-native-gesture-handler';
import {
Extrapolate,
interpolate,
useAnimatedStyle,
useSharedValue,
withSpring,
} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useHeaderHeight } from '@react-navigation/elements';
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
type Props = {
top: number;
bottom: number;
initialBottomOffset: number;
};
export const BottomSheet: React.FC<Props> = ({
children,
top,
bottom,
initialBottomOffset,
}) => {
const translateY = useSharedValue(0);
const insets = useSafeAreaInsets();
const HEADER_HIGHT = useHeaderHeight();
const MAX_TRANSLATE_Y = -SCREEN_HEIGHT + top;
const MIN_TRANSLATE_Y = -(bottom + insets.bottom);
const scrollTo = useCallback((destination: number) => {
'worklet';
translateY.value = withSpring(destination, { damping: 50 });
}, []);
const context = useSharedValue({ y: 0 });
const gesture = Gesture.Pan()
.onStart(() => {
context.value = { y: translateY.value };
})
.onUpdate(event => {
translateY.value = event.translationY + context.value.y;
translateY.value =
event.translationY < 0
? Math.max(translateY.value, MAX_TRANSLATE_Y)
: Math.min(translateY.value, MIN_TRANSLATE_Y);
if (Math.abs(event.velocityY) > 300) {
if (event.translationY < 0) {
scrollTo(MAX_TRANSLATE_Y);
} else {
scrollTo(MIN_TRANSLATE_Y);
}
}
});
const animatedBottomSheetStyle = useAnimatedStyle(() => {
const borderRadius = interpolate(
translateY.value,
[MAX_TRANSLATE_Y + HEADER_HIGHT, MAX_TRANSLATE_Y],
[25, 0],
Extrapolate.CLAMP,
);
return {
borderRadius,
transform: [{ translateY: translateY.value }],
};
});
useEffect(() => {
scrollTo(MIN_TRANSLATE_Y);
}, [bottom]);
useEffect(() => {
scrollTo(-initialBottomOffset);
}, [initialBottomOffset]);
return (
<GestureDetector gesture={gesture}>
<Animated.View
style={[styles.bottomSheetContainer, animatedBottomSheetStyle]}>
<TouchableOpacity
style={styles.handle}
onPress={() => scrollTo(MAX_TRANSLATE_Y)}
/>
<ScrollView>{children}</ScrollView>
</Animated.View>
</GestureDetector>
);
};
const styles = StyleSheet.create({
bottomSheetContainer: {
position: 'absolute',
top: SCREEN_HEIGHT,
width: '100%',
height: '100%',
backgroundColor: '#fff',
borderRadius: 25,
flex: 1,
},
handle: {
width: 45,
height: 5,
backgroundColor: '#CEDEEA',
alignSelf: 'center',
marginVertical: 10,
borderRadius: 4,
},
});