Skip to content

Instantly share code, notes, and snippets.

@jai-adapptor
Last active December 10, 2023 05:07
Show Gist options
  • Save jai-adapptor/bc3650ab20232d8ab076fa73829caebb to your computer and use it in GitHub Desktop.
Save jai-adapptor/bc3650ab20232d8ab076fa73829caebb to your computer and use it in GitHub Desktop.
FULL
import React, { useEffect, useState } from 'react';
import { Dimensions, SafeAreaView, StyleSheet, Text, View } from 'react-native';
import {
PanGestureHandler,
TouchableOpacity,
ScrollView,
} from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withSpring,
WithSpringConfig,
} from 'react-native-reanimated';
interface SheetProps {
minHeight?: number;
maxHeight?: number;
expandedHeight?: number;
}
type SheetPositions = 'minimised' | 'maximised' | 'expanded';
const window = Dimensions.get('window');
const screen = Dimensions.get('screen');
const NAV_HEIGHT = 48;
const Sheet: React.FC<SheetProps> = (props) => {
const [dimensions, setDimensions] = useState({ window, screen });
useEffect(() => {
// Watch for screen size changes and update the dimensions
const subscription = Dimensions.addEventListener(
'change',
({ window, screen }) => {
setDimensions({ window, screen });
},
);
return () => subscription?.remove();
});
// Fixed values (for snap positions)
const minHeight = props.minHeight || 120;
const maxHeight = props.maxHeight || dimensions.screen.height;
const expandedHeight = props.expandedHeight || dimensions.screen.height * 0.6;
// Animated values
const position = useSharedValue<SheetPositions>('minimised');
const sheetHeight = useSharedValue(-minHeight);
const navHeight = useSharedValue(0);
const springConfig: WithSpringConfig = {
damping: 50,
mass: 0.3,
stiffness: 120,
overshootClamping: true,
restSpeedThreshold: 0.3,
restDisplacementThreshold: 0.3,
};
const DRAG_BUFFER = 40;
const onGestureEvent = useAnimatedGestureHandler({
// Set the context value to the sheet's current height value
onStart: (_ev, ctx: any) => {
ctx.offsetY = sheetHeight.value;
},
// Update the sheet's height value based on the gesture
onActive: (ev, ctx: any) => {
sheetHeight.value = ctx.offsetY + ev.translationY;
},
// Snap the sheet to the correct position once the gesture ends
onEnd: () => {
// 'worklet' directive is required for animations to work based on shared values
'worklet';
// Snap to expanded position if the sheet is dragged up from minimised position
// or dragged down from maximised position
const shouldExpand =
(position.value === 'maximised' &&
-sheetHeight.value < maxHeight - DRAG_BUFFER) ||
(position.value === 'minimised' &&
-sheetHeight.value > minHeight + DRAG_BUFFER);
// Snap to minimised position if the sheet is dragged down from expanded position
const shouldMinimise =
position.value === 'expanded' &&
-sheetHeight.value < expandedHeight - DRAG_BUFFER;
// Snap to maximised position if the sheet is dragged up from expanded position
const shouldMaximise =
position.value === 'expanded' &&
-sheetHeight.value > expandedHeight + DRAG_BUFFER;
// Update the sheet's position with spring animation
if (shouldExpand) {
navHeight.value = withSpring(0, springConfig);
sheetHeight.value = withSpring(-expandedHeight, springConfig);
position.value = 'expanded';
} else if (shouldMaximise) {
navHeight.value = withSpring(NAV_HEIGHT + 10, springConfig);
sheetHeight.value = withSpring(-maxHeight, springConfig);
position.value = 'maximised';
} else if (shouldMinimise) {
navHeight.value = withSpring(0, springConfig);
sheetHeight.value = withSpring(-minHeight, springConfig);
position.value = 'minimised';
} else {
sheetHeight.value = withSpring(
position.value === 'expanded'
? -expandedHeight
: position.value === 'maximised'
? -maxHeight
: -minHeight,
springConfig,
);
}
},
});
const sheetHeightAnimatedStyle = useAnimatedStyle(() => ({
// The 'worklet' directive is included with useAnimatedStyle hook by default
height: -sheetHeight.value,
}));
const sheetContentAnimatedStyle = useAnimatedStyle(() => ({
paddingBottom: position.value === 'maximised' ? 180 : 0,
paddingTop: position.value === 'maximised' ? 40 : 20,
paddingHorizontal: 20,
}));
const sheetNavigationAnimatedStyle = useAnimatedStyle(() => ({
height: navHeight.value,
overflow: 'hidden',
}));
return (
<View style={styles.container}>
<PanGestureHandler onGestureEvent={onGestureEvent}>
<Animated.View style={[sheetHeightAnimatedStyle, styles.sheet]}>
<View style={styles.handleContainer}>
<View style={styles.handle} />
</View>
<Animated.View style={sheetContentAnimatedStyle}>
<Animated.View style={sheetNavigationAnimatedStyle}>
<TouchableOpacity
style={styles.closeButton}
onPress={() => {
navHeight.value = withSpring(0, springConfig);
sheetHeight.value = withSpring(-expandedHeight, springConfig);
position.value = 'expanded';
}}
>
<Text>{`❌`}</Text>
</TouchableOpacity>
</Animated.View>
<SafeAreaView>
<ScrollView>{props.children}</ScrollView>
</SafeAreaView>
</Animated.View>
</Animated.View>
</PanGestureHandler>
</View>
);
};
const styles = StyleSheet.create({
// The sheet is positioned absolutely to sit at the bottom of the screen
container: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
},
sheet: {
justifyContent: 'flex-start',
backgroundColor: '#FFFFFF',
// Round the top corners
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
minHeight: 80,
// Add a shadow to the top of the sheet
shadowColor: '#000',
shadowOffset: {
width: 0,
height: -2,
},
shadowOpacity: 0.23,
shadowRadius: 2.62,
elevation: 4,
},
handleContainer: {
alignItems: 'center',
justifyContent: 'center',
paddingTop: 10,
},
// Add a small handle component to indicate the sheet can be dragged
handle: {
width: '15%',
height: 4,
borderRadius: 8,
backgroundColor: '#CCCCCC',
},
closeButton: {
width: NAV_HEIGHT,
height: NAV_HEIGHT,
borderRadius: NAV_HEIGHT,
alignItems: 'center',
justifyContent: 'center',
alignSelf: 'flex-start',
marginBottom: 10,
},
});
export default Sheet;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment