|
@@ -0,0 +1,851 @@
|
|
|
|
|
+import {
|
|
|
|
|
+ useImperativeHandle,
|
|
|
|
|
+ type ComponentRef,
|
|
|
|
|
+ forwardRef,
|
|
|
|
|
+ useEffect,
|
|
|
|
|
+ useState,
|
|
|
|
|
+ useRef
|
|
|
|
|
+} from "react";
|
|
|
|
|
+import {
|
|
|
|
|
+ type LayoutChangeEvent,
|
|
|
|
|
+ PanResponder,
|
|
|
|
|
+ ScrollView,
|
|
|
|
|
+ Animated,
|
|
|
|
|
+ Easing,
|
|
|
|
|
+ View
|
|
|
|
|
+} from "react-native";
|
|
|
|
|
+import type IBottomSheetProps from "./type";
|
|
|
|
|
+import type {
|
|
|
|
|
+ IBottomSheetRef
|
|
|
|
|
+} from "./type";
|
|
|
|
|
+import stylesheet from "./stylesheet";
|
|
|
|
|
+import {
|
|
|
|
|
+ NCoreUIKitTheme
|
|
|
|
|
+} from "../../core/hooks";
|
|
|
|
|
+import {
|
|
|
|
|
+ useSafeAreaInsets
|
|
|
|
|
+} from "react-native-safe-area-context";
|
|
|
|
|
+import type {
|
|
|
|
|
+ RefForwardingComponent
|
|
|
|
|
+} from "../../types";
|
|
|
|
|
+import type {
|
|
|
|
|
+ IModalRef
|
|
|
|
|
+} from "../modal/type";
|
|
|
|
|
+import {
|
|
|
|
|
+ windowHeight,
|
|
|
|
|
+ uuid
|
|
|
|
|
+} from "../../utils";
|
|
|
|
|
+import Modal from "../modal";
|
|
|
|
|
+
|
|
|
|
|
+const BottomSheet: RefForwardingComponent<IBottomSheetRef, IBottomSheetProps> = ({
|
|
|
|
|
+ renderHeader: RenderHeaderComponent,
|
|
|
|
|
+ renderBottom: RenderBottomComponent,
|
|
|
|
|
+ isForceFullScreenOnSwipe = false,
|
|
|
|
|
+ isCanFullScreenOnSwipe = false,
|
|
|
|
|
+ handleContainerBackgroundColor,
|
|
|
|
|
+ handleHeight: handleHeightProp,
|
|
|
|
|
+ isWrapSafeAreaContext = true,
|
|
|
|
|
+ backgroundColor = "default",
|
|
|
|
|
+ isWorkAsFullScreen = false,
|
|
|
|
|
+ scrollEndThreshold = 0.85,
|
|
|
|
|
+ isWorkWithPortal = true,
|
|
|
|
|
+ isCloseOnOverlay = true,
|
|
|
|
|
+ handleContainerSpacing,
|
|
|
|
|
+ isActive: isActiveProp,
|
|
|
|
|
+ handleBackgroundColor,
|
|
|
|
|
+ isAutoHeight = false,
|
|
|
|
|
+ isSwipeClose = true,
|
|
|
|
|
+ isShowHandle = true,
|
|
|
|
|
+ isCanSwipe = true,
|
|
|
|
|
+ onOverlayPressed,
|
|
|
|
|
+ scrollViewProps,
|
|
|
|
|
+ scrollViewStyle,
|
|
|
|
|
+ onScrollEnd,
|
|
|
|
|
+ customTheme,
|
|
|
|
|
+ modalProps,
|
|
|
|
|
+ snapPoint,
|
|
|
|
|
+ customKey,
|
|
|
|
|
+ children,
|
|
|
|
|
+ onClosed,
|
|
|
|
|
+ onOpened,
|
|
|
|
|
+ onClose,
|
|
|
|
|
+ onOpen,
|
|
|
|
|
+ style,
|
|
|
|
|
+ ...props
|
|
|
|
|
+}, ref) => {
|
|
|
|
|
+ const {
|
|
|
|
|
+ colors,
|
|
|
|
|
+ spaces
|
|
|
|
|
+ } = NCoreUIKitTheme.useContext(customTheme);
|
|
|
|
|
+
|
|
|
|
|
+ const {
|
|
|
|
|
+ bottom,
|
|
|
|
|
+ top
|
|
|
|
|
+ } = useSafeAreaInsets();
|
|
|
|
|
+
|
|
|
|
|
+ const [
|
|
|
|
|
+ isMeasured,
|
|
|
|
|
+ setIsMeasured
|
|
|
|
|
+ ] = useState(false);
|
|
|
|
|
+
|
|
|
|
|
+ const [
|
|
|
|
|
+ isActive,
|
|
|
|
|
+ setIsActive
|
|
|
|
|
+ ] = useState(isActiveProp === undefined ? false : isActiveProp);
|
|
|
|
|
+
|
|
|
|
|
+ const bottomSheetKey = useRef(customKey ? customKey : uuid());
|
|
|
|
|
+
|
|
|
|
|
+ let bottomSafeArea = isWrapSafeAreaContext ? bottom : 0;
|
|
|
|
|
+ let topSafeArea = isWrapSafeAreaContext ? top : 0;
|
|
|
|
|
+
|
|
|
|
|
+ if(isForceFullScreenOnSwipe) {
|
|
|
|
|
+ topSafeArea = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if(!isWorkWithPortal) {
|
|
|
|
|
+ bottomSafeArea = 0;
|
|
|
|
|
+ topSafeArea = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const scrollViewRef = useRef<ComponentRef<ScrollView>>(null);
|
|
|
|
|
+ const modalRef = useRef<IModalRef>(null);
|
|
|
|
|
+
|
|
|
|
|
+ const containerHeightRef = useRef(windowHeight);
|
|
|
|
|
+
|
|
|
|
|
+ const animatedTranslateY = useRef(new Animated.Value(snapPoint && !isWorkAsFullScreen ? snapPoint : containerHeightRef.current)).current;
|
|
|
|
|
+ const animatedHeight = useRef(new Animated.Value(
|
|
|
|
|
+ isAutoHeight ? 0 : snapPoint ?? 0
|
|
|
|
|
+ )).current;
|
|
|
|
|
+
|
|
|
|
|
+ const TOP_GRAB_AREA = 140;
|
|
|
|
|
+
|
|
|
|
|
+ const maxHeight = useRef(isWorkAsFullScreen ? containerHeightRef.current - (isWrapSafeAreaContext ? topSafeArea : 0) : containerHeightRef.current - (isForceFullScreenOnSwipe ? 0 : isWrapSafeAreaContext ? topSafeArea : 0));
|
|
|
|
|
+ const heightValue = useRef(isWorkAsFullScreen ? containerHeightRef.current : snapPoint ?? 0);
|
|
|
|
|
+ const initialTranslateY = useRef(0);
|
|
|
|
|
+ const translateYValue = useRef(0);
|
|
|
|
|
+ const contentHeight = useRef(-1);
|
|
|
|
|
+ const initialHeight = useRef(0);
|
|
|
|
|
+
|
|
|
|
|
+ const scrollViewContentHeight = useRef(-1);
|
|
|
|
|
+ const scrollViewLayoutHeight = useRef(-1);
|
|
|
|
|
+ const initialScrollOffset = useRef(0);
|
|
|
|
|
+ const scrollOffset = useRef(0);
|
|
|
|
|
+
|
|
|
|
|
+ const gestureStartY = useRef(0);
|
|
|
|
|
+
|
|
|
|
|
+ const isCanFullScreenOnSwipeRef = useRef(isCanFullScreenOnSwipe);
|
|
|
|
|
+ const isWorkAsFullScreenRef = useRef(isWorkAsFullScreen);
|
|
|
|
|
+ const isSwipeCloseRef = useRef(isSwipeClose);
|
|
|
|
|
+ const isCanSwipeRef = useRef(isCanSwipe);
|
|
|
|
|
+
|
|
|
|
|
+ if(!isWorkAsFullScreen && !isCanFullScreenOnSwipe && snapPoint) {
|
|
|
|
|
+ maxHeight.current = snapPoint;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ useImperativeHandle(
|
|
|
|
|
+ ref,
|
|
|
|
|
+ () => ({
|
|
|
|
|
+ close: (callback) => {
|
|
|
|
|
+ closeAnimation(undefined, callback);
|
|
|
|
|
+ },
|
|
|
|
|
+ open: () => {
|
|
|
|
|
+ setIsActive(true);
|
|
|
|
|
+ }
|
|
|
|
|
+ }),
|
|
|
|
|
+ []
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ isCanSwipeRef.current = isCanSwipe;
|
|
|
|
|
+ }, [isCanSwipe]);
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ isSwipeCloseRef.current = isSwipeClose;
|
|
|
|
|
+ }, [isSwipeClose]);
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ isCanFullScreenOnSwipeRef.current = isCanFullScreenOnSwipe;
|
|
|
|
|
+ }, [isCanFullScreenOnSwipe]);
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ isWorkAsFullScreenRef.current = isWorkAsFullScreen;
|
|
|
|
|
+ }, [isWorkAsFullScreen]);
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ if(isMeasured && contentHeight.current !== -1) {
|
|
|
|
|
+ if(isCanFullScreenOnSwipe && !isWorkAsFullScreen) {
|
|
|
|
|
+ maxHeight.current = containerHeightRef.current - (isForceFullScreenOnSwipe ? 0 : topSafeArea);
|
|
|
|
|
+ } else if(isAutoHeight || !snapPoint) {
|
|
|
|
|
+ maxHeight.current = contentHeight.current;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [isMeasured]);
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ if(isActive && isMeasured) {
|
|
|
|
|
+ if(!isWorkAsFullScreen && !isCanFullScreenOnSwipe && snapPoint) {
|
|
|
|
|
+ maxHeight.current = snapPoint;
|
|
|
|
|
+ } else if(isWorkAsFullScreen) {
|
|
|
|
|
+ maxHeight.current = containerHeightRef.current - (isWrapSafeAreaContext ? topSafeArea : 0);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ maxHeight.current = containerHeightRef.current - (isForceFullScreenOnSwipe ? 0 : isWrapSafeAreaContext ? topSafeArea : 0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ openAnimation();
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [
|
|
|
|
|
+ isActive,
|
|
|
|
|
+ isMeasured
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ if(isActiveProp !== undefined) {
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ setIsActive(isActiveProp);
|
|
|
|
|
+ }, [isActiveProp]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ if (!isWorkAsFullScreen && !isCanFullScreenOnSwipe && snapPoint) {
|
|
|
|
|
+ maxHeight.current = snapPoint;
|
|
|
|
|
+ } else if (!isWorkAsFullScreen) {
|
|
|
|
|
+ maxHeight.current = containerHeightRef.current - (isForceFullScreenOnSwipe ? 0 : isWrapSafeAreaContext ? topSafeArea : 0);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ maxHeight.current = containerHeightRef.current - (isWrapSafeAreaContext ? topSafeArea : 0);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [
|
|
|
|
|
+ isForceFullScreenOnSwipe,
|
|
|
|
|
+ isCanFullScreenOnSwipe,
|
|
|
|
|
+ isWrapSafeAreaContext,
|
|
|
|
|
+ isWorkAsFullScreen,
|
|
|
|
|
+ topSafeArea,
|
|
|
|
|
+ snapPoint
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ if (!isActive) {
|
|
|
|
|
+ const newSnapValue = isWorkAsFullScreen
|
|
|
|
|
+ ? containerHeightRef.current
|
|
|
|
|
+ : (snapPoint ?? contentHeight.current);
|
|
|
|
|
+
|
|
|
|
|
+ animatedHeight.setValue(isAutoHeight ? 0 : newSnapValue);
|
|
|
|
|
+ animatedTranslateY.setValue(newSnapValue);
|
|
|
|
|
+
|
|
|
|
|
+ heightValue.current = newSnapValue;
|
|
|
|
|
+ translateYValue.current = newSnapValue;
|
|
|
|
|
+ initialHeight.current = newSnapValue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!isWorkAsFullScreen && !isCanFullScreenOnSwipe && snapPoint) {
|
|
|
|
|
+ maxHeight.current = snapPoint;
|
|
|
|
|
+ } else if (!isWorkAsFullScreen) {
|
|
|
|
|
+ maxHeight.current = containerHeightRef.current - (
|
|
|
|
|
+ isForceFullScreenOnSwipe ? 0 : isWrapSafeAreaContext ? topSafeArea : 0
|
|
|
|
|
+ );
|
|
|
|
|
+ } else {
|
|
|
|
|
+ maxHeight.current = containerHeightRef.current - (
|
|
|
|
|
+ isWrapSafeAreaContext ? topSafeArea : 0
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [
|
|
|
|
|
+ snapPoint,
|
|
|
|
|
+ isWorkAsFullScreen
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ const listenerAHeightId = animatedHeight.addListener(({
|
|
|
|
|
+ value
|
|
|
|
|
+ }) => {
|
|
|
|
|
+ heightValue.current = value;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const listenerTYId = animatedTranslateY.addListener(({
|
|
|
|
|
+ value
|
|
|
|
|
+ }) => {
|
|
|
|
|
+ translateYValue.current = value;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return () => {
|
|
|
|
|
+ animatedHeight.removeListener(listenerAHeightId);
|
|
|
|
|
+ animatedTranslateY.removeListener(listenerTYId);
|
|
|
|
|
+ };
|
|
|
|
|
+ }, []);
|
|
|
|
|
+
|
|
|
|
|
+ const checkScrollThreshold = () => {
|
|
|
|
|
+ if (!onScrollEnd) return;
|
|
|
|
|
+
|
|
|
|
|
+ const maxS = Math.max(0, scrollViewContentHeight.current - scrollViewLayoutHeight.current);
|
|
|
|
|
+
|
|
|
|
|
+ if (maxS <= 0) return;
|
|
|
|
|
+
|
|
|
|
|
+ const ratio = scrollOffset.current / maxS;
|
|
|
|
|
+
|
|
|
|
|
+ if (ratio >= scrollEndThreshold) {
|
|
|
|
|
+ onScrollEnd();
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const openAnimation = () => {
|
|
|
|
|
+ resetState();
|
|
|
|
|
+
|
|
|
|
|
+ if(onOpen) onOpen();
|
|
|
|
|
+
|
|
|
|
|
+ Animated.timing(animatedTranslateY, {
|
|
|
|
|
+ useNativeDriver: false,
|
|
|
|
|
+ duration: 300,
|
|
|
|
|
+ toValue: 0
|
|
|
|
|
+ }).start(({
|
|
|
|
|
+ finished
|
|
|
|
|
+ }) => {
|
|
|
|
|
+ if(finished) {
|
|
|
|
|
+ if(onOpened) onOpened();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const closeAnimation = (toValue?: number, callback?: () => void) => {
|
|
|
|
|
+ if(onClose) onClose();
|
|
|
|
|
+
|
|
|
|
|
+ const currentSnapPoint = isWorkAsFullScreen
|
|
|
|
|
+ ? containerHeightRef.current
|
|
|
|
|
+ : (isAutoHeight || !snapPoint ? contentHeight.current : snapPoint);
|
|
|
|
|
+
|
|
|
|
|
+ Animated.timing(animatedTranslateY, {
|
|
|
|
|
+ toValue: toValue ?? currentSnapPoint,
|
|
|
|
|
+ useNativeDriver: false,
|
|
|
|
|
+ duration: 300
|
|
|
|
|
+ }).start(({
|
|
|
|
|
+ finished
|
|
|
|
|
+ }) => {
|
|
|
|
|
+ if(finished) {
|
|
|
|
|
+ resetState();
|
|
|
|
|
+
|
|
|
|
|
+ setIsActive(false);
|
|
|
|
|
+
|
|
|
|
|
+ if(onClosed) onClosed();
|
|
|
|
|
+ if(callback) callback();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const onLayout = (event: LayoutChangeEvent) => {
|
|
|
|
|
+ if (isMeasured) return;
|
|
|
|
|
+
|
|
|
|
|
+ const {
|
|
|
|
|
+ height
|
|
|
|
|
+ } = event.nativeEvent.layout;
|
|
|
|
|
+
|
|
|
|
|
+ animatedHeight.setValue(height);
|
|
|
|
|
+
|
|
|
|
|
+ contentHeight.current = height;
|
|
|
|
|
+
|
|
|
|
|
+ setIsMeasured(true);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const resetState = () => {
|
|
|
|
|
+ animatedHeight.setOffset(0);
|
|
|
|
|
+ animatedTranslateY.setOffset(0);
|
|
|
|
|
+
|
|
|
|
|
+ scrollOffset.current = 0;
|
|
|
|
|
+ initialScrollOffset.current = 0;
|
|
|
|
|
+
|
|
|
|
|
+ const pivot = isWorkAsFullScreen
|
|
|
|
|
+ ? containerHeightRef.current
|
|
|
|
|
+ : (snapPoint ?? contentHeight.current);
|
|
|
|
|
+
|
|
|
|
|
+ initialHeight.current = pivot;
|
|
|
|
|
+ initialTranslateY.current = 0;
|
|
|
|
|
+
|
|
|
|
|
+ heightValue.current = pivot;
|
|
|
|
|
+ translateYValue.current = 0;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const panResponder = useRef(
|
|
|
|
|
+ PanResponder.create({
|
|
|
|
|
+ onStartShouldSetPanResponder: () => false,
|
|
|
|
|
+ onMoveShouldSetPanResponderCapture: (_, gestureState) => {
|
|
|
|
|
+ const {
|
|
|
|
|
+ dy
|
|
|
|
|
+ } = gestureState;
|
|
|
|
|
+
|
|
|
|
|
+ if(!isCanSwipeRef.current) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if(Math.abs(dy) < 20) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return true;
|
|
|
|
|
+ },
|
|
|
|
|
+ onPanResponderGrant: (evt) => {
|
|
|
|
|
+ if(!isCanSwipeRef.current) return;
|
|
|
|
|
+
|
|
|
|
|
+ gestureStartY.current = evt.nativeEvent.pageY;
|
|
|
|
|
+
|
|
|
|
|
+ animatedTranslateY.stopAnimation((currentY) => {
|
|
|
|
|
+ translateYValue.current = currentY;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ animatedHeight.stopAnimation((currentH) => {
|
|
|
|
|
+ heightValue.current = currentH;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ animatedTranslateY.flattenOffset();
|
|
|
|
|
+ animatedHeight.flattenOffset();
|
|
|
|
|
+
|
|
|
|
|
+ initialTranslateY.current = translateYValue.current;
|
|
|
|
|
+ initialScrollOffset.current = scrollOffset.current;
|
|
|
|
|
+ initialHeight.current = heightValue.current;
|
|
|
|
|
+
|
|
|
|
|
+ animatedHeight.setOffset(heightValue.current);
|
|
|
|
|
+ animatedHeight.setValue(0);
|
|
|
|
|
+
|
|
|
|
|
+ animatedTranslateY.setOffset(translateYValue.current);
|
|
|
|
|
+ animatedTranslateY.setValue(0);
|
|
|
|
|
+ },
|
|
|
|
|
+ onPanResponderMove: (_, gestureState) => {
|
|
|
|
|
+ if(!isCanSwipeRef.current) return;
|
|
|
|
|
+
|
|
|
|
|
+ const {
|
|
|
|
|
+ dy
|
|
|
|
|
+ } = gestureState;
|
|
|
|
|
+
|
|
|
|
|
+ const isAtTop = scrollOffset.current <= 0;
|
|
|
|
|
+ const isAtTavan = heightValue.current >= maxHeight.current - 1;
|
|
|
|
|
+ const hasScroll = scrollViewContentHeight.current > scrollViewLayoutHeight.current;
|
|
|
|
|
+
|
|
|
|
|
+ const isFromTopArea = gestureStartY.current < TOP_GRAB_AREA;
|
|
|
|
|
+
|
|
|
|
|
+ if (dy > 0 && isAtTavan && hasScroll && !isAtTop && !isFromTopArea) {
|
|
|
|
|
+ const currentDelta = -dy;
|
|
|
|
|
+
|
|
|
|
|
+ const initialS = initialScrollOffset.current;
|
|
|
|
|
+
|
|
|
|
|
+ const usedForS = Math.max(currentDelta, -initialS);
|
|
|
|
|
+
|
|
|
|
|
+ scrollOffset.current = initialS + usedForS;
|
|
|
|
|
+
|
|
|
|
|
+ scrollViewRef.current?.scrollTo({
|
|
|
|
|
+ y: scrollOffset.current,
|
|
|
|
|
+ animated: false
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const delta = -dy;
|
|
|
|
|
+
|
|
|
|
|
+ const pivot = snapPoint ?? contentHeight.current;
|
|
|
|
|
+
|
|
|
|
|
+ const initialH = initialHeight.current;
|
|
|
|
|
+ const initialS = initialScrollOffset.current;
|
|
|
|
|
+ const initialT = initialTranslateY.current;
|
|
|
|
|
+
|
|
|
|
|
+ const maxS = Math.max(0, scrollViewContentHeight.current - scrollViewLayoutHeight.current);
|
|
|
|
|
+
|
|
|
|
|
+ if (dy < 0) {
|
|
|
|
|
+ let currentDelta = delta;
|
|
|
|
|
+
|
|
|
|
|
+ const effectiveInitialT = Math.max(0, initialT);
|
|
|
|
|
+ const usedForT = Math.min(currentDelta, effectiveInitialT);
|
|
|
|
|
+
|
|
|
|
|
+ animatedTranslateY.setValue(-usedForT);
|
|
|
|
|
+ currentDelta -= usedForT;
|
|
|
|
|
+
|
|
|
|
|
+ if (currentDelta > 0) {
|
|
|
|
|
+ if (initialH < pivot) {
|
|
|
|
|
+ const spaceToPivot = pivot - initialH;
|
|
|
|
|
+ const usedForH = Math.min(currentDelta, spaceToPivot);
|
|
|
|
|
+
|
|
|
|
|
+ animatedHeight.setValue(usedForH);
|
|
|
|
|
+
|
|
|
|
|
+ currentDelta -= usedForH;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const isRestoringHeight = initialH < pivot;
|
|
|
|
|
+ const canScroll = isCanFullScreenOnSwipeRef.current || isWorkAsFullScreenRef.current || !isRestoringHeight;
|
|
|
|
|
+
|
|
|
|
|
+ if (currentDelta > 0 && canScroll) {
|
|
|
|
|
+ const remainingScroll = maxS - initialS;
|
|
|
|
|
+ if (remainingScroll > 0) {
|
|
|
|
|
+ const usedForS = Math.min(currentDelta, remainingScroll);
|
|
|
|
|
+ scrollOffset.current = initialS + usedForS;
|
|
|
|
|
+
|
|
|
|
|
+ scrollViewRef.current?.scrollTo({
|
|
|
|
|
+ y: scrollOffset.current,
|
|
|
|
|
+ animated: false
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ currentDelta -= usedForS;
|
|
|
|
|
+
|
|
|
|
|
+ animatedHeight.setValue(initialH < pivot ? (pivot - initialH) : 0);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (currentDelta > 0) {
|
|
|
|
|
+ if (isCanFullScreenOnSwipeRef.current) {
|
|
|
|
|
+ const totalUsedBefore = (initialH < pivot ? (pivot - initialH) : 0);
|
|
|
|
|
+
|
|
|
|
|
+ animatedHeight.setValue(totalUsedBefore + currentDelta);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ animatedHeight.setValue(initialH < pivot ? (pivot - initialH) : 0);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ let currentDelta = delta;
|
|
|
|
|
+
|
|
|
|
|
+ if (initialH > pivot || initialS > 0) {
|
|
|
|
|
+ if (initialS > 0) {
|
|
|
|
|
+ const usedForS = Math.max(currentDelta, -initialS);
|
|
|
|
|
+ scrollOffset.current = initialS + usedForS;
|
|
|
|
|
+
|
|
|
|
|
+ scrollViewRef.current?.scrollTo({
|
|
|
|
|
+ y: scrollOffset.current,
|
|
|
|
|
+ animated: false
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ currentDelta -= usedForS;
|
|
|
|
|
+
|
|
|
|
|
+ animatedHeight.setValue(initialH > pivot ? 0 : pivot - initialH);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (currentDelta < 0 && initialH > pivot) {
|
|
|
|
|
+ const distanceToPivot = pivot - initialH;
|
|
|
|
|
+ const usedForH = Math.max(currentDelta, distanceToPivot);
|
|
|
|
|
+
|
|
|
|
|
+ animatedHeight.setValue(usedForH);
|
|
|
|
|
+
|
|
|
|
|
+ currentDelta -= usedForH;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (currentDelta < 0) {
|
|
|
|
|
+ animatedHeight.setValue(initialH > pivot ? (pivot - initialH) : 0);
|
|
|
|
|
+ animatedTranslateY.setValue(-currentDelta);
|
|
|
|
|
+
|
|
|
|
|
+ scrollOffset.current = 0;
|
|
|
|
|
+
|
|
|
|
|
+ scrollViewRef.current?.scrollTo({
|
|
|
|
|
+ y: 0,
|
|
|
|
|
+ animated: false
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ onPanResponderEnd: (_, gestureState) => {
|
|
|
|
|
+ if(!isCanSwipeRef.current) return;
|
|
|
|
|
+
|
|
|
|
|
+ const isAtTop = scrollOffset.current <= 1;
|
|
|
|
|
+ const isAtTavan = heightValue.current >= maxHeight.current - 1;
|
|
|
|
|
+ const hasScroll = scrollViewContentHeight.current > scrollViewLayoutHeight.current;
|
|
|
|
|
+
|
|
|
|
|
+ const isFromTopArea = gestureStartY.current < TOP_GRAB_AREA;
|
|
|
|
|
+
|
|
|
|
|
+ if (isAtTavan && hasScroll && !isAtTop && !isFromTopArea) {
|
|
|
|
|
+ const velocity = gestureState.vy;
|
|
|
|
|
+
|
|
|
|
|
+ if (Math.abs(velocity) > 0.1) {
|
|
|
|
|
+ const momentum = velocity * -350;
|
|
|
|
|
+
|
|
|
|
|
+ const maxS = Math.max(0, scrollViewContentHeight.current - scrollViewLayoutHeight.current);
|
|
|
|
|
+
|
|
|
|
|
+ const targetOffset = Math.max(0, Math.min(scrollOffset.current + momentum, maxS));
|
|
|
|
|
+
|
|
|
|
|
+ const scrollAnim = new Animated.Value(scrollOffset.current);
|
|
|
|
|
+
|
|
|
|
|
+ scrollAnim.addListener(({
|
|
|
|
|
+ value
|
|
|
|
|
+ }) => {
|
|
|
|
|
+ scrollViewRef.current?.scrollTo({
|
|
|
|
|
+ y: value,
|
|
|
|
|
+ animated: false
|
|
|
|
|
+ });
|
|
|
|
|
+ scrollOffset.current = value;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ Animated.timing(scrollAnim, {
|
|
|
|
|
+ duration: Math.min(Math.abs(momentum) * 2, 600),
|
|
|
|
|
+ easing: Easing.out(Easing.quad),
|
|
|
|
|
+ useNativeDriver: false,
|
|
|
|
|
+ toValue: targetOffset
|
|
|
|
|
+ }).start(() => {
|
|
|
|
|
+ scrollAnim.removeAllListeners();
|
|
|
|
|
+
|
|
|
|
|
+ checkScrollThreshold();
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ checkScrollThreshold();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const currentH = (animatedHeight as Animated.Value & {
|
|
|
|
|
+ __getValue(): number;
|
|
|
|
|
+ }).__getValue();
|
|
|
|
|
+
|
|
|
|
|
+ const currentT = (animatedTranslateY as Animated.Value & {
|
|
|
|
|
+ __getValue(): number;
|
|
|
|
|
+ }).__getValue();
|
|
|
|
|
+
|
|
|
|
|
+ animatedHeight.flattenOffset();
|
|
|
|
|
+ animatedTranslateY.flattenOffset();
|
|
|
|
|
+
|
|
|
|
|
+ heightValue.current = currentH;
|
|
|
|
|
+ translateYValue.current = currentT;
|
|
|
|
|
+
|
|
|
|
|
+ const pivot = snapPoint ?? contentHeight.current;
|
|
|
|
|
+ const tavan = maxHeight.current;
|
|
|
|
|
+
|
|
|
|
|
+ const isFastSwipeDown = gestureState.vy > 0.5;
|
|
|
|
|
+ const isFastSwipeUp = gestureState.vy < -0.5;
|
|
|
|
|
+
|
|
|
|
|
+ if (currentT <= Math.max(pivot * 0.2, 60)) {
|
|
|
|
|
+ let toValue = pivot;
|
|
|
|
|
+
|
|
|
|
|
+ if (isFastSwipeUp && isCanFullScreenOnSwipeRef.current) {
|
|
|
|
|
+ toValue = tavan;
|
|
|
|
|
+ } else if (isFastSwipeDown) {
|
|
|
|
|
+ toValue = pivot;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (!isCanFullScreenOnSwipeRef.current) {
|
|
|
|
|
+ toValue = pivot;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const totalRange = tavan - pivot;
|
|
|
|
|
+ const distFromPivot = currentH - pivot;
|
|
|
|
|
+
|
|
|
|
|
+ if (distFromPivot < totalRange * 0.33) {
|
|
|
|
|
+ toValue = pivot;
|
|
|
|
|
+ } else if (distFromPivot > totalRange * 0.66) {
|
|
|
|
|
+ toValue = tavan;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ toValue = (tavan - currentH) < (currentH - pivot) ? tavan : pivot;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Animated.spring(animatedHeight, {
|
|
|
|
|
+ useNativeDriver: false,
|
|
|
|
|
+ toValue: toValue,
|
|
|
|
|
+ friction: 10,
|
|
|
|
|
+ tension: 40
|
|
|
|
|
+ }).start();
|
|
|
|
|
+
|
|
|
|
|
+ Animated.spring(animatedTranslateY, {
|
|
|
|
|
+ useNativeDriver: false,
|
|
|
|
|
+ toValue: 0,
|
|
|
|
|
+ friction: 10,
|
|
|
|
|
+ tension: 40
|
|
|
|
|
+ }).start();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ let toValueT = 0;
|
|
|
|
|
+ let isClosing = false;
|
|
|
|
|
+
|
|
|
|
|
+ if (!isSwipeCloseRef.current) {
|
|
|
|
|
+ Animated.spring(animatedHeight, {
|
|
|
|
|
+ useNativeDriver: false,
|
|
|
|
|
+ toValue: pivot,
|
|
|
|
|
+ friction: 10,
|
|
|
|
|
+ tension: 40
|
|
|
|
|
+ }).start();
|
|
|
|
|
+
|
|
|
|
|
+ Animated.timing(animatedTranslateY, {
|
|
|
|
|
+ useNativeDriver: false,
|
|
|
|
|
+ duration: 300,
|
|
|
|
|
+ toValue: 0
|
|
|
|
|
+ }).start();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (isFastSwipeDown) {
|
|
|
|
|
+ toValueT = pivot + 100;
|
|
|
|
|
+
|
|
|
|
|
+ isClosing = true;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (currentT > pivot * 0.5) {
|
|
|
|
|
+ toValueT = pivot + 100;
|
|
|
|
|
+
|
|
|
|
|
+ isClosing = true;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ toValueT = 0;
|
|
|
|
|
+
|
|
|
|
|
+ isClosing = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if(onClose) onClose();
|
|
|
|
|
+
|
|
|
|
|
+ Animated.timing(animatedTranslateY, {
|
|
|
|
|
+ useNativeDriver: false,
|
|
|
|
|
+ toValue: toValueT,
|
|
|
|
|
+ duration: 300
|
|
|
|
|
+ }).start(({
|
|
|
|
|
+ finished
|
|
|
|
|
+ }) => {
|
|
|
|
|
+ if (finished && isClosing) {
|
|
|
|
|
+ setIsActive(false);
|
|
|
|
|
+
|
|
|
|
|
+ if (onClosed) onClosed();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ onPanResponderTerminationRequest: () => false,
|
|
|
|
|
+ onShouldBlockNativeResponder: () => true
|
|
|
|
|
+ })
|
|
|
|
|
+ ).current;
|
|
|
|
|
+
|
|
|
|
|
+ const renderView = () => {
|
|
|
|
|
+ return <Animated.View
|
|
|
|
|
+ {...props}
|
|
|
|
|
+ {...panResponder.panHandlers}
|
|
|
|
|
+ onLayout={onLayout}
|
|
|
|
|
+ style={[
|
|
|
|
|
+ style,
|
|
|
|
|
+ {
|
|
|
|
|
+ height: (isAutoHeight || !snapPoint) && !isMeasured ? "auto" : animatedHeight,
|
|
|
|
|
+ backgroundColor: colors.content.container[backgroundColor],
|
|
|
|
|
+ paddingBottom: bottomSafeArea + spaces.spacingMd,
|
|
|
|
|
+ opacity: isMeasured || !isAutoHeight ? 1 : 0,
|
|
|
|
|
+ paddingRight: spaces.spacingMd,
|
|
|
|
|
+ paddingLeft: spaces.spacingMd,
|
|
|
|
|
+ paddingTop: spaces.spacingMd,
|
|
|
|
|
+ maxHeight: maxHeight.current,
|
|
|
|
|
+ transform: [{
|
|
|
|
|
+ translateY: animatedTranslateY
|
|
|
|
|
+ }]
|
|
|
|
|
+ },
|
|
|
|
|
+ stylesheet.container
|
|
|
|
|
+ ]}
|
|
|
|
|
+ >
|
|
|
|
|
+ {renderHeader()}
|
|
|
|
|
+ <ScrollView
|
|
|
|
|
+ {...scrollViewProps}
|
|
|
|
|
+ onContentSizeChange={(w, h) => {
|
|
|
|
|
+ scrollViewContentHeight.current = h;
|
|
|
|
|
+ }}
|
|
|
|
|
+ onLayout={(e) => {
|
|
|
|
|
+ scrollViewLayoutHeight.current = e.nativeEvent.layout.height;
|
|
|
|
|
+ }}
|
|
|
|
|
+ onStartShouldSetResponderCapture={() => false}
|
|
|
|
|
+ onMoveShouldSetResponderCapture={() => false}
|
|
|
|
|
+ showsHorizontalScrollIndicator={false}
|
|
|
|
|
+ showsVerticalScrollIndicator={false}
|
|
|
|
|
+ scrollEnabled={!isCanSwipe}
|
|
|
|
|
+ scrollEventThrottle={1}
|
|
|
|
|
+ overScrollMode="never"
|
|
|
|
|
+ ref={scrollViewRef}
|
|
|
|
|
+ bounces={false}
|
|
|
|
|
+ style={[
|
|
|
|
|
+ scrollViewStyle
|
|
|
|
|
+ ]}
|
|
|
|
|
+ >
|
|
|
|
|
+ {children}
|
|
|
|
|
+ </ScrollView>
|
|
|
|
|
+ {renderBottom()}
|
|
|
|
|
+ {renderHandle()}
|
|
|
|
|
+ </Animated.View>;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const renderHeader = () => {
|
|
|
|
|
+ if(!RenderHeaderComponent) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return RenderHeaderComponent();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const renderBottom = () => {
|
|
|
|
|
+ if(!RenderBottomComponent) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return <RenderBottomComponent/>;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const renderHandle = () => {
|
|
|
|
|
+ if(!isShowHandle || isWorkAsFullScreen) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const handleHeight = handleHeightProp ? handleHeightProp : 8;
|
|
|
|
|
+ const handleContainerHeight = handleHeight + ((handleContainerSpacing ? spaces[handleContainerSpacing] : spaces.spacingSm) * 2);
|
|
|
|
|
+ const handleBorderRadius = Math.ceil(handleHeight / 2);
|
|
|
|
|
+
|
|
|
|
|
+ return <View
|
|
|
|
|
+ {...panResponder.panHandlers}
|
|
|
|
|
+ onStartShouldSetResponderCapture={() => false}
|
|
|
|
|
+ onMoveShouldSetResponderCapture={() => false}
|
|
|
|
|
+ style={[
|
|
|
|
|
+ stylesheet.handleContainer,
|
|
|
|
|
+ {
|
|
|
|
|
+ backgroundColor: handleContainerBackgroundColor
|
|
|
|
|
+ ? colors.content.container[handleContainerBackgroundColor]
|
|
|
|
|
+ : "transparent",
|
|
|
|
|
+ height: handleContainerHeight,
|
|
|
|
|
+ transform: [{
|
|
|
|
|
+ translateY: -handleContainerHeight
|
|
|
|
|
+ }]
|
|
|
|
|
+ }
|
|
|
|
|
+ ]}
|
|
|
|
|
+ >
|
|
|
|
|
+ <View
|
|
|
|
|
+ style={[
|
|
|
|
|
+ stylesheet.handle,
|
|
|
|
|
+ {
|
|
|
|
|
+ backgroundColor: handleBackgroundColor
|
|
|
|
|
+ ? colors.content.container[handleBackgroundColor]
|
|
|
|
|
+ : colors.content.container.default,
|
|
|
|
|
+ borderRadius: handleBorderRadius,
|
|
|
|
|
+ height: handleHeight
|
|
|
|
|
+ }
|
|
|
|
|
+ ]}
|
|
|
|
|
+ />
|
|
|
|
|
+ </View>;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return <Modal
|
|
|
|
|
+ {...modalProps}
|
|
|
|
|
+ isWorkWithPortal={isWorkWithPortal}
|
|
|
|
|
+ key={`${bottomSheetKey}-modal`}
|
|
|
|
|
+ isContentRequired={false}
|
|
|
|
|
+ isActive={isActive}
|
|
|
|
|
+ alignContent="free"
|
|
|
|
|
+ isAnimated={false}
|
|
|
|
|
+ ref={modalRef}
|
|
|
|
|
+ onContainerLayout={(e) => {
|
|
|
|
|
+ const containerLayoutHeight = e.nativeEvent.layout.height;
|
|
|
|
|
+
|
|
|
|
|
+ if (containerLayoutHeight && containerLayoutHeight !== containerHeightRef.current) {
|
|
|
|
|
+ containerHeightRef.current = containerLayoutHeight;
|
|
|
|
|
+
|
|
|
|
|
+ if (!isWorkAsFullScreen && !isCanFullScreenOnSwipe && snapPoint) {
|
|
|
|
|
+ maxHeight.current = snapPoint;
|
|
|
|
|
+ } else if (!isWorkAsFullScreen) {
|
|
|
|
|
+ maxHeight.current = containerLayoutHeight - (isForceFullScreenOnSwipe ? 0 : isWrapSafeAreaContext ? topSafeArea : 0);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ maxHeight.current = containerLayoutHeight;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }}
|
|
|
|
|
+ overlayProps={{
|
|
|
|
|
+ onStartShouldSetResponderCapture: () => false,
|
|
|
|
|
+ onMoveShouldSetResponderCapture: () => false
|
|
|
|
|
+ }}
|
|
|
|
|
+ onOverlayPress={() => {
|
|
|
|
|
+ if(onOverlayPressed) onOverlayPressed();
|
|
|
|
|
+
|
|
|
|
|
+ if(isCloseOnOverlay) {
|
|
|
|
|
+ closeAnimation();
|
|
|
|
|
+ }
|
|
|
|
|
+ }}
|
|
|
|
|
+ style={[
|
|
|
|
|
+ {
|
|
|
|
|
+ paddingTop: topSafeArea
|
|
|
|
|
+ }
|
|
|
|
|
+ ]}
|
|
|
|
|
+ >
|
|
|
|
|
+ {renderView()}
|
|
|
|
|
+ </Modal>;
|
|
|
|
|
+};
|
|
|
|
|
+export default forwardRef(BottomSheet);
|