0

I created a custom sheet in react native by following an online tutorial. Beyond the typical scroll view bottom sheet, I decided to add a header and footer component in the bottom sheet with a scroll view in between. The bottom sheet functions as it should, except for one issue. When dragging to close the bottom sheet, the footer remains fixed on the bottom of the sheet until the header collapses onto it.

The behaviour that I was expecting is for the footer component to move in tandem with the rest of the sheet when dragging to close. I tried wrapping my footer component in an animated view or animating the footer itself, but the behaviour did not change.

I have attached my code for the custom sheet for reproducibility:

import { forwardRef, useCallback, useImperativeHandle, useState } from "react";
import { Dimensions, View } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, {
  runOnJS,
  useAnimatedScrollHandler,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
  withTiming,
} from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import BackDrop from "../Backdrop/BackDrop";
import { styledContainer, styledContentContainer, styledIndicator, styledIndicatorContainer } from "./styles/BottomSheetStyles";

export const BottomSheet = forwardRef(
  (
    {
      snapTo,
      backgroundColor,
      backdropColour,
      type,
      children,
      headerComponent,
      footerComponent,
      ...props
    },
    ref
  ) => {
    const insets = useSafeAreaInsets();
    const { height } = Dimensions.get("screen");
    const percentage = parseFloat(snapTo) / 100;
    const openHeight = height - height * percentage;
    const closeHeight = height;
    const topAnimation = useSharedValue(closeHeight);
    const context = useSharedValue(0);
    const scrollBegin = useSharedValue(0);
    const scrollY = useSharedValue(0);
    const [enabledScroll, setEnableScroll] = useState(true);

    const expand = useCallback(() => {
      "worklet";
      topAnimation.value = withTiming(openHeight);
    }, [openHeight, topAnimation]);

    const close = useCallback(() => {
      "worklet";
      topAnimation.value = withTiming(closeHeight);
    }, [closeHeight, topAnimation]);

    useImperativeHandle(ref, () => ({ expand, close }), [expand, close]);

    const animationStyle = useAnimatedStyle(() => {
      const top = topAnimation.value;
      return { top };
    });

    const pan = Gesture.Pan()
      .onBegin(() => {
        context.value = topAnimation.value;
      })
      .onUpdate((event) => {
        if (event.translationY < 0) {
          topAnimation.value = withSpring(openHeight, {
            damping: 100,
            stiffness: 400,
          });
        } else {
          topAnimation.value = withSpring(event.translationY + context.value, {
            damping: 100,
            stiffness: 400,
          });
        }
      })
      .onEnd(() => {
        if (topAnimation.value > openHeight + 50) {
          topAnimation.value = withSpring(closeHeight, {
            damping: 100,
            stiffness: 400,
          });
        } else {
          topAnimation.value = withSpring(openHeight, {
            damping: 100,
            stiffness: 400,
          });
        }
      });

    const onScroll = useAnimatedScrollHandler({
      onBeginDrag: (event) => {
        scrollBegin.value = event.contentOffset.y;
      },
      onScroll: (event) => {
        scrollY.value = event.contentOffset.y;
      },
    });

    const panScroll = Gesture.Pan()
      .onBegin(() => {
        context.value = topAnimation.value;
      })
      .onUpdate((event) => {
        if (event.translationY < 0) {
          runOnJS(setEnableScroll)(true);
          topAnimation.value = withSpring(openHeight, {
            damping: 100,
            stiffness: 400,
          });
        } else if (event.translationY > 0 && scrollY.value === 0) {
          runOnJS(setEnableScroll)(false);
          topAnimation.value = withSpring(
            Math.max(
              context.value + event.translationY - scrollBegin.value, 
              openHeight
            ), 
            {
              damping: 100,
              stiffness: 400,
            }
          );
        }
      })
      .onEnd(() => {
        runOnJS(setEnableScroll)(true);
        if (topAnimation.value > openHeight + 50) {
          topAnimation.value = withSpring(closeHeight, {
            damping: 100,
            stiffness: 400,
          });
        } else {
          topAnimation.value = withSpring(openHeight, {
            damping: 100,
            stiffness: 400,
          });
        }
      });

    const scrollViewGesture = Gesture.Native();

    return (
      <>
        <BackDrop
          topAnimation={topAnimation}
          closeHeight={closeHeight}
          openHeight={openHeight}
          close={close}
          backdropColour={backdropColour}
        />
        <GestureDetector gesture={pan}>
          <Animated.View
            style={[
              styledContainer,
              animationStyle,
              {
                backgroundColor: backgroundColor,
                paddingBottom: insets.bottom,
              },
            ]}
          >
            <View style={styledIndicatorContainer}>
              <View style={styledIndicator} />
            </View>

            <View style={styledContentContainer}>
              <View>{headerComponent}</View>

              {type === "scroll" ? (
                <GestureDetector
                  gesture={Gesture.Simultaneous(panScroll, scrollViewGesture)}
                >
                  <Animated.ScrollView
                    {...props}
                    bounces={false}
                    scrollEventThrottle={16}
                    onScroll={onScroll}
                    scrollEnabled={enabledScroll}
                  >
                    {children}
                  </Animated.ScrollView>
                </GestureDetector>
              ) : (
                children
              )}

              <View>{footerComponent}</View>
            </View>
          </Animated.View>
        </GestureDetector>
      </>
    );
  }
);

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.