gorhom / react-native-bottom-sheet

A performant interactive bottom sheet with fully configurable options 🚀
https://ui.gorhom.dev/components/bottom-sheet
MIT License
6.58k stars 734 forks source link

[v4] | [v2] [iOS] Backdrop flashes when content height is dynamic in BottomSheetModal #1763

Open badalsaibo opened 5 months ago

badalsaibo commented 5 months ago

Bug

Environment info

Library Version
@gorhom/bottom-sheet 4.6.1
react-native 0.72.5
react-native-reanimated 3.5.4
react-native-gesture-handler 2.13.4

Steps To Reproduce

import { BottomSheetModal } from '@gorhom/bottom-sheet';
import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
import { ForwardedRef, forwardRef, useMemo } from 'react';
import { useBottomSheetDynamicSnapPoints } from './hooks/useBottomSheetDynamicSnapPoints';
import CustomBackdrop from './custom-backdrop.component';
import { View } from 'react-native';
import { useBottomSheetBackHandler } from './hooks/useBottomSheetBackHandler';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

const GenericBottomSheetModal = (
  props: {
    children: React.ReactNode;
    enableDismissOnClose?: boolean;
    backdropComponent?: (props: any) => React.JSX.Element;
  },
  ref: ForwardedRef<BottomSheetModalMethods>,
) => {
  const { handleSheetPositionChange } = useBottomSheetBackHandler(ref);

  const insets = useSafeAreaInsets();

  const { children, ...rest } = props;
  const initialSnapPoints = useMemo(() => ['CONTENT_HEIGHT'], []);
  const {
    animatedHandleHeight,
    animatedSnapPoints,
    animatedContentHeight,
    handleContentLayout,
  } = useBottomSheetDynamicSnapPoints(initialSnapPoints);

  return (
    <BottomSheetModal
      ref={ref}
      snapPoints={animatedSnapPoints as any}
      handleHeight={animatedHandleHeight}
      contentHeight={animatedContentHeight}
      handleIndicatorStyle={{ display: 'none' }}
      backdropComponent={CustomBackdrop}
      onChange={handleSheetPositionChange}
      {...rest}>
      <View
        className="flex-1"
        onLayout={handleContentLayout}
        style={{ paddingBottom: insets.bottom }}>
        {children}
      </View>
    </BottomSheetModal>
  );
};

export default forwardRef(GenericBottomSheetModal);
// useBottomSheetDynamicSnapPoints

import { useCallback } from 'react';
import { useDerivedValue, useSharedValue } from 'react-native-reanimated';

const INITIAL_HANDLE_HEIGHT = -999;
const INITIAL_SNAP_POINT = -999;

/**
 * Provides dynamic content height calculating functionalities, by
 * replacing the placeholder `CONTENT_HEIGHT` with calculated layout.
 * @example
 * [0, 'CONTENT_HEIGHT', '100%']
 * @param initialSnapPoints your snap point with content height placeholder.
 * @returns {
 *  - animatedSnapPoints: an animated snap points to be set on `BottomSheet` or `BottomSheetModal`.
 *  - animatedHandleHeight: an animated handle height callback node to be set on `BottomSheet` or `BottomSheetModal`.
 *  - animatedContentHeight: an animated content height callback node to be set on `BottomSheet` or `BottomSheetModal`.
 *  - handleContentLayout: a `onLayout` callback method to be set on `BottomSheetView` component.
 * }
 */
export const useBottomSheetDynamicSnapPoints = (
  initialSnapPoints: Array<string | number>,
) => {
  // variables
  const animatedContentHeight = useSharedValue(0);
  const animatedHandleHeight = useSharedValue(INITIAL_HANDLE_HEIGHT);
  const animatedSnapPoints = useDerivedValue(() => {
    if (
      animatedHandleHeight.value === INITIAL_HANDLE_HEIGHT ||
      animatedContentHeight.value === 0
    ) {
      return initialSnapPoints.map(() => INITIAL_SNAP_POINT);
    }
    const contentWithHandleHeight =
      animatedContentHeight.value + animatedHandleHeight.value;

    return initialSnapPoints.map((snapPoint) =>
      snapPoint === 'CONTENT_HEIGHT' ? contentWithHandleHeight : snapPoint,
    );
  }, []);

  type HandleContentLayoutProps = {
    nativeEvent: {
      layout: { height: number };
    };
  };
  // callbacks
  const handleContentLayout = useCallback(
    ({
      nativeEvent: {
        layout: { height },
      },
    }: HandleContentLayoutProps) => {
      animatedContentHeight.value = height;
    },
    [animatedContentHeight],
  );

  return {
    animatedSnapPoints,
    animatedHandleHeight,
    animatedContentHeight,
    handleContentLayout,
  };
};

Describe what you expected to happen:

  1. The backdrop shouldn't flicker.
  2. The dynamic height shouldn't change.

Reproducible sample code

Videos / Images

https://github.com/gorhom/react-native-bottom-sheet/assets/14058003/341711bb-4c6e-4b83-9a6b-674633713fac

VictorioMolina commented 5 months ago

+1

Acetyld commented 5 months ago

Exactly the same issue here, @badalsaibo did you found a solution to this?

github-actions[bot] commented 4 months ago

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.

severinferard commented 4 months ago

Having the same issue here

lexi-stein commented 3 months ago

also having the same issue!

Karthik-B-06 commented 3 months ago

I am also having the same issue, has anyone found a solution?

stichingsd-vitrion commented 3 months ago

Also having same issue here

badalsaibo commented 3 months ago

@Acetyld I think for a fix I gave it a fixed height for iOS only.

  const initialSnapPoints = useMemo(() => ['50%'], []);

I created a non adaptive component for iOS separately too

{
  Platform.OS === "ios" ? (
    <SearchTutorBottomSheetNonAdaptive
      bottomSheetModalRef={tutorSearchBottomSheetRef}
      handleClose={handleClose}
    />
  ) : (
    <SearchTutorBottomSheet
      bottomSheetModalRef={tutorSearchBottomSheetRef}
      handleClose={handleClose}
    />
  );
}
freddy-eturi commented 3 months ago

+1

nhuesmann commented 2 months ago

I am also having this issue. Have tried a few things and can't resolve it. Would love to see a fix for this!

github-actions[bot] commented 1 month ago

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.

DaltonPelkey commented 1 month ago

This isn't stale. The issue is caused by the animatedIndex being set back to -1 when the height changes. Logging the index using useAnimatedReaction in a custom backdrop component I can inspect this properly: image "form" is an update inside the modal which causes the height to change. Notice how the index jumps from -1 and back to 0

DaltonPelkey commented 1 month ago

My current, sloppy, solution is to animate using useAnimatedReaction so that I can compare the current and previous values. This way I can prevent the animation from happening when the value suddenly changes:

function AppBottomSheetBackdrop({animatedIndex, style}: BottomSheetBackdropProps) {
  const sheet = useBottomSheet();
  const opacity = useSharedValue(0);

  useAnimatedReaction(() => animatedIndex.value, (current, previous) => {
    if (current === -1 && previous === 0) return;
    opacity.value = interpolate(
      animatedIndex.value,
      [-1, 0],
      [0, 1],
      Extrapolation.CLAMP
    )
  });

  return (
    <Animated.View
      style={[
        style,
        {
          backgroundColor: 'rgba(0,0,0,0.3)',
          opacity
        }
      ]}
    >
...
marcshilling commented 1 month ago

@DaltonPelkey thanks, this works great!

github-actions[bot] commented 3 days ago

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.