From 9ca5f29b200e1192712859dd9fe31f8c411fadf1 Mon Sep 17 00:00:00 2001 From: Mo Gorhom Date: Sun, 16 May 2021 22:38:20 +0100 Subject: [PATCH] feat: added snap to position (#443) * chore: added snapToPosition and rename snapToIndex * chore: updated example * fix: allow temporary position to snap to top --- example/src/Dev.tsx | 6 +- example/src/Perf.tsx | 2 +- .../advanced/CustomBackgroundExample.tsx | 2 +- .../screens/advanced/CustomHandleExample.tsx | 2 +- example/src/screens/basic/BasicExamples.tsx | 14 +-- .../screens/integrations/NavigatorExample.tsx | 2 +- src/components/bottomSheet/BottomSheet.tsx | 110 ++++++++++++------ .../bottomSheetBackdrop/usePressBehavior.ts | 8 +- .../bottomSheetModal/BottomSheetModal.tsx | 28 +++-- src/hooks/useInteractivePanGestureHandler.ts | 57 ++++++--- src/types.d.ts | 69 ++++++----- src/utilities/normalizeSnapPoint.ts | 2 +- 12 files changed, 191 insertions(+), 111 deletions(-) diff --git a/example/src/Dev.tsx b/example/src/Dev.tsx index 84c10f684..38c86a44d 100644 --- a/example/src/Dev.tsx +++ b/example/src/Dev.tsx @@ -87,10 +87,10 @@ const BasicExample = () => { console.log('handleSheetChanges', index); }, []); const handleSnapPress = useCallback(index => { - bottomSheetRef.current?.snapTo(index); + bottomSheetRef.current?.snapToIndex(index); }, []); const handleSnapPosition = useCallback(position => { - bottomSheetRef.current?.snapTo(position); + bottomSheetRef.current?.snapToIndex(position); }, []); const handleClosePress = useCallback(() => { bottomSheetRef.current?.close(); @@ -103,7 +103,7 @@ const BasicExample = () => { setOptions({ headerShown: shownHeader.current, }); - }, []); + }, [setOptions]); //#endregion // renders diff --git a/example/src/Perf.tsx b/example/src/Perf.tsx index e62fa9bbe..208a40b28 100644 --- a/example/src/Perf.tsx +++ b/example/src/Perf.tsx @@ -53,7 +53,7 @@ const App = () => { loop++; } - bottomSheetRef.current?.snapTo(index++); + bottomSheetRef.current?.snapToIndex(index++); }, 1000); return () => { diff --git a/example/src/screens/advanced/CustomBackgroundExample.tsx b/example/src/screens/advanced/CustomBackgroundExample.tsx index 7969240bc..2555d44ba 100644 --- a/example/src/screens/advanced/CustomBackgroundExample.tsx +++ b/example/src/screens/advanced/CustomBackgroundExample.tsx @@ -14,7 +14,7 @@ const CustomBackgroundExample = () => { // callbacks const handleSnapPress = useCallback(index => { - bottomSheetRef.current?.snapTo(index); + bottomSheetRef.current?.snapToIndex(index); }, []); const handleExpandPress = useCallback(() => { bottomSheetRef.current?.expand(); diff --git a/example/src/screens/advanced/CustomHandleExample.tsx b/example/src/screens/advanced/CustomHandleExample.tsx index 774a7174a..c93d25ede 100644 --- a/example/src/screens/advanced/CustomHandleExample.tsx +++ b/example/src/screens/advanced/CustomHandleExample.tsx @@ -14,7 +14,7 @@ const CustomHandleExample = () => { // callbacks const handleSnapPress = useCallback(index => { - bottomSheetRef.current?.snapTo(index); + bottomSheetRef.current?.snapToIndex(index); }, []); const handleExpandPress = useCallback(() => { bottomSheetRef.current?.expand(); diff --git a/example/src/screens/basic/BasicExamples.tsx b/example/src/screens/basic/BasicExamples.tsx index 0c83c4eb9..b1e77afb5 100644 --- a/example/src/screens/basic/BasicExamples.tsx +++ b/example/src/screens/basic/BasicExamples.tsx @@ -13,14 +13,10 @@ interface ExampleScreenProps { const createExampleScreen = ({ type, count = 25 }: ExampleScreenProps) => memo(() => { //#region state - const [ - enableContentPanningGesture, - setEnableContentPanningGesture, - ] = useState(true); - const [ - enableHandlePanningGesture, - setEnableHandlePanningGesture, - ] = useState(true); + const [enableContentPanningGesture, setEnableContentPanningGesture] = + useState(true); + const [enableHandlePanningGesture, setEnableHandlePanningGesture] = + useState(true); //#endregion //#region refs @@ -65,7 +61,7 @@ const createExampleScreen = ({ type, count = 25 }: ExampleScreenProps) => [] ); const handleSnapPress = useCallback(index => { - bottomSheetRef.current?.snapTo(index); + bottomSheetRef.current?.snapToIndex(index); }, []); const handleExpandPress = useCallback(() => { bottomSheetRef.current?.expand(); diff --git a/example/src/screens/integrations/NavigatorExample.tsx b/example/src/screens/integrations/NavigatorExample.tsx index fde80530e..80d615fd8 100644 --- a/example/src/screens/integrations/NavigatorExample.tsx +++ b/example/src/screens/integrations/NavigatorExample.tsx @@ -83,7 +83,7 @@ const NavigatorExample = () => { console.log('handleSheetChange', index); }, []); const handleSnapPress = useCallback(index => { - bottomSheetRef.current?.snapTo(index); + bottomSheetRef.current?.snapToIndex(index); }, []); const handleExpandPress = useCallback(() => { bottomSheetRef.current?.expand(); diff --git a/src/components/bottomSheet/BottomSheet.tsx b/src/components/bottomSheet/BottomSheet.tsx index 56b432c0e..9d4ecf9b5 100644 --- a/src/components/bottomSheet/BottomSheet.tsx +++ b/src/components/bottomSheet/BottomSheet.tsx @@ -48,7 +48,12 @@ import { SCROLLABLE_STATE, KEYBOARD_BLUR_BEHAVIOR, } from '../../constants'; -import { animate, getKeyboardAnimationConfigs, print } from '../../utilities'; +import { + animate, + getKeyboardAnimationConfigs, + normalizeSnapPoint, + print, +} from '../../utilities'; import { DEFAULT_ANIMATION_EASING, DEFAULT_ANIMATION_DURATION, @@ -514,7 +519,7 @@ const BottomSheetComponent = forwardRef( animatedPosition, animatedSnapPoints, animatedContainerHeight, - isExtendedByKeyboard: isInTemporaryPosition, + isInTemporaryPosition, scrollableContentOffsetY, animateToPoint: animateToPosition, }); @@ -530,17 +535,16 @@ const BottomSheetComponent = forwardRef( animatedPosition, animatedSnapPoints, animatedContainerHeight, - isExtendedByKeyboard: isInTemporaryPosition, + isInTemporaryPosition, animateToPoint: animateToPosition, }); //#endregion //#region public methods - const handleSnapTo = useCallback( - function handleSnapToPoint( + const handleSnapToIndex = useCallback( + function handleSnapToIndex( index: number, - animationDuration: number = DEFAULT_ANIMATION_DURATION, - animationEasing: Animated.EasingFunction = DEFAULT_ANIMATION_EASING + animationConfigs?: Animated.WithSpringConfig | Animated.WithTimingConfig ) { const snapPoints = animatedSnapPoints.value; invariant( @@ -565,22 +569,61 @@ const BottomSheetComponent = forwardRef( } const newSnapPoint = snapPoints[index]; - runOnUI(animateToPosition)(newSnapPoint, 0, { - duration: animationDuration, - easing: animationEasing, - }); + runOnUI(animateToPosition)(newSnapPoint, 0, animationConfigs); }, [ animateToPosition, animatedSnapPoints, - animatedContainerHeight.value, + animatedContainerHeight, animatedPosition, ] ); + const handleSnapToPosition = useWorkletCallback( + function handleSnapToPosition( + position: number | string, + animationConfigs?: Animated.WithSpringConfig | Animated.WithTimingConfig + ) { + /** + * verify if sheet is closed. + */ + if (animatedPosition.value === animatedContainerHeight.value) { + isClosing.current = false; + } + + /** + * exit method if sheet is closing. + */ + if (isClosing.current) { + return; + } + + /** + * mark the new position as temporary. + */ + isInTemporaryPosition.value = true; + + /** + * normalized provided position. + */ + const nextPosition = normalizeSnapPoint( + position, + animatedContainerHeight.value, + topInset, + bottomInset + ); + animateToPosition(nextPosition, 0, animationConfigs); + }, + [ + animateToPosition, + animatedContainerHeight, + animatedPosition, + bottomInset, + topInset, + ] + ); const handleClose = useCallback( function handleClose( - animationDuration: number = DEFAULT_ANIMATION_DURATION, - animationEasing: Animated.EasingFunction = DEFAULT_ANIMATION_EASING + animationConfigs?: Animated.WithSpringConfig | Animated.WithTimingConfig ) { print({ component: BottomSheet.name, @@ -601,17 +644,17 @@ const BottomSheetComponent = forwardRef( } isClosing.current = true; - runOnUI(animateToPosition)(animatedContainerHeight.value, 0, { - duration: animationDuration, - easing: animationEasing, - }); + runOnUI(animateToPosition)( + animatedContainerHeight.value, + 0, + animationConfigs + ); }, [animateToPosition, animatedContainerHeight, animatedPosition] ); const handleExpand = useCallback( function handleExpand( - animationDuration: number = DEFAULT_ANIMATION_DURATION, - animationEasing: Animated.EasingFunction = DEFAULT_ANIMATION_EASING + animationConfigs?: Animated.WithSpringConfig | Animated.WithTimingConfig ) { /** * verify if sheet is closed. @@ -628,10 +671,7 @@ const BottomSheetComponent = forwardRef( } const snapPoints = animatedSnapPoints.value; const newSnapPoint = snapPoints[snapPoints.length - 1]; - runOnUI(animateToPosition)(newSnapPoint, 0, { - duration: animationDuration, - easing: animationEasing, - }); + runOnUI(animateToPosition)(newSnapPoint, 0, animationConfigs); }, [ animateToPosition, @@ -642,8 +682,7 @@ const BottomSheetComponent = forwardRef( ); const handleCollapse = useCallback( function handleCollapse( - animationDuration: number = DEFAULT_ANIMATION_DURATION, - animationEasing: Animated.EasingFunction = DEFAULT_ANIMATION_EASING + animationConfigs?: Animated.WithSpringConfig | Animated.WithTimingConfig ) { /** * verify if sheet is closed. @@ -661,10 +700,7 @@ const BottomSheetComponent = forwardRef( const snapPoints = animatedSnapPoints.value; const newSnapPoint = snapPoints[0]; - runOnUI(animateToPosition)(newSnapPoint, 0, { - duration: animationDuration, - easing: animationEasing, - }); + runOnUI(animateToPosition)(newSnapPoint, 0, animationConfigs); }, [ animateToPosition, @@ -675,7 +711,8 @@ const BottomSheetComponent = forwardRef( ); useImperativeHandle(ref, () => ({ - snapTo: handleSnapTo, + snapToIndex: handleSnapToIndex, + snapToPosition: handleSnapToPosition, expand: handleExpand, collapse: handleCollapse, close: handleClose, @@ -725,12 +762,19 @@ const BottomSheetComponent = forwardRef( ); const externalContextVariables = useMemo( () => ({ - snapTo: handleSnapTo, + snapToIndex: handleSnapToIndex, + snapToPosition: handleSnapToPosition, expand: handleExpand, collapse: handleCollapse, close: handleClose, }), - [handleSnapTo, handleExpand, handleCollapse, handleClose] + [ + handleSnapToIndex, + handleSnapToPosition, + handleExpand, + handleCollapse, + handleClose, + ] ); //#endregion diff --git a/src/components/bottomSheetBackdrop/usePressBehavior.ts b/src/components/bottomSheetBackdrop/usePressBehavior.ts index ae06674b8..100900dad 100644 --- a/src/components/bottomSheetBackdrop/usePressBehavior.ts +++ b/src/components/bottomSheetBackdrop/usePressBehavior.ts @@ -15,7 +15,7 @@ export const usePressBehavior = ({ 'closeOnPress' | 'disappearsOnIndex' | 'pressBehavior' >) => { //#region hooks - const { snapTo, close } = useBottomSheet(); + const { snapToIndex, close } = useBottomSheet(); //#endregion //#region variables @@ -35,11 +35,11 @@ export const usePressBehavior = ({ if (syntheticPressBehavior === 'close') { close(); } else if (syntheticPressBehavior === 'collapse') { - snapTo(disappearsOnIndex as number); + snapToIndex(disappearsOnIndex as number); } else if (typeof syntheticPressBehavior === 'number') { - snapTo(syntheticPressBehavior); + snapToIndex(syntheticPressBehavior); } - }, [close, disappearsOnIndex, syntheticPressBehavior, snapTo]); + }, [snapToIndex, close, disappearsOnIndex, syntheticPressBehavior]); //#endregion //#region effects diff --git a/src/components/bottomSheetModal/BottomSheetModal.tsx b/src/components/bottomSheetModal/BottomSheetModal.tsx index feea34b67..67a7dbe3d 100644 --- a/src/components/bottomSheetModal/BottomSheetModal.tsx +++ b/src/components/bottomSheetModal/BottomSheetModal.tsx @@ -48,12 +48,8 @@ const BottomSheetModalComponent = forwardRef< //#endregion //#region hooks - const { - containerHeight, - mountSheet, - unmountSheet, - willUnmountSheet, - } = useBottomSheetModalInternal(); + const { containerHeight, mountSheet, unmountSheet, willUnmountSheet } = + useBottomSheetModalInternal(); const { removePortal: unmountPortal } = usePortal(); //#endregion @@ -112,11 +108,22 @@ const BottomSheetModalComponent = forwardRef< //#endregion //#region bottom sheet methods - const handleSnapTo = useCallback((...args) => { + const handleSnapToIndex = useCallback( + (...args) => { + if (minimized.current) { + return; + } + bottomSheetRef.current?.snapToIndex(...args); + }, + [] + ); + const handleSnapToPosition = useCallback< + BottomSheetMethods['snapToPosition'] + >((...args) => { if (minimized.current) { return; } - bottomSheetRef.current?.snapTo(...args); + bottomSheetRef.current?.snapToPosition(...args); }, []); const handleExpand = useCallback((...args) => { if (minimized.current) { @@ -224,7 +231,7 @@ const BottomSheetModalComponent = forwardRef< return; } minimized.current = false; - bottomSheetRef.current?.snapTo(restoreIndexRef.current); + bottomSheetRef.current?.snapToIndex(restoreIndexRef.current); }, []); //#endregion @@ -289,7 +296,8 @@ const BottomSheetModalComponent = forwardRef< //#region expose methods useImperativeHandle(ref, () => ({ // sheet - snapTo: handleSnapTo, + snapToIndex: handleSnapToIndex, + snapToPosition: handleSnapToPosition, expand: handleExpand, collapse: handleCollapse, close: handleClose, diff --git a/src/hooks/useInteractivePanGestureHandler.ts b/src/hooks/useInteractivePanGestureHandler.ts index a9c015135..d22b0bb82 100644 --- a/src/hooks/useInteractivePanGestureHandler.ts +++ b/src/hooks/useInteractivePanGestureHandler.ts @@ -24,7 +24,7 @@ export interface useInteractivePanGestureHandlerConfigs { enablePanDownToClose: boolean; overDragResistanceFactor: number; keyboardBehavior: keyof typeof KEYBOARD_BEHAVIOR; - isExtendedByKeyboard: Animated.SharedValue; + isInTemporaryPosition: Animated.SharedValue; keyboardState: Animated.SharedValue; keyboardHeight: Animated.SharedValue; animatedSnapPoints: Animated.SharedValue; @@ -35,7 +35,7 @@ export interface useInteractivePanGestureHandlerConfigs { } type InteractivePanGestureHandlerContextType = { - currentPosition: number; + startPosition: number; keyboardState: KEYBOARD_STATE; }; @@ -47,7 +47,7 @@ export const useInteractivePanGestureHandler = ({ keyboardState, keyboardBehavior, keyboardHeight, - isExtendedByKeyboard, + isInTemporaryPosition, animatedPosition, animatedSnapPoints, animatedContainerHeight, @@ -72,7 +72,7 @@ export const useInteractivePanGestureHandler = ({ cancelAnimation(animatedPosition); // store current animated position - context.currentPosition = animatedPosition.value; + context.startPosition = animatedPosition.value; context.keyboardState = keyboardState.value; if ( @@ -80,7 +80,7 @@ export const useInteractivePanGestureHandler = ({ (keyboardBehavior === KEYBOARD_BEHAVIOR.interactive || keyboardBehavior === KEYBOARD_BEHAVIOR.fullScreen) ) { - isExtendedByKeyboard.value = true; + isInTemporaryPosition.value = true; } // set variables @@ -93,16 +93,33 @@ export const useInteractivePanGestureHandler = ({ gestureTranslationY.value = translationY; gestureVelocityY.value = velocityY; - const position = context.currentPosition + translationY; - const maxSnapPoint = isExtendedByKeyboard.value - ? context.currentPosition - : animatedSnapPoints.value[animatedSnapPoints.value.length - 1]; + const position = context.startPosition + translationY; + let maxSnapPoint = + animatedSnapPoints.value[animatedSnapPoints.value.length - 1]; + /** + * if keyboard is shown, then we set the max point to the current + * position. + */ + if ( + isInTemporaryPosition.value && + context.keyboardState === KEYBOARD_STATE.SHOWN + ) { + maxSnapPoint = context.startPosition; + } + /** + * if current position is out of provided `snapPoints` and smaller then + * max snap pont, then we set the max point to the current position. + */ + if (isInTemporaryPosition.value && context.startPosition < maxSnapPoint) { + maxSnapPoint = context.startPosition; + } + const minSnapPoint = enablePanDownToClose ? animatedContainerHeight.value : animatedSnapPoints.value[0]; const negativeScrollableContentOffset = - context.currentPosition === maxSnapPoint && scrollableContentOffsetY + context.startPosition === maxSnapPoint && scrollableContentOffsetY ? scrollableContentOffsetY.value * -1 : 0; const clampedPosition = clamp( @@ -163,18 +180,22 @@ export const useInteractivePanGestureHandler = ({ onEnd: ({ state }, context) => { gestureState.value = state; + /** + * if + */ if ( - isExtendedByKeyboard.value && - context.currentPosition >= animatedPosition.value + isInTemporaryPosition.value && + context.keyboardState === KEYBOARD_STATE.SHOWN && + context.startPosition >= animatedPosition.value ) { - if (context.currentPosition > animatedPosition.value) { - animateToPoint(context.currentPosition, gestureVelocityY.value / 2); + if (context.startPosition > animatedPosition.value) { + animateToPoint(context.startPosition, gestureVelocityY.value / 2); } return; } - if (isExtendedByKeyboard.value) { - isExtendedByKeyboard.value = false; + if (isInTemporaryPosition.value) { + isInTemporaryPosition.value = false; } const snapPoints = animatedSnapPoints.value.slice(); @@ -183,7 +204,7 @@ export const useInteractivePanGestureHandler = ({ } const destinationPoint = snapPoint( - gestureTranslationY.value + context.currentPosition, + gestureTranslationY.value + context.startPosition, gestureVelocityY.value, snapPoints ); @@ -198,7 +219,7 @@ export const useInteractivePanGestureHandler = ({ if ( (scrollableContentOffsetY ? scrollableContentOffsetY.value : 0) > 0 && - context.currentPosition === + context.startPosition === animatedSnapPoints.value[animatedSnapPoints.value.length - 1] && animatedPosition.value === animatedSnapPoints.value[animatedSnapPoints.value.length - 1] diff --git a/src/types.d.ts b/src/types.d.ts index 40088ab4f..9e028ee9f 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -6,62 +6,73 @@ 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 + * @param animationConfigs snap animation configs. + * + * @see {Animated.WithSpringConfig} + * @see {Animated.WithTimingConfig} */ - snapTo: ( + snapToIndex: ( index: number, - animationDuration?: number, - animationEasing?: Animated.EasingFunction + animationConfigs?: Animated.WithSpringConfig | Animated.WithTimingConfig + ) => void; + /** + * Snap to a position out of provided `snapPoints`. + * @param position position in pixel or percentage. + * @param animationConfigs snap animation configs. + * + * @see {Animated.WithSpringConfig} + * @see {Animated.WithTimingConfig} + */ + snapToPosition: ( + position: number | string, + animationConfigs?: Animated.WithSpringConfig | Animated.WithTimingConfig ) => void; /** * Snap to the maximum provided point from `snapPoints`. - * @param animationDuration snap animation duration. - * @param animationEasing snap animation easing function. - * @type () => void + * @param animationConfigs snap animation configs. + * + * @see {Animated.WithSpringConfig} + * @see {Animated.WithTimingConfig} */ expand: ( - animationDuration?: number, - animationEasing?: Animated.EasingFunction + animationConfigs?: Animated.WithSpringConfig | Animated.WithTimingConfig ) => void; /** * Snap to the minimum provided point from `snapPoints`. - * @param animationDuration snap animation duration. - * @param animationEasing snap animation easing function. - * @type () => void + * @param animationConfigs snap animation configs. + * + * @see {Animated.WithSpringConfig} + * @see {Animated.WithTimingConfig} */ collapse: ( - animationDuration?: number, - animationEasing?: Animated.EasingFunction + animationConfigs?: Animated.WithSpringConfig | Animated.WithTimingConfig ) => void; /** * Close the bottom sheet. - * @param animationDuration snap animation duration. - * @param animationEasing snap animation easing function. - * @type () => void + * @param animationConfigs snap animation configs. + * + * @see {Animated.WithSpringConfig} + * @see {Animated.WithTimingConfig} */ close: ( - animationDuration?: number, - animationEasing?: Animated.EasingFunction + animationConfigs?: Animated.WithSpringConfig | Animated.WithTimingConfig ) => void; } export interface BottomSheetModalMethods extends BottomSheetMethods { /** - * Mount and present the modal. - * @type () => void + * Mount and present the bottom sheet modal to the initial snap point. */ present: () => void; /** - * Close and unmount the modal. - * @param animationDuration snap animation duration. - * @param animationEasing snap animation easing function. - * @type () => void; + * Close and unmount the bottom sheet modal. + * @param animationConfigs snap animation configs. + * + * @see {Animated.WithSpringConfig} + * @see {Animated.WithTimingConfig} */ dismiss: ( - animationDuration?: number, - animationEasing?: Animated.EasingFunction + animationConfigs?: Animated.WithSpringConfig | Animated.WithTimingConfig ) => void; } //#endregion diff --git a/src/utilities/normalizeSnapPoint.ts b/src/utilities/normalizeSnapPoint.ts index ead8c0398..059116b10 100644 --- a/src/utilities/normalizeSnapPoint.ts +++ b/src/utilities/normalizeSnapPoint.ts @@ -6,7 +6,7 @@ export const normalizeSnapPoint = ( containerHeight: number, topInset: number, _bottomInset: number, - $modal: boolean + $modal: boolean = false ) => { 'worklet'; let normalizedSnapPoint = snapPoint;