Przeglądaj źródła

Feature: BottomSheet Component base structure completed.

lfabl 2 miesięcy temu
rodzic
commit
53fbbd26d5

+ 1 - 2
example/src/index.tsx

@@ -68,13 +68,12 @@ const App = () => {
         />
         <BottomSheet
             isCanFullScreenOnSwipe={true}
-            isWrapSafeareaContext={true}
             snapPoint={300}
         >
             <View
                 style={{
                     backgroundColor: "red",
-                    height: 400
+                    height: 2700
                 }}
             />
             <Text>Deneme 123</Text>

+ 214 - 152
src/components/bottomSheet/index.tsx

@@ -1,8 +1,9 @@
 import {
+    type ComponentRef,
+    useEffect,
     useState,
     type FC,
-    useRef,
-    useEffect
+    useRef
 } from "react";
 import {
     type LayoutChangeEvent,
@@ -57,18 +58,6 @@ const BottomSheet: FC<IBottomSheetProps> = ({
         setIsMeasured
     ] = useState(false);
 
-    const animatedHeight = useRef(new Animated.Value(
-        (!isAutoHeight && typeof snapPoint === "number") ? snapPoint : 0
-    )).current;
-    const animatedTranslateY = useRef(new Animated.Value(0)).current;
-
-    const lastTranslateY = useRef(0);
-    const isAtBottom = useRef(false);
-    const lastHeight = useRef(0);
-    const startDy = useRef(0);
-
-    const scrollOffset = useRef(0);
-
     let bottomSafeArea = isWrapSafeareaContext ? bottom : 0;
     let topSafeArea = isWrapSafeareaContext ? top : 0;
 
@@ -81,21 +70,57 @@ const BottomSheet: FC<IBottomSheetProps> = ({
         topSafeArea = 0;
     }
 
-    const maxHeight = windowHeight - topSafeArea;
+    const scrollViewRef = useRef<ComponentRef<ScrollView>>(null);
 
-    let initialHeight: string | number = "auto";
+    const animatedTranslateY = useRef(new Animated.Value(0)).current;
+    const animatedHeight = useRef(new Animated.Value(
+        isAutoHeight ? 0 : snapPoint ?? 0
+    )).current;
+
+    const maxHeight = useRef(isWorkAsFullScreen ? windowHeight : windowHeight - (isForceFullScreenOnSwipe ? 0 : isWrapSafeareaContext ? topSafeArea : 0));
+    const heightValue = useRef(isWorkAsFullScreen ? windowHeight : snapPoint ?? 0);
+    const initialTranslateY = useRef(0);
+    const translateYValue = useRef(0);
+    const contentHeight = useRef(-1);
+    const initialHeight = useRef(0);
 
-    if(!isAutoHeight && snapPoint) {
-        initialHeight = snapPoint;
+    const scrollViewContentHeight = useRef(-1);
+    const scrollViewLayoutHeight = useRef(-1);
+    const initialScrollOffset = useRef(0);
+    const scrollOffset = useRef(0);
 
-        if(initialHeight > maxHeight) initialHeight = maxHeight;
+    if(!isWorkAsFullScreen && !isCanFullScreenOnSwipe && snapPoint) {
+        maxHeight.current = snapPoint;
     }
 
     useEffect(() => {
-        if (!isAutoHeight && typeof initialHeight === "number") {
-            animatedHeight.setValue(initialHeight);
+        if(isMeasured && contentHeight.current !== -1) {
+            if(isCanFullScreenOnSwipe && !isWorkAsFullScreen) {
+                maxHeight.current = windowHeight - (isForceFullScreenOnSwipe ? 0 : topSafeArea);
+            } else if(isAutoHeight || !snapPoint) {
+                maxHeight.current = contentHeight.current;
+            }
         }
-    }, [initialHeight]);
+    }, [isMeasured]);
+
+    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 onLayout = (event: LayoutChangeEvent) => {
         if (isMeasured) return;
@@ -104,177 +129,204 @@ const BottomSheet: FC<IBottomSheetProps> = ({
             height
         } = event.nativeEvent.layout;
 
-        let _height = height;
-
-        if(height > maxHeight) _height = maxHeight;
-
-        animatedHeight.setValue(_height);
+        animatedHeight.setValue(height);
+        contentHeight.current = height;
         setIsMeasured(true);
     };
 
-    const snapTo = (targetHeight: number, targetTranslateY: number) => {
-        Animated.parallel([
-            Animated.spring(animatedHeight, {
-                useNativeDriver: true,
-                toValue: targetHeight,
-                tension: 50,
-                friction: 8
-            }),
-            Animated.spring(animatedTranslateY, {
-                toValue: targetTranslateY,
-                useNativeDriver: true,
-                tension: 50,
-                friction: 8
-            })
-        ]).start();
-    };
-
     const panResponder = useRef(
         PanResponder.create({
             onStartShouldSetPanResponder: () => false,
-            onMoveShouldSetPanResponder: (evt, gestureState) => {
+            onMoveShouldSetPanResponderCapture: () => true,
+            onPanResponderGrant: () => {
+                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) => {
                 const {
-                    dy,
-                    dx
+                    dy
                 } = gestureState;
-                // @ts-ignore
-                const currentH = animatedHeight._value;
-                const isAtMax = currentH >= maxHeight - 5;
 
-                // Yatay kaydırmayı engelle (içeride slider varsa çakışmasın)
-                if (Math.abs(dx) > Math.abs(dy)) return false;
+                const delta = -dy;
 
-                // DURUM 1: Aşağı çekiyoruz (dy > 0)
-                if (dy > 0) {
-                // Scroll en tepedeyse kontrolü PanResponder alır
-                    if (scrollOffset.current <= 0) return true;
-                }
+                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);
 
-                // DURUM 2: Yukarı çekiyoruz (dy < 0)
                 if (dy < 0) {
-                // Sayfa tam açık değilse VEYA scroll en sona dayandıysa kontrolü al
-                // (isCanFullScreenOnSwipe false olsa bile snapPoint'e kadar çekebilmeli)
-                    if (!isAtMax || isAtBottom.current) return true;
-                }
+                    let currentDelta = delta;
 
-                return false;
-            },
+                    if (initialT > 0) {
+                        const usedForT = Math.min(currentDelta, initialT);
 
-            onPanResponderGrant: (evt, gestureState) => {
-                startDy.current = gestureState.dy;
-                // @ts-ignore
-                lastHeight.current = animatedHeight._value;
-                // @ts-ignore
-                lastTranslateY.current = animatedTranslateY._value;
-            },
+                        animatedTranslateY.setValue(-usedForT);
 
-            onPanResponderMove: (evt, gestureState) => {
-                const correctedDy = gestureState.dy - startDy.current;
-                const currentSnap = typeof snapPoint === "number" ? snapPoint : lastHeight.current;
+                        currentDelta -= usedForT;
+                    }
 
-                if (correctedDy < 0) { // YUKARI HAREKET
-                    if (lastTranslateY.current > 0) {
-                    // Sayfa aşağı kaymışsa önce translateY'i sıfırla
-                        const nextY = Math.max(0, lastTranslateY.current + correctedDy);
-                        animatedTranslateY.setValue(nextY);
-                    } else {
-                    // ÜST LİMİT KONTROLÜ: Tam ekran izni yoksa snapPoint/lastHeight'ta kilitle
-                    // Not: isCanFullScreenOnSwipe prop'unu buradan kontrol ediyoruz
-                        const upperLimit = isCanFullScreenOnSwipe ? maxHeight : currentSnap;
+                    if (currentDelta > 0) {
+                        if (initialH < pivot) {
+                            const spaceToPivot = pivot - initialH;
+                            const usedForH = Math.min(currentDelta, spaceToPivot);
+
+                            animatedHeight.setValue(usedForH);
+
+                            currentDelta -= usedForH;
+                        }
+
+                        if (currentDelta > 0) {
+                            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
+                                });
 
-                        const nextH = Math.min(upperLimit, lastHeight.current - correctedDy);
-                        animatedHeight.setValue(nextH);
+                                currentDelta -= usedForS;
+
+                                animatedHeight.setValue(initialH < pivot ? (pivot - initialH) : 0);
+                            }
+                        }
+
+                        if (currentDelta > 0) {
+                            if (isCanFullScreenOnSwipe) {
+                                const totalUsedBefore = (initialH < pivot ? (pivot - initialH) : 0);
+
+                                animatedHeight.setValue(totalUsedBefore + currentDelta);
+                            } else {
+                                animatedHeight.setValue(initialH < pivot ? (pivot - initialH) : 0);
+                            }
+                        }
                     }
-                }
-                else { // AŞAĞI HAREKET
-                    if (lastHeight.current > currentSnap) {
-                    // Boyu snapPoint'e kadar düşür
-                        const nextH = Math.max(currentSnap, lastHeight.current - correctedDy);
-                        animatedHeight.setValue(nextH);
-                    } else {
-                    // snapPoint'teyiz, artık komple aşağı kaydır
-                        const nextY = lastTranslateY.current + correctedDy;
-                        animatedTranslateY.setValue(nextY);
+                } 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;
+                        }
                     }
-                }
-            },
 
-            onPanResponderRelease: (evt, gestureState) => {
-                const {
-                    dy,
-                    vy
-                } = gestureState;
-                // @ts-ignore
-                const currentH = animatedHeight._value;
-                const currentSnap = typeof snapPoint === "number" ? snapPoint : lastHeight.current;
-
-                // 1. Karar: Aşağı fırlatma
-                if (vy > 0.5 || dy > 150) {
-                    if (currentH > currentSnap + 50) {
-                    // Tam ekrandan snapPoint'e düş
-                        snapTo(currentSnap, 0);
-                    } else {
-                    // snapPoint'ten aşağı, yani kapat
-                        snapTo(currentSnap, windowHeight);
+                    if (currentDelta < 0) {
+                        animatedHeight.setValue(initialH > pivot ? (pivot - initialH) : 0);
+                        animatedTranslateY.setValue(-currentDelta);
+
+                        scrollOffset.current = 0;
+
+                        scrollViewRef.current?.scrollTo({
+                            y: 0,
+                            animated: false
+                        });
                     }
                 }
-                // 2. Karar: Yukarı fırlatma
-                else if (vy < -0.5 || dy < -100) {
-                    if (isCanFullScreenOnSwipe) {
-                        snapTo(maxHeight, 0);
+            },
+            onPanResponderEnd: (_, gestureState) => {
+                animatedHeight.flattenOffset();
+                animatedTranslateY.flattenOffset();
+
+                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;
+
+                if (currentT <= 0.5) {
+                    let toValue = pivot;
+
+                    if (isFastSwipeUp && isCanFullScreenOnSwipe) {
+                        toValue = tavan;
+                    } else if (isFastSwipeDown) {
+                        toValue = pivot;
                     } else {
-                        snapTo(currentSnap, 0);
+                        const distToTavan = Math.max(0, tavan - currentH);
+                        const distToPivot = Math.max(0, Math.abs(currentH - pivot));
+
+                        toValue = (distToTavan < distToPivot && isCanFullScreenOnSwipe) ? tavan : pivot;
                     }
-                }
-                // 3. Karar: Yavaş bırakma (En yakın noktaya mıknatısla)
-                else {
-                // @ts-ignore
-                    const currentY = animatedTranslateY._value;
-
-                    if (isCanFullScreenOnSwipe && currentH > (maxHeight + currentSnap) / 2) {
-                        snapTo(maxHeight, 0);
-                    } else if (currentY > 100) {
-                        snapTo(currentSnap, windowHeight); // Çok aşağıda kaldıysa kapat
+
+                    Animated.spring(animatedHeight, {
+                        useNativeDriver: false,
+                        toValue: toValue,
+                        friction: 10,
+                        tension: 40
+                    }).start();
+                } else {
+                    let toValueT = 0;
+
+                    if (isFastSwipeDown) {
+                        toValueT = pivot + 100;
                     } else {
-                        snapTo(currentSnap, 0); // Diğer durumlarda snapPoint/Mevcut boyda kal
+                        if (currentT > pivot * 0.4) {
+                            toValueT = pivot + 100;
+                        } else {
+                            toValueT = 0;
+                        }
                     }
+
+                    Animated.spring(animatedTranslateY, {
+                        useNativeDriver: false,
+                        toValue: toValueT,
+                        friction: 10,
+                        tension: 40
+                    }).start();
                 }
             },
-
             onPanResponderTerminationRequest: () => false,
             onShouldBlockNativeResponder: () => true
         }),
     ).current;
 
-    const handleScroll = (event: any) => {
-        const {
-            layoutMeasurement,
-            contentOffset,
-            contentSize
-        } = event.nativeEvent;
-        scrollOffset.current = contentOffset.y;
-
-        const isBottom = layoutMeasurement.height + contentOffset.y >= contentSize.height - 5;
-        isAtBottom.current = isBottom;
-    };
-
     const renderView = () => {
-        const currentHeight = (isAutoHeight && !isMeasured) ? undefined : animatedHeight;
-
         return <Animated.View
             {...props}
             {...panResponder.panHandlers}
             onLayout={onLayout}
             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,
-                    height: currentHeight,
-                    maxHeight: maxHeight,
+                    maxHeight: maxHeight.current,
                     transform: [{
                         translateY: animatedTranslateY
                     }]
@@ -284,13 +336,20 @@ const BottomSheet: FC<IBottomSheetProps> = ({
             ]}
         >
             <ScrollView
+                onContentSizeChange={(w, h) => {
+                    scrollViewContentHeight.current = h;
+                }}
+                onLayout={(e) => {
+                    scrollViewLayoutHeight.current = e.nativeEvent.layout.height;
+                }}
                 onStartShouldSetResponderCapture={() => false}
                 onMoveShouldSetResponderCapture={() => false}
                 showsHorizontalScrollIndicator={false}
                 showsVerticalScrollIndicator={false}
-                scrollEventThrottle={16}
-                onScroll={handleScroll}
+                scrollEventThrottle={1}
                 overScrollMode="never"
+                scrollEnabled={false}
+                ref={scrollViewRef}
                 bounces={false}
             >
                 {children}
@@ -324,7 +383,9 @@ const BottomSheet: FC<IBottomSheetProps> = ({
             style={[
                 stylesheet.handleContainer,
                 {
-                    backgroundColor: handleContainerBackgroundColor ? colors.content.container[handleContainerBackgroundColor] : "transparent",
+                    backgroundColor: handleContainerBackgroundColor
+                        ? colors.content.container[handleContainerBackgroundColor]
+                        : "transparent",
                     height: handleContainerHeight,
                     transform: [{
                         translateY: -handleContainerHeight
@@ -336,7 +397,9 @@ const BottomSheet: FC<IBottomSheetProps> = ({
                 style={[
                     stylesheet.handle,
                     {
-                        backgroundColor: handleBackgroundColor ? colors.content.container[handleBackgroundColor] : colors.content.container.default,
+                        backgroundColor: handleBackgroundColor
+                            ? colors.content.container[handleBackgroundColor]
+                            : colors.content.container.default,
                         borderRadius: handleBorderRadius,
                         height: handleHeight
                     }
@@ -349,7 +412,6 @@ const BottomSheet: FC<IBottomSheetProps> = ({
         isContentRequired={false}
         isAnimated={false}
         overlayProps={{
-            ...panResponder.panHandlers,
             onStartShouldSetResponderCapture: () => false,
             onMoveShouldSetResponderCapture: () => false
         }}

+ 0 - 1
src/components/bottomSheet/stylesheet.ts

@@ -8,7 +8,6 @@ import {
 const stylesheet = StyleSheet.create({
     container: {
         transformOrigin: "bottom",
-        backgroundColor: "blue",
         position: "absolute",
         zIndex: 99999,
         bottom: 0,