Я пытаюсь создать представления с регулируемой высотой с помощью React Native для приложения, которое я создаю. Я продолжаю застревать на этом одном аспекте. Я пытаюсь создать два сложенных представления с линией между ними, чтобы они регулировались по высоте при перетаскивании строки вверх или вниз, а также настраивали содержимое в ней. Изображение ниже является представлением того, что я пытаюсь сделать. «Вариант дома 2» — это состояние по умолчанию, «Вариант дома 1.3» — когда ползунок перетаскивается вниз, а «Вариант дома 1.2» — наоборот — ползунок перетаскивается вверх.
С панелью приложений внизу. (у меня еще не готово)
Любые мысли или помощь приветствуются!
Вот мой код для 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%",
},
});
Код для BottomSheet.tsx (который я использовал в качестве эталона для идеального 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.info(translateY.value);
})
.onEnd(() => {
if (translateY.value > -SCREEN_HEIGHT / 3) {
scrollTo(0);
} else if (translateY.value < -SCREEN_HEIGHT / 1.5) {
scrollTo(MAX_TRANSLATE_Y);
}
console.info('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;
Компонент Bar будет иметь привязанный к нему GestureHandler. Интерполируйте yTranslation в значение от 0 до 1. SharedValue компонента Bar передается как свойство, чтобы другие компоненты в его родительском содержании использовали его:
import {
StyleSheet,
ViewStyle,
Dimensions,
View,
useWindowDimensions,
} from 'react-native';
import Animated, {
SharedValue,
useAnimatedStyle,
interpolate,
withTiming,
} from 'react-native-reanimated';
import { GestureDetector, Gesture } from 'react-native-gesture-handler';
type Props = {
anim: SharedValue<number>;
style?: ViewStyle;
};
const snapPoints = [0.2, 0.5, 0.8];
export default function Bar({ anim, style }: Props) {
const { height } = useWindowDimensions();
const gesture = Gesture.Pan()
.onUpdate((e) => {
// interpolate yTranslation to a value that snapPoints can work with
anim.value = interpolate(
e.translationY,
[-height * 0.5, height * 0.5],
[0, 1]
);
})
// snap to nearest point
.onEnd(() => {
const snapPoint = snapPoints.reduce((prev, curr) => {
const prevDist = Math.abs(prev - anim.value);
const currDist = Math.abs(curr - anim.value);
return prevDist < currDist ? prev : curr;
}, snapPoints[0]);
console.info('snapping to ', snapPoint);
// animate snapping to snapPoint
anim.value = withTiming(snapPoint);
});
return (
<GestureDetector gesture = {gesture}>
<View style = {styles.barContainer}>
<View style = {styles.bar} />
</View>
</GestureDetector>
);
}
const styles = StyleSheet.create({
barContainer: {
backgroundColor: 'transparent',
width: '100%',
//padding to make bar easier to press
padding: 10,
justifyContent: 'center',
},
bar: {
backgroundColor: '#c4c4c4',
width: '80%',
height: 7,
alignSelf: 'center',
borderRadius: 25,
},
});
Теперь, когда translationY представляет собой процент, его можно использовать для определения степени гибкости каждого представления:
import React from 'react';
import {
View,
StyleSheet,
} from 'react-native';
import Constants from 'expo-constants';
import Animated, {
useSharedValue,
useAnimatedStyle,
} from 'react-native-reanimated';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import SliderBar from './SliderBar';
import View1 from './View1';
import View2 from './View2';
import { footerHeight, ScreenWidth, ScreenHeight, MAX_FLEX } from './Constants';
export default function App() {
const barValue = useSharedValue(0.5);
const view1Style = useAnimatedStyle(() => {
return {
flex: barValue.value * MAX_FLEX,
};
});
const view2Style = useAnimatedStyle(() => {
return {
flex: Math.abs(barValue.value - 1) * MAX_FLEX,
};
});
return (
<GestureHandlerRootView
style = {{ width: ScreenWidth, height: ScreenHeight }}>
<View style = {styles.container}>
<Animated.View style = {[styles.viewStyle, view1Style]}>
<View1 />
</Animated.View>
<SliderBar anim = {barValue} />
<Animated.View style = {[styles.viewStyle, view2Style]}>
<View2 />
</Animated.View>
<View style = {styles.footer} />
</View>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
padding: 8,
margin: 5,
},
viewStyle: {
backgroundColor: '#c4c4c4',
flex: 1,
marginVertical: 10,
borderRadius: 10,
},
footer: {
backgroundColor: '#6f6f6f',
height: footerHeight,
borderRadius: 10,
},
});
Я предполагаю, что это должна быть интерполяция. Ожидается, что перетаскивание будет между половиной screenHeight (положительное и отрицательное) и сопоставляет его со значением от 0 до 1. Поэтому, когда yTranslation действительно мало, оно будет интерполировано до значения, близкого к 0,5. Я думаю, что смещение, как они делают здесь, может исправить это
Отлично! Однако у меня есть вопрос: я заметил, что когда я нажимаю/перетаскиваю линию после того, как переместил ее в положение привязки, линия возвращается к середине. Есть мысли, почему?