I am trying to create Height adjustable Views with React Native for an app I am building. I keep getting stuck on this one aspect. I am trying to create two stacked Views, with a line inbetween them so they are height adjustable when dragging the line up or down, adjusting content in it as well. Image below is a representation of what I am trying to make. "Home Option 2" is default state, "Home Option 1.3 is when the slider is dragged down, and "Home Option 1.2" is opposite - slider dragged up.
With an app bar at the bottom. (I dont have it made yet)
Here is my code for App.tsx
import * as React from 'react';import { GestureHandlerRootView } from 'react-native-gesture-handler';import { StatusBar } from 'expo-status-bar';import { StyleSheet, TouchableOpacity, View } from 'react-native';import BottomSheet, { BottomSheetRefProps } from './components/BottomSheet';import { useCallback, useRef } from 'react';import MapView, { Marker, Geojson } from "react-native-maps";import { PROVIDER_GOOGLE } from "react-native-maps";export default function App() { const ref = useRef<BottomSheetRefProps>(null); const [topViewHeight, setTopViewHeight] = React.useState(0); const onPress = useCallback(() => { const isActive = ref?.current?.isActive(); if (isActive) { ref?.current?.scrollTo(0); } else { ref?.current?.scrollTo(-200); } }, []); return (<GestureHandlerRootView style={{ flex: 1 }}><View style={styles.mapViewContainer}><MapView provider={PROVIDER_GOOGLE} showsUserLocation={true} style={styles.mapView} initialRegion={{ latitude: 00.00 , longitude: -00.00 , latitudeDelta: 00.00 , longitudeDelta: 00.00 , }}><Marker coordinate={{ latitude: 00.00, longitude: 00.00 }} /></MapView></View><View style={styles.container}><StatusBar style="light" /><TouchableOpacity style={styles.button} onPress={onPress} /><BottomSheet ref={ref} {...{setTopViewHeight, topViewHeight}}><View style={{ flex: 1, backgroundColor: 'orange' }} /></BottomSheet></View></GestureHandlerRootView> );}const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#111', alignItems: 'center', justifyContent: 'center', }, button: { height: 50, borderRadius: 25, aspectRatio: 1, backgroundColor: 'white', opacity: 0.6, }, mapViewContainer: { height: "50%", width: "95%", overflow: "hidden", background: "transparent", borderRadius: 13, }, mapView: { height: "100%", width: "100%", },});
Code for BottomSheet.tsx (Which i was using as a reference for the ideal UX)
import { Dimensions, StyleSheet, Text, View } from 'react-native';import React, { useCallback, useEffect, useImperativeHandle } from 'react';import { Gesture, GestureDetector } from 'react-native-gesture-handler';import Animated, { Extrapolate, interpolate, useAnimatedStyle, useSharedValue, withSpring, withTiming,} from 'react-native-reanimated';const { height: SCREEN_HEIGHT } = Dimensions.get('window');const TOP_VIEW_HEIGHT = 50;const VIEW_RESIZE = 2.5;const MAX_TRANSLATE_Y = -SCREEN_HEIGHT / VIEW_RESIZE;type BottomSheetProps = { children?: React.ReactNode; setTopViewHeight: (height: number) => void; topViewHeight: number;};export type BottomSheetRefProps = { scrollTo: (destination: number) => void; isActive: () => boolean;};const BottomSheet = React.forwardRef<BottomSheetRefProps, BottomSheetProps>( ({ children }, ref) => { const translateY = useSharedValue(0); const active = useSharedValue(false); const scrollTo = useCallback((destination: number) => {'worklet'; active.value = destination !== 0; translateY.value = withSpring(destination, { damping: 50 }); }, []); const isActive = useCallback(() => { return active.value; }, []); useImperativeHandle(ref, () => ({ scrollTo, isActive }), [ scrollTo, isActive, ]); 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 = Math.max(translateY.value, MAX_TRANSLATE_Y); console.log(translateY.value); }) .onEnd(() => { if (translateY.value > -SCREEN_HEIGHT / 3) { scrollTo(0); } else if (translateY.value < -SCREEN_HEIGHT / 1.5) { scrollTo(MAX_TRANSLATE_Y); } console.log('end: '+ translateY.value) }); const rBottomSheetStyle = useAnimatedStyle(() => { const borderRadius = interpolate( translateY.value, [MAX_TRANSLATE_Y + 50, MAX_TRANSLATE_Y], [25, 5], Extrapolate.CLAMP ); return { borderRadius, transform: [{ translateY: translateY.value }], maxHeight: 500, }; }); return (<GestureDetector gesture={gesture}><Animated.View style={[styles.bottomSheetContainer, rBottomSheetStyle] }><View style={styles.line} /> {children}</Animated.View></GestureDetector> ); });const styles = StyleSheet.create({ bottomSheetContainer: { minHeight: SCREEN_HEIGHT - 50, width: '100%', backgroundColor: 'white', position: 'relative', top: SCREEN_HEIGHT - 500, borderRadius: 25, }, line: { width: 75, height: 4, backgroundColor: 'grey', alignSelf: 'center', marginVertical: 15, borderRadius: 2, },});export default BottomSheet;