diff --git a/example/src/screens/modal/SimpleExample.tsx b/example/src/screens/modal/SimpleExample.tsx index 66fe9412d..cb16584ce 100644 --- a/example/src/screens/modal/SimpleExample.tsx +++ b/example/src/screens/modal/SimpleExample.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useRef } from 'react'; +import React, { useCallback, useMemo, useRef } from 'react'; import { View, StyleSheet } from 'react-native'; import { BottomSheetModal } from '@gorhom/bottom-sheet'; import Button from '../../components/button'; @@ -6,8 +6,12 @@ import ContactListContainer from '../../components/contactListContainer'; import withModalProvider from '../withModalProvider'; const SimpleExample = () => { + // refs const bottomSheetRef = useRef(null); + // variables + const snapPoints = useMemo(() => ['25%', '50%'], []); + // callbacks const handleChange = useCallback((index: number) => { // eslint-disable-next-line no-console @@ -55,7 +59,7 @@ const SimpleExample = () => { /> ( //#region extract props const { // animations configurations - animationDuration = DEFAULT_ANIMATION_DURATION, - animationEasing = DEFAULT_ANIMATION_EASING, + animationDuration: _providedAnimationDuration = DEFAULT_ANIMATION_DURATION, + animationEasing: _providedAnimationEasing = DEFAULT_ANIMATION_EASING, animationConfigs: _providedAnimationConfigs, // configurations @@ -289,39 +294,52 @@ const BottomSheetComponent = forwardRef( animationState.value = ANIMATION_STATE.STOPPED; }, [animatedIndex, animationState, handleOnChange, refreshUIElements]); const animateToPoint = useWorkletCallback( - (point: number, velocity: number = 0) => { + ( + point: number, + velocity: number = 0, + animationDuration?: number, + animationEasing?: Animated.EasingFunction + ) => { animationState.value = ANIMATION_STATE.RUNNING; runOnJS(handleOnAnimate)(point); - if (_providedAnimationConfigs) { + /** + * force animation configs from parameters, if provided + */ + if (animationDuration !== undefined) { + animatedPosition.value = animate(ANIMATION_METHOD.TIMING, { + duration: animationDuration, + easing: animationEasing + ? animationEasing + : DEFAULT_ANIMATION_EASING, + })(point, velocity, animateToPointCompleted); + } else if (_providedAnimationConfigs) { + /** + * use animationConfigs callback, if provided + */ animatedPosition.value = _providedAnimationConfigs( point, velocity, animateToPointCompleted ); - return; + } else { + /** + * @deprecated this will be removed in next major release. + */ + animatedPosition.value = animate(ANIMATION_METHOD.TIMING, { + duration: _providedAnimationDuration, + easing: _providedAnimationEasing, + })(point, velocity, animateToPointCompleted); } - - /** - * @deprecated this will be removed in next major release. - */ - animatedPosition.value = withTiming( - point, - { - duration: animationDuration, - easing: animationEasing, - }, - animateToPointCompleted - ); }, [ - _providedAnimationConfigs, animationState, animatedPosition, - animationDuration, - animationEasing, animateToPointCompleted, handleOnAnimate, + _providedAnimationConfigs, + _providedAnimationDuration, + _providedAnimationEasing, ] ); @@ -368,7 +386,7 @@ const BottomSheetComponent = forwardRef( //#region public methods const handleSnapTo = useCallback( - (index: number) => { + (index: number, ...args) => { invariant( index >= -1 && index <= snapPoints.length - 1, `'index' was provided but out of the provided snap points range! expected value to be between -1, ${ @@ -379,31 +397,40 @@ const BottomSheetComponent = forwardRef( return; } const newSnapPoint = snapPoints[index]; - runOnUI(animateToPoint)(newSnapPoint); + runOnUI(animateToPoint)(newSnapPoint, 0, ...args); + }, + [animateToPoint, snapPoints] + ); + const handleClose = useCallback( + (...args) => { + if (isClosing.current) { + return; + } + isClosing.current = true; + runOnUI(animateToPoint)(safeContainerHeight, 0, ...args); + }, + [animateToPoint, safeContainerHeight] + ); + const handleExpand = useCallback( + (...args) => { + if (isClosing.current) { + return; + } + const newSnapPoint = snapPoints[snapPoints.length - 1]; + runOnUI(animateToPoint)(newSnapPoint, 0, ...args); + }, + [animateToPoint, snapPoints] + ); + const handleCollapse = useCallback( + (...args) => { + if (isClosing.current) { + return; + } + const newSnapPoint = snapPoints[0]; + runOnUI(animateToPoint)(newSnapPoint, 0, ...args); }, [animateToPoint, snapPoints] ); - const handleClose = useCallback(() => { - if (isClosing.current) { - return; - } - isClosing.current = true; - runOnUI(animateToPoint)(safeContainerHeight); - }, [animateToPoint, safeContainerHeight]); - const handleExpand = useCallback(() => { - if (isClosing.current) { - return; - } - const newSnapPoint = snapPoints[snapPoints.length - 1]; - runOnUI(animateToPoint)(newSnapPoint); - }, [animateToPoint, snapPoints]); - const handleCollapse = useCallback(() => { - if (isClosing.current) { - return; - } - const newSnapPoint = snapPoints[0]; - runOnUI(animateToPoint)(newSnapPoint); - }, [animateToPoint, snapPoints]); useImperativeHandle(ref, () => ({ snapTo: handleSnapTo, expand: handleExpand, @@ -512,7 +539,8 @@ const BottomSheetComponent = forwardRef( isLayoutCalculated && didMountOnAnimate.current === false && isClosing.current === false && - snapPoints[_providedIndex] !== safeContainerHeight + snapPoints[_providedIndex] !== safeContainerHeight && + _providedIndex !== -1 ) { const newSnapPoint = snapPoints[_providedIndex]; requestAnimationFrame(() => runOnUI(animateToPoint)(newSnapPoint)); diff --git a/src/components/bottomSheetModal/BottomSheetModal.tsx b/src/components/bottomSheetModal/BottomSheetModal.tsx index 3eec71374..1db435a68 100644 --- a/src/components/bottomSheetModal/BottomSheetModal.tsx +++ b/src/components/bottomSheetModal/BottomSheetModal.tsx @@ -87,7 +87,8 @@ const BottomSheetModalComponent = forwardRef< forcedDismissed.current = false; }, []); const adjustIndex = useCallback( - (_index: number) => (dismissOnPanDown ? _index - 1 : _index), + (_index: number, internal = true) => + dismissOnPanDown ? (internal ? _index - 1 : _index + 1) : _index, [dismissOnPanDown] ); const unmount = useCallback(() => { @@ -113,34 +114,40 @@ const BottomSheetModalComponent = forwardRef< //#endregion //#region bottom sheet methods - const handleSnapTo = useCallback(() => { - if (minimized.current) { - return; - } + const handleSnapTo = useCallback( + (_index: number, ...args) => { + if (minimized.current) { + return; + } - bottomSheetRef.current?.snapTo(adjustIndex(currentIndexRef.current)); - }, [adjustIndex]); - const handleExpand = useCallback(() => { + bottomSheetRef.current?.snapTo(adjustIndex(_index, false), ...args); + }, + [adjustIndex] + ); + const handleExpand = useCallback((...args) => { if (minimized.current) { return; } - bottomSheetRef.current?.expand(); + bottomSheetRef.current?.expand(...args); }, []); - const handleCollapse = useCallback(() => { - if (minimized.current) { - return; - } - if (dismissOnPanDown) { - bottomSheetRef.current?.snapTo(1); - } else { - bottomSheetRef.current?.collapse(); - } - }, [dismissOnPanDown]); - const handleClose = useCallback(() => { + const handleCollapse = useCallback( + (...args) => { + if (minimized.current) { + return; + } + if (dismissOnPanDown) { + bottomSheetRef.current?.snapTo(1, ...args); + } else { + bottomSheetRef.current?.collapse(...args); + } + }, + [dismissOnPanDown] + ); + const handleClose = useCallback((...args) => { if (minimized.current) { return; } - bottomSheetRef.current?.close(); + bottomSheetRef.current?.close(...args); }, []); //#endregion @@ -151,22 +158,25 @@ const BottomSheetModalComponent = forwardRef< mountSheet(key, ref, stackBehavior); }); }, [key, stackBehavior, ref, mountSheet]); - const handleDismiss = useCallback(() => { - /** - * if modal is already been dismiss, we exit the method. - */ - if (currentIndexRef.current === -1 && minimized.current === false) { - return; - } + const handleDismiss = useCallback( + (...args) => { + /** + * if modal is already been dismiss, we exit the method. + */ + if (currentIndexRef.current === -1 && minimized.current === false) { + return; + } - if (minimized.current) { - unmount(); - return; - } - willUnmountSheet(key); - forcedDismissed.current = true; - bottomSheetRef.current?.close(); - }, [willUnmountSheet, unmount, key]); + if (minimized.current) { + unmount(); + return; + } + willUnmountSheet(key); + forcedDismissed.current = true; + bottomSheetRef.current?.close(...args); + }, + [willUnmountSheet, unmount, key] + ); const handleMinimize = useCallback(() => { if (minimized.current) { return; diff --git a/src/constants.ts b/src/constants.ts index 024cae9ef..8463f1b68 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -14,6 +14,11 @@ enum ANIMATION_STATE { STOPPED, } +enum ANIMATION_METHOD { + TIMING, + SPRING, +} + const MODAL_STACK_BEHAVIOR = { replace: 'replace', push: 'push', @@ -24,5 +29,6 @@ export { WINDOW_WIDTH, GESTURE, ANIMATION_STATE, + ANIMATION_METHOD, MODAL_STACK_BEHAVIOR, }; diff --git a/src/hooks/useBottomSheetSpringConfigs.ts b/src/hooks/useBottomSheetSpringConfigs.ts index ca3122635..f628f5bf4 100644 --- a/src/hooks/useBottomSheetSpringConfigs.ts +++ b/src/hooks/useBottomSheetSpringConfigs.ts @@ -1,18 +1,13 @@ -import Animated, { - useWorkletCallback, - withSpring, -} from 'react-native-reanimated'; +import Animated from 'react-native-reanimated'; +import { ANIMATION_METHOD } from '../constants'; +import { animate } from '../utilities'; +/** + * Generate spring animation configs. + * @param configs overridable configs. + */ export const useBottomSheetSpringConfigs = ( configs: Omit ) => { - const animationConfig = useWorkletCallback( - (point: number, velocity: number = 0, callback: () => void) => { - // @ts-ignore override velocity - configs.velocity = velocity; - return withSpring(point, configs, callback); - }, - [configs] - ); - return animationConfig; + return animate(ANIMATION_METHOD.SPRING, configs); }; diff --git a/src/hooks/useBottomSheetTimingConfigs.ts b/src/hooks/useBottomSheetTimingConfigs.ts index 7c659a87f..edfcb5203 100644 --- a/src/hooks/useBottomSheetTimingConfigs.ts +++ b/src/hooks/useBottomSheetTimingConfigs.ts @@ -1,15 +1,14 @@ import { useMemo } from 'react'; -import Animated, { - useWorkletCallback, - withTiming, -} from 'react-native-reanimated'; +import Animated from 'react-native-reanimated'; +import { ANIMATION_METHOD } from '../constants'; +import { animate } from '../utilities'; import { DEFAULT_ANIMATION_DURATION, DEFAULT_ANIMATION_EASING, } from '../components/bottomSheet/constants'; /** - * Generate animation timing configs. + * Generate timing animation configs. * @default * - easing: Easing.out(Easing.exp) * - duration 500 @@ -33,10 +32,6 @@ export const useBottomSheetTimingConfigs = ( } return _configs; }, [configs.duration, configs.easing]); - const animationConfig = useWorkletCallback( - (point: number, _, callback: () => void) => - withTiming(point, overrideConfigs, callback), - [overrideConfigs] - ); - return animationConfig; + + return animate(ANIMATION_METHOD.TIMING, overrideConfigs); }; diff --git a/src/types.d.ts b/src/types.d.ts index 868223187..357c33bac 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,27 +1,50 @@ import type { FlatList, ScrollView, SectionList } from 'react-native'; +import type Animated from 'react-native-reanimated'; //#region Methods export interface BottomSheetMethods { /** * Snap to one of the provided points from `snapPoints`. + * @param index snap point index. + * @param animationDuration snap animation duration. + * @param animationEasing snap animation easing function. * @type (index: number) => void */ - snapTo: (index: number) => void; + snapTo: ( + index: number, + animationDuration?: number, + animationEasing?: Animated.EasingFunction + ) => void; /** * Snap to the maximum provided point from `snapPoints`. + * @param animationDuration snap animation duration. + * @param animationEasing snap animation easing function. * @type () => void */ - expand: () => void; + expand: ( + animationDuration?: number, + animationEasing?: Animated.EasingFunction + ) => void; /** * Snap to the minimum provided point from `snapPoints`. + * @param animationDuration snap animation duration. + * @param animationEasing snap animation easing function. * @type () => void */ - collapse: () => void; + collapse: ( + animationDuration?: number, + animationEasing?: Animated.EasingFunction + ) => void; /** * Close the bottom sheet. + * @param animationDuration snap animation duration. + * @param animationEasing snap animation easing function. * @type () => void */ - close: () => void; + close: ( + animationDuration?: number, + animationEasing?: Animated.EasingFunction + ) => void; } export interface BottomSheetModalMethods extends BottomSheetMethods { @@ -32,9 +55,14 @@ export interface BottomSheetModalMethods extends BottomSheetMethods { present: () => void; /** * Close and unmount the modal. + * @param animationDuration snap animation duration. + * @param animationEasing snap animation easing function. * @type () => void; */ - dismiss: () => void; + dismiss: ( + animationDuration?: number, + animationEasing?: Animated.EasingFunction + ) => void; } //#endregion diff --git a/src/utilities/animate.ts b/src/utilities/animate.ts new file mode 100644 index 000000000..68435444b --- /dev/null +++ b/src/utilities/animate.ts @@ -0,0 +1,22 @@ +import Animated, { withSpring, withTiming } from 'react-native-reanimated'; +import { ANIMATION_METHOD } from '../constants'; + +export const animate = ( + type: ANIMATION_METHOD, + configs: Animated.WithSpringConfig | Animated.WithTimingConfig +) => { + 'worklet'; + if (type === ANIMATION_METHOD.TIMING) { + return (point: number, _: number, callback: () => void) => { + 'worklet'; + return withTiming(point, configs as Animated.WithTimingConfig, callback); + }; + } else { + return (point: number, velocity: number = 0, callback: () => void) => { + 'worklet'; + // @ts-ignore + configs.velocity = velocity; + return withSpring(point, configs as Animated.WithSpringConfig, callback); + }; + } +}; diff --git a/src/utilities/index.ts b/src/utilities/index.ts index f4aa7f415..08d3a405d 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -1 +1,2 @@ export { normalizeSnapPoints } from './normalizeSnapPoints'; +export { animate } from './animate';