feat: allow modify animation configs (#333)

* chore: modify animation hooks

* refactor: updated bottom sheet animation handling

* refactor: updated bottom sheet modal

* chore: updated examples
This commit is contained in:
Mo Gorhom
2021-03-07 13:59:07 +00:00
committed by GitHub
parent 1caf96ed48
commit f78a663e89
9 changed files with 202 additions and 113 deletions

View File

@@ -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<BottomSheetModal>(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 = () => {
/>
<BottomSheetModal
ref={bottomSheetRef}
snapPoints={['25%', '50%']}
snapPoints={snapPoints}
animationDuration={250}
onDismiss={handleDismiss}
onChange={handleChange}

View File

@@ -22,7 +22,6 @@ import Animated, {
Extrapolate,
runOnUI,
useWorkletCallback,
withTiming,
} from 'react-native-reanimated';
import { State } from 'react-native-gesture-handler';
import {
@@ -42,7 +41,13 @@ import BottomSheetHandleContainer from '../bottomSheetHandleContainer';
import BottomSheetBackgroundContainer from '../bottomSheetBackgroundContainer';
import BottomSheetDraggableView from '../bottomSheetDraggableView';
// import BottomSheetDebugView from '../bottomSheetDebugView';
import { GESTURE, ANIMATION_STATE, WINDOW_HEIGHT } from '../../constants';
import {
GESTURE,
ANIMATION_STATE,
WINDOW_HEIGHT,
ANIMATION_METHOD,
} from '../../constants';
import { animate } from '../../utilities';
import {
DEFAULT_ANIMATION_EASING,
DEFAULT_ANIMATION_DURATION,
@@ -74,8 +79,8 @@ const BottomSheetComponent = forwardRef<BottomSheet, BottomSheetProps>(
//#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<BottomSheet, BottomSheetProps>(
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<BottomSheet, BottomSheetProps>(
//#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<BottomSheet, BottomSheetProps>(
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<BottomSheet, BottomSheetProps>(
isLayoutCalculated &&
didMountOnAnimate.current === false &&
isClosing.current === false &&
snapPoints[_providedIndex] !== safeContainerHeight
snapPoints[_providedIndex] !== safeContainerHeight &&
_providedIndex !== -1
) {
const newSnapPoint = snapPoints[_providedIndex];
requestAnimationFrame(() => runOnUI(animateToPoint)(newSnapPoint));

View File

@@ -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(() => {
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;
}
if (dismissOnPanDown) {
bottomSheetRef.current?.snapTo(1);
} else {
bottomSheetRef.current?.collapse();
}
}, [dismissOnPanDown]);
const handleClose = useCallback(() => {
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;

View File

@@ -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,
};

View File

@@ -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<Animated.WithSpringConfig, 'velocity'>
) => {
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);
};

View File

@@ -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);
};

38
src/types.d.ts vendored
View File

@@ -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

22
src/utilities/animate.ts Normal file
View File

@@ -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);
};
}
};

View File

@@ -1 +1,2 @@
export { normalizeSnapPoints } from './normalizeSnapPoints';
export { animate } from './animate';