diff --git a/src/components/bottomSheet/BottomSheet.tsx b/src/components/bottomSheet/BottomSheet.tsx index ce33ddc3d..66eda4203 100644 --- a/src/components/bottomSheet/BottomSheet.tsx +++ b/src/components/bottomSheet/BottomSheet.tsx @@ -50,6 +50,7 @@ import { KEYBOARD_INPUT_MODE, SCROLLABLE_TYPE, WINDOW_HEIGHT, + ANIMATION_SOURCE, } from '../../constants'; import { animate, @@ -72,7 +73,6 @@ import { INITIAL_SNAP_POINT, DEFAULT_ENABLE_PAN_DOWN_TO_CLOSE, INITIAL_CONTAINER_OFFSET, - DEFAULT_ANIMATION_CONFIGS, } from './constants'; import type { BottomSheetMethods, Insets } from '../../types'; import type { BottomSheetProps } from './types'; @@ -97,7 +97,7 @@ const BottomSheetComponent = forwardRef( //#region extract props const { // animations configurations - animationConfigs: _providedAnimationConfigs = DEFAULT_ANIMATION_CONFIGS, + animationConfigs: _providedAnimationConfigs, // configurations index: _providedIndex = 0, @@ -310,6 +310,9 @@ const BottomSheetComponent = forwardRef( //#region state/dynamic variables // states const animatedAnimationState = useSharedValue(ANIMATION_STATE.UNDETERMINED); + const animatedAnimationSource = useSharedValue( + ANIMATION_SOURCE.MOUNT + ); const animatedSheetState = useDerivedValue(() => { // closed position = position >= container height if (animatedPosition.value >= animatedClosedPosition.value) @@ -454,7 +457,7 @@ const BottomSheetComponent = forwardRef( adjustedSnapPoints.push(animatedContainerHeight.value); adjustedSnapPointsIndexes.push(-1); - return isLayoutCalculated.value + const currentIndex = isLayoutCalculated.value ? interpolate( animatedPosition.value, adjustedSnapPoints, @@ -462,7 +465,22 @@ const BottomSheetComponent = forwardRef( Extrapolate.CLAMP ) : -1; - }); + + /** + * if the sheet is currently running an animation by the keyboard opening, + * then we clamp the index on android with resize keyboard mode. + */ + if ( + android_keyboardInputMode === KEYBOARD_INPUT_MODE.adjustResize && + animatedAnimationSource.value === ANIMATION_SOURCE.KEYBOARD && + animatedAnimationState.value === ANIMATION_STATE.RUNNING && + isInTemporaryPosition.value + ) { + return Math.max(animatedCurrentIndex.value, currentIndex); + } + + return currentIndex; + }, [android_keyboardInputMode]); const isSheetClosing = useDerivedValue( () => animatedNextPosition.value === animatedClosedPosition.value && @@ -592,6 +610,8 @@ const BottomSheetComponent = forwardRef( animatedNextPositionIndex: animatedNextPositionIndex.value, }, }); + + animatedAnimationSource.value = ANIMATION_SOURCE.NONE; animatedAnimationState.value = ANIMATION_STATE.STOPPED; animatedNextPosition.value = Number.NEGATIVE_INFINITY; animatedNextPositionIndex.value = Number.NEGATIVE_INFINITY; @@ -601,6 +621,7 @@ const BottomSheetComponent = forwardRef( function animateToPosition( position: number, velocity: number = 0, + source: ANIMATION_SOURCE, configs?: Animated.WithTimingConfig | Animated.WithSpringConfig ) { if ( @@ -625,6 +646,7 @@ const BottomSheetComponent = forwardRef( */ cancelAnimation(animatedPosition); + // store /** * store next position */ @@ -633,9 +655,10 @@ const BottomSheetComponent = forwardRef( animatedSnapPoints.value.indexOf(position); /** - * set animation state to running + * set animation state to running, and source */ animatedAnimationState.value = ANIMATION_STATE.RUNNING; + animatedAnimationSource.value = source; /** * fire `onAnimate` callback @@ -646,32 +669,22 @@ const BottomSheetComponent = forwardRef( * force animation configs from parameters, if provided */ if (configs !== undefined) { - animatedPosition.value = animate( - position, + animatedPosition.value = animate({ + point: position, configs, velocity, - animateToPositionCompleted - ); - } else if (_providedAnimationConfigs) { + onComplete: animateToPositionCompleted, + }); + } else { /** * use animationConfigs callback, if provided */ - animatedPosition.value = animate( - position, - _providedAnimationConfigs, + animatedPosition.value = animate({ + point: position, velocity, - animateToPositionCompleted - ); - } else { - /** - * fallback to default animation configs - */ - animatedPosition.value = animate( - position, - DEFAULT_ANIMATION_CONFIGS, - velocity, - animateToPositionCompleted - ); + configs: _providedAnimationConfigs, + onComplete: animateToPositionCompleted, + }); } }, [handleOnAnimate, _providedAnimationConfigs] @@ -719,7 +732,12 @@ const BottomSheetComponent = forwardRef( */ isInTemporaryPosition.value = false; - runOnUI(animateToPosition)(nextPosition, 0, animationConfigs); + runOnUI(animateToPosition)( + nextPosition, + 0, + ANIMATION_SOURCE.USER, + animationConfigs + ); }, [ animateToPosition, @@ -770,7 +788,12 @@ const BottomSheetComponent = forwardRef( */ isInTemporaryPosition.value = true; - runOnUI(animateToPosition)(nextPosition, 0, animationConfigs); + runOnUI(animateToPosition)( + nextPosition, + 0, + ANIMATION_SOURCE.USER, + animationConfigs + ); }, [ animateToPosition, @@ -809,7 +832,12 @@ const BottomSheetComponent = forwardRef( */ isInTemporaryPosition.value = false; - runOnUI(animateToPosition)(nextPosition, 0, animationConfigs); + runOnUI(animateToPosition)( + nextPosition, + 0, + ANIMATION_SOURCE.USER, + animationConfigs + ); }, [ animateToPosition, @@ -849,7 +877,12 @@ const BottomSheetComponent = forwardRef( */ isInTemporaryPosition.value = false; - runOnUI(animateToPosition)(nextPosition, 0, animationConfigs); + runOnUI(animateToPosition)( + nextPosition, + 0, + ANIMATION_SOURCE.USER, + animationConfigs + ); }, [ animateToPosition, @@ -889,7 +922,12 @@ const BottomSheetComponent = forwardRef( */ isInTemporaryPosition.value = false; - runOnUI(animateToPosition)(nextPosition, 0, animationConfigs); + runOnUI(animateToPosition)( + nextPosition, + 0, + ANIMATION_SOURCE.USER, + animationConfigs + ); }, [ animateToPosition, @@ -1123,7 +1161,11 @@ const BottomSheetComponent = forwardRef( context.initialPosition >= animatedPosition.value ) { if (context.initialPosition > animatedPosition.value) { - animateToPosition(context.initialPosition, velocityY / 2); + animateToPosition( + context.initialPosition, + velocityY / 2, + ANIMATION_SOURCE.GESTURE + ); } return; } @@ -1221,7 +1263,11 @@ const BottomSheetComponent = forwardRef( return; } - animateToPosition(destinationPoint, velocityY / 2); + animateToPosition( + destinationPoint, + velocityY / 2, + ANIMATION_SOURCE.GESTURE + ); }, [ enablePanDownToClose, @@ -1360,7 +1406,10 @@ const BottomSheetComponent = forwardRef( } return { - height: animate(animatedContentHeight.value, _providedAnimationConfigs), + height: animate({ + point: animatedContentHeight.value, + configs: _providedAnimationConfigs, + }), }; }, [animatedContentHeight, _providedContentHeight]); const contentContainerStyle = useMemo( @@ -1440,7 +1489,7 @@ const BottomSheetComponent = forwardRef( } if (animateOnMount) { - animateToPosition(nextPosition); + animateToPosition(nextPosition, 0, ANIMATION_SOURCE.MOUNT); } else { animatedPosition.value = nextPosition; } @@ -1456,11 +1505,17 @@ const BottomSheetComponent = forwardRef( * @alias OnSnapPointsChange */ useAnimatedReaction( - () => animatedSnapPoints.value, - (_animatedSnapPoints, _previousAnimatedSnapPoints) => { + () => ({ + snapPoints: animatedSnapPoints.value, + containerHeight: animatedContainerHeight.value, + }), + (result, _previousResult) => { + const { snapPoints, containerHeight } = result; + const _previousSnapPoints = _previousResult?.snapPoints; + const _previousContainerHeight = _previousResult?.containerHeight; + if ( - JSON.stringify(_animatedSnapPoints) === - JSON.stringify(_previousAnimatedSnapPoints) || + JSON.stringify(snapPoints) === JSON.stringify(_previousSnapPoints) || !isLayoutCalculated.value || !isAnimatedOnMount.value ) { @@ -1471,16 +1526,13 @@ const BottomSheetComponent = forwardRef( component: BottomSheet.name, method: 'useAnimatedReaction::OnSnapPointChange', params: { - animatedSnapPoints: _animatedSnapPoints, - animatedIndex: animatedIndex.value, - animatedCurrentIndex: animatedCurrentIndex.value, - animatedPosition: animatedPosition.value, - animatedNextPosition: animatedNextPosition.value, - animatedNextPositionIndex: animatedNextPositionIndex.value, + snapPoints, }, }); let nextPosition; + let animationConfig; + let animationSource = ANIMATION_SOURCE.SNAP_POINT_CHANGE; /** * if snap points changed while sheet is animating, then @@ -1489,16 +1541,27 @@ const BottomSheetComponent = forwardRef( if (animatedAnimationState.value === ANIMATION_STATE.RUNNING) { nextPosition = animatedNextPositionIndex.value !== -1 - ? _animatedSnapPoints[animatedNextPositionIndex.value] + ? snapPoints[animatedNextPositionIndex.value] : animatedNextPosition.value; } else if (animatedCurrentIndex.value === -1) { nextPosition = animatedClosedPosition.value; } else if (isInTemporaryPosition.value) { nextPosition = getNextPosition(); } else { - nextPosition = _animatedSnapPoints[animatedCurrentIndex.value]; + nextPosition = snapPoints[animatedCurrentIndex.value]; + + /** + * if snap points changes because of the container height change, + * then we skip the snap animation by setting the duration to 0. + */ + if (containerHeight !== _previousContainerHeight) { + animationSource = ANIMATION_SOURCE.CONTAINER_RESIZE; + animationConfig = { + duration: 0, + }; + } } - animateToPosition(nextPosition); + animateToPosition(nextPosition, 0, animationSource, animationConfig); } ); @@ -1542,7 +1605,12 @@ const BottomSheetComponent = forwardRef( keyboardAnimationDuration.value ); const nextPosition = getNextPosition(); - animateToPosition(nextPosition, 0, animationConfigs); + animateToPosition( + nextPosition, + 0, + ANIMATION_SOURCE.KEYBOARD, + animationConfigs + ); }, [ keyboardBehavior, diff --git a/src/components/bottomSheet/constants.ts b/src/components/bottomSheet/constants.ts index c0922a977..0893f9b15 100644 --- a/src/components/bottomSheet/constants.ts +++ b/src/components/bottomSheet/constants.ts @@ -10,14 +10,6 @@ import { exp } from '../../utilities/easingExp'; // default values const DEFAULT_ANIMATION_EASING: Animated.EasingFunction = Easing.out(exp); const DEFAULT_ANIMATION_DURATION = 500; -const DEFAULT_ANIMATION_CONFIGS: Animated.WithSpringConfig = { - damping: 500, - stiffness: 1000, - mass: 3, - overshootClamping: true, - restDisplacementThreshold: 10, - restSpeedThreshold: 10, -}; const DEFAULT_HANDLE_HEIGHT = 24; const DEFAULT_OVER_DRAG_RESISTANCE_FACTOR = 2.5; @@ -45,7 +37,6 @@ const INITIAL_HANDLE_HEIGHT = -999; const INITIAL_POSITION = WINDOW_HEIGHT; export { - DEFAULT_ANIMATION_CONFIGS, DEFAULT_ANIMATION_EASING, DEFAULT_ANIMATION_DURATION, DEFAULT_HANDLE_HEIGHT, diff --git a/src/constants.ts b/src/constants.ts index b72c86974..4db0c1ccb 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -35,10 +35,14 @@ enum ANIMATION_STATE { STOPPED, } -enum KEYBOARD_STATE { - UNDETERMINED = 0, - SHOWN, - HIDDEN, +enum ANIMATION_SOURCE { + NONE = 0, + MOUNT, + GESTURE, + USER, + CONTAINER_RESIZE, + SNAP_POINT_CHANGE, + KEYBOARD, } enum ANIMATION_METHOD { @@ -46,6 +50,12 @@ enum ANIMATION_METHOD { SPRING, } +enum KEYBOARD_STATE { + UNDETERMINED = 0, + SHOWN, + HIDDEN, +} + const SCROLLABLE_DECELERATION_RATE_MAPPER = { [SCROLLABLE_STATE.LOCKED]: 0, [SCROLLABLE_STATE.UNLOCKED]: Platform.select({ @@ -83,10 +93,11 @@ export { GESTURE_SOURCE, SHEET_STATE, ANIMATION_STATE, + ANIMATION_METHOD, + ANIMATION_SOURCE, SCROLLABLE_TYPE, SCROLLABLE_STATE, KEYBOARD_STATE, - ANIMATION_METHOD, WINDOW_HEIGHT, WINDOW_WIDTH, SCROLLABLE_DECELERATION_RATE_MAPPER, diff --git a/src/utilities/animate.ts b/src/utilities/animate.ts index 69aba3d08..70ee05965 100644 --- a/src/utilities/animate.ts +++ b/src/utilities/animate.ts @@ -1,14 +1,43 @@ -import Animated, { withSpring, withTiming } from 'react-native-reanimated'; +import { Platform } from 'react-native'; +import Animated, { + Easing, + withSpring, + withTiming, +} from 'react-native-reanimated'; import { ANIMATION_METHOD } from '../constants'; -export const animate = ( - point: number, - configs: Animated.WithSpringConfig | Animated.WithTimingConfig, - velocity: number = 0, - onComplete?: (isFinished: boolean) => void -) => { +interface AnimateParams { + point: number; + velocity?: number; + configs?: Animated.WithSpringConfig | Animated.WithTimingConfig; + onComplete?: (isFinished: boolean) => void; +} + +export const animate = ({ + point, + configs = undefined, + velocity = 0, + onComplete, +}: AnimateParams) => { 'worklet'; + if (!configs) { + configs = + Platform.OS === 'android' + ? { + duration: 250, + easing: Easing.out(Easing.exp), + } + : { + damping: 500, + stiffness: 1000, + mass: 3, + overshootClamping: true, + restDisplacementThreshold: 10, + restSpeedThreshold: 10, + }; + } + // detect animation type const type = 'duration' in configs || 'easing' in configs