Bläddra i källkod

Feature: BottomSheet system completed.

lfabl 2 månader sedan
förälder
incheckning
8992d6cc63

+ 5 - 7
example/app.json

@@ -1,15 +1,15 @@
 {
     "expo": {
-        "name": "NcoreUiKitMobile Example",
+        "name": "NCore | Ui Kit - Mobile Example",
         "slug": "ncore-ui-kit-mobile-example",
         "version": "1.0.0",
         "orientation": "portrait",
         "icon": "./assets/icon.png",
-        "userInterfaceStyle": "light",
+        "userInterfaceStyle": "dark",
         "splash": {
             "image": "./assets/splash-icon.png",
             "resizeMode": "contain",
-            "backgroundColor": "#ffffff"
+            "backgroundColor": "#222222"
         },
         "ios": {
             "supportsTablet": true,
@@ -17,10 +17,8 @@
         },
         "android": {
             "adaptiveIcon": {
-                "backgroundColor": "#E6F4FE",
-                "foregroundImage": "./assets/android-icon-foreground.png",
-                "backgroundImage": "./assets/android-icon-background.png",
-                "monochromeImage": "./assets/android-icon-monochrome.png"
+                "backgroundColor": "#222222",
+                "foregroundImage": "./assets/icon.png"
             },
             "package": "ncoreuikitmobile.example"
         },

BIN
example/assets/adaptive-icon.png


BIN
example/assets/android-icon-background.png


BIN
example/assets/android-icon-foreground.png


BIN
example/assets/android-icon-monochrome.png


BIN
example/assets/favicon.png


BIN
example/assets/icon.png


BIN
example/assets/partial-react-logo.png


BIN
example/assets/react-logo.png


BIN
example/assets/react-logo@2x.png


BIN
example/assets/react-logo@3x.png


BIN
example/assets/splash-icon-dark.png


BIN
example/assets/splash-icon.png


BIN
example/assets/splash.png


+ 11 - 3
example/src/index.tsx

@@ -1,8 +1,12 @@
+import {
+    useRef
+} from "react";
 import {
     StyleSheet,
     View
 } from "react-native";
 import {
+    type IBottomSheetRef,
     setupNCoreUIKit,
     NCoreUIKitTheme,
     BottomSheet,
@@ -13,8 +17,8 @@ import {
 import {
     useFonts
 } from "expo-font";
-import packageJSON from "../package.json";
 import TextInput from "../../src/components/textInput";
+import packageJSON from "../package.json";
 
 const NCoreUIKitBase = setupNCoreUIKit({
     initialSelectedGapPropagation: "compact",
@@ -32,6 +36,8 @@ const App = () => {
         colors
     } = NCoreUIKitTheme.useContext();
 
+    const bottomSheetRef = useRef<IBottomSheetRef>(null);
+
     return <View
         style={[
             styles.container,
@@ -61,6 +67,7 @@ const App = () => {
         <Text>Version: v{packageJSON.version}</Text>
         <Button
             onPress={() => {
+                bottomSheetRef.current?.open();
             }}
             type="success"
             title="Ahmet"
@@ -68,12 +75,13 @@ const App = () => {
         />
         <BottomSheet
             isCanFullScreenOnSwipe={true}
-            // snapPoint={300}
+            ref={bottomSheetRef}
+            snapPoint={300}
         >
             <View
                 style={{
                     backgroundColor: "red",
-                    height: 400
+                    height: 2400
                 }}
             />
             <Text>Deneme 123</Text>

+ 192 - 24
src/components/bottomSheet/index.tsx

@@ -1,8 +1,9 @@
 import {
+    useImperativeHandle,
     type ComponentRef,
+    forwardRef,
     useEffect,
     useState,
-    type FC,
     useRef
 } from "react";
 import {
@@ -13,6 +14,9 @@ import {
     View
 } from "react-native";
 import type IBottomSheetProps from "./type";
+import type {
+    IBottomSheetRef
+} from "./type";
 import stylesheet from "./stylesheet";
 import {
     NCoreUIKitTheme
@@ -20,12 +24,18 @@ import {
 import {
     useSafeAreaInsets
 } from "react-native-safe-area-context";
-import Modal from "../modal";
+import type {
+    RefForwardingComponent
+} from "../../types";
+import type {
+    IModalRef
+} from "../modal/type";
 import {
     windowHeight
 } from "../../utils";
+import Modal from "../modal";
 
-const BottomSheet: FC<IBottomSheetProps> = ({
+const BottomSheet: RefForwardingComponent<IBottomSheetRef, IBottomSheetProps> = ({
     renderBottom: RenderBottomComponent,
     isForceFullScreenOnSwipe = false,
     isCanFullScreenOnSwipe = false,
@@ -34,15 +44,21 @@ const BottomSheet: FC<IBottomSheetProps> = ({
     isWrapSafeareaContext = true,
     backgroundColor = "default",
     isWorkAsFullScreen = false,
+    isCloseOnOverlay = true,
     handleContainerSpacing,
+    isActive: isActiveProp,
     handleBackgroundColor,
     isAutoHeight = false,
     isShowHandle = true,
     snapPoint,
     children,
+    onClosed,
+    onOpened,
+    onClose,
+    onOpen,
     style,
     ...props
-}) => {
+}, ref) => {
     const {
         colors,
         spaces
@@ -58,6 +74,11 @@ const BottomSheet: FC<IBottomSheetProps> = ({
         setIsMeasured
     ] = useState(false);
 
+    const [
+        isActive,
+        setIsActive
+    ] = useState(isActiveProp === undefined ? false : isActiveProp);
+
     let bottomSafeArea = isWrapSafeareaContext ? bottom : 0;
     let topSafeArea = isWrapSafeareaContext ? top : 0;
 
@@ -71,12 +92,15 @@ const BottomSheet: FC<IBottomSheetProps> = ({
     }
 
     const scrollViewRef = useRef<ComponentRef<ScrollView>>(null);
+    const modalRef = useRef<IModalRef>(null);
 
     const animatedTranslateY = useRef(new Animated.Value(snapPoint && !isWorkAsFullScreen ? snapPoint : windowHeight)).current;
     const animatedHeight = useRef(new Animated.Value(
         isAutoHeight ? 0 : snapPoint ?? 0
     )).current;
 
+    const TOP_GRAB_AREA = 140;
+
     const maxHeight = useRef(isWorkAsFullScreen ? windowHeight : windowHeight - (isForceFullScreenOnSwipe ? 0 : isWrapSafeareaContext ? topSafeArea : 0));
     const heightValue = useRef(isWorkAsFullScreen ? windowHeight : snapPoint ?? 0);
     const initialTranslateY = useRef(0);
@@ -89,10 +113,25 @@ const BottomSheet: FC<IBottomSheetProps> = ({
     const initialScrollOffset = useRef(0);
     const scrollOffset = useRef(0);
 
+    const gestureStartY = useRef(0);
+
     if(!isWorkAsFullScreen && !isCanFullScreenOnSwipe && snapPoint) {
         maxHeight.current = snapPoint;
     }
 
+    useImperativeHandle(
+        ref,
+        () => ({
+            close: () => {
+                closeAnimation();
+            },
+            open: () => {
+                setIsActive(true);
+            }
+        }),
+        []
+    );
+
     useEffect(() => {
         if(isMeasured && contentHeight.current !== -1) {
             if(isCanFullScreenOnSwipe && !isWorkAsFullScreen) {
@@ -103,6 +142,15 @@ const BottomSheet: FC<IBottomSheetProps> = ({
         }
     }, [isMeasured]);
 
+    useEffect(() => {
+        if(isActive && isMeasured) {
+            openAnimation();
+        }
+    }, [
+        isActive,
+        isMeasured
+    ]);
+
     useEffect(() => {
         const listenerAHeightId = animatedHeight.addListener(({
             value
@@ -115,7 +163,6 @@ const BottomSheet: FC<IBottomSheetProps> = ({
         }) => {
             translateYValue.current = value;
         });
-        openAnimation();
 
         return () => {
             animatedHeight.removeListener(listenerAHeightId);
@@ -124,16 +171,39 @@ const BottomSheet: FC<IBottomSheetProps> = ({
     }, []);
 
     const openAnimation = () => {
-        Animated.spring(animatedTranslateY, {
+        resetState();
+        if(onOpen) onOpen();
+
+        Animated.timing(animatedTranslateY, {
             useNativeDriver: false,
-            friction: 10,
-            tension: 40,
+            duration: 300,
             toValue: 0
-        }).start();
+        }).start(({
+            finished
+        }) => {
+            if(finished) {
+                if(onOpened) onOpened();
+            }
+        });
     };
 
-    const closeAnimation = () => {
+    const closeAnimation = (toValue?: number) => {
+        if(onClose) onClose();
+
+        Animated.timing(animatedTranslateY, {
+            toValue: toValue ? toValue : isAutoHeight || !snapPoint ? contentHeight.current : snapPoint,
+            useNativeDriver: false,
+            duration: 300
+        }).start(({
+            finished
+        }) => {
+            if(finished) {
+                resetState();
+                setIsActive(false);
 
+                if(onClosed) onClosed();
+            }
+        });
     };
 
     const onLayout = (event: LayoutChangeEvent) => {
@@ -144,15 +214,44 @@ const BottomSheet: FC<IBottomSheetProps> = ({
         } = 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;
+
+        initialHeight.current = snapPoint ?? contentHeight.current;
+        initialTranslateY.current = 0;
+
+        heightValue.current = snapPoint ?? contentHeight.current;
+        translateYValue.current = 0;
+    };
+
     const panResponder = useRef(
         PanResponder.create({
             onStartShouldSetPanResponder: () => false,
             onMoveShouldSetPanResponderCapture: () => true,
-            onPanResponderGrant: () => {
+            onPanResponderGrant: (evt) => {
+                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;
@@ -168,6 +267,29 @@ const BottomSheet: FC<IBottomSheetProps> = ({
                     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;
@@ -268,15 +390,33 @@ const BottomSheet: FC<IBottomSheetProps> = ({
                 }
             },
             onPanResponderEnd: (_, gestureState) => {
+                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) {
+                    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 currentH = heightValue.current;
-                const currentT = translateYValue.current;
-
                 const isFastSwipeDown = gestureState.vy > 0.5;
                 const isFastSwipeUp = gestureState.vy < -0.5;
 
@@ -288,10 +428,20 @@ const BottomSheet: FC<IBottomSheetProps> = ({
                     } else if (isFastSwipeDown) {
                         toValue = pivot;
                     } else {
-                        const distToTavan = Math.max(0, tavan - currentH);
-                        const distToPivot = Math.max(0, Math.abs(currentH - pivot));
+                        if (!isCanFullScreenOnSwipe) {
+                            toValue = pivot;
+                        } else {
+                            const totalRange = tavan - pivot;
+                            const distFromPivot = currentH - pivot;
 
-                        toValue = (distToTavan < distToPivot && isCanFullScreenOnSwipe) ? tavan : 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, {
@@ -302,23 +452,37 @@ const BottomSheet: FC<IBottomSheetProps> = ({
                     }).start();
                 } else {
                     let toValueT = 0;
+                    let isClosing = false;
 
                     if (isFastSwipeDown) {
                         toValueT = pivot + 100;
+
+                        isClosing = true;
                     } else {
-                        if (currentT > pivot * 0.4) {
+                        if (currentT > pivot * 0.5) {
                             toValueT = pivot + 100;
+
+                            isClosing = true;
                         } else {
                             toValueT = 0;
+
+                            isClosing = false;
                         }
                     }
 
-                    Animated.spring(animatedTranslateY, {
+                    Animated.timing(animatedTranslateY, {
                         useNativeDriver: false,
                         toValue: toValueT,
-                        friction: 10,
-                        tension: 40
-                    }).start();
+                        duration: 300
+                    }).start(({
+                        finished
+                    }) => {
+                        if (finished && isClosing) {
+                            setIsActive(false);
+
+                            if (onClosed) onClosed();
+                        }
+                    });
                 }
             },
             onPanResponderTerminationRequest: () => false,
@@ -424,13 +588,17 @@ const BottomSheet: FC<IBottomSheetProps> = ({
 
     return <Modal
         isContentRequired={false}
+        isActive={isActive}
         isAnimated={false}
+        ref={modalRef}
         overlayProps={{
             onStartShouldSetResponderCapture: () => false,
             onMoveShouldSetResponderCapture: () => false
         }}
         onOverlayPress={() => {
-            console.error("pressed overlay");
+            if(isCloseOnOverlay) {
+                closeAnimation();
+            }
         }}
         style={[
             {
@@ -441,4 +609,4 @@ const BottomSheet: FC<IBottomSheetProps> = ({
         {renderView()}
     </Modal>;
 };
-export default BottomSheet;
+export default forwardRef(BottomSheet);

+ 11 - 0
src/components/bottomSheet/type.ts

@@ -6,6 +6,11 @@ import type {
     ViewStyle
 } from "react-native";
 
+export interface IBottomSheetRef {
+    close: () => void;
+    open: () => void;
+}
+
 interface IBottomSheetProps {
     handleContainerBackgroundColor?: keyof NCoreUIKit.ContainerContentColors;
     handleContainerSpacing?: keyof NCoreUIKit.ActivePalette["spaces"];
@@ -17,11 +22,17 @@ interface IBottomSheetProps {
     renderBottom?: () => ReactNode;
     isWorkAsFullScreen?: boolean;
     style?: StyleProp<ViewStyle>;
+    isCloseOnOverlay?: boolean;
     isShowHandle?: boolean;
     isAutoHeight?: boolean;
     handleHeight?: number;
+    onClosed?: () => void;
+    onOpened?: () => void;
+    onClose?: () => void;
     children?: ReactNode;
+    onOpen?: () => void;
     snapPoint?: number;
+    isActive?: boolean;
 }
 export type {
     IBottomSheetProps as default

+ 4 - 0
src/components/index.ts

@@ -42,3 +42,7 @@ export {
 export {
     default as BottomSheet
 } from "./bottomSheet";
+
+export type {
+    IBottomSheetRef
+} from "./bottomSheet/type";

+ 19 - 1
src/components/modal/index.tsx

@@ -2,6 +2,7 @@ import {
     useImperativeHandle,
     forwardRef,
     useEffect,
+    useState,
     useRef
 } from "react";
 import {
@@ -35,6 +36,7 @@ const Modal: RefForwardingComponent<IModalRef, IModalProps> = ({
     isContentRequired = true,
     isOverlayVisible = true,
     alignContent = "center",
+    isActive: isActiveProp,
     isAnimated = true,
     onOverlayPress,
     contentStyle,
@@ -48,6 +50,11 @@ const Modal: RefForwardingComponent<IModalRef, IModalProps> = ({
 
     const scaleAnim = useRef(new Animated.Value(isAnimated ? 0 : 1)).current;
 
+    const [
+        isActive,
+        setIsActive
+    ] = useState(isActiveProp === undefined ? true : isActiveProp);
+
     useEffect(() => {
         if(isAnimated) {
             Animated.timing(scaleAnim, {
@@ -59,10 +66,17 @@ const Modal: RefForwardingComponent<IModalRef, IModalProps> = ({
         }
     }, []);
 
+    useEffect(() => {
+        if(isActiveProp !== undefined) {
+            setIsActive(isActiveProp);
+        }
+    }, [isActiveProp]);
+
     useImperativeHandle(
         ref,
         () => ({
-            closeAnimation
+            closeAnimation,
+            setIsActive
         }),
         []
     );
@@ -130,6 +144,10 @@ const Modal: RefForwardingComponent<IModalRef, IModalProps> = ({
         </View>;
     };
 
+    if(!isActive) {
+        return null;
+    }
+
     return <Portal>
         <Animated.View
             style={[

+ 2 - 0
src/components/modal/type.ts

@@ -9,6 +9,7 @@ import {
 
 export type IModalRef = {
     closeAnimation: (onClosed?: () => void) => void;
+    setIsActive: (newActiveState: boolean) => void;
 };
 
 export type ModalAlignContentProp = "center" | "free";
@@ -26,6 +27,7 @@ export type ModalInternalProps = {
     overlayProps?: ViewProps;
     isAnimated?: boolean;
     children?: ReactNode;
+    isActive?: boolean;
 };
 
 interface IModalProps extends ModalInternalProps {

+ 1 - 0
src/index.tsx

@@ -21,6 +21,7 @@ export {
 } from "./components";
 
 export type {
+    IBottomSheetRef,
     // IDialogRef,
     IModalRef
 } from "./components";