From 8f65da78fd913f87e089ddbc3c402fc42e41d9d2 Mon Sep 17 00:00:00 2001 From: Fernando Rojo Date: Mon, 22 Mar 2021 13:13:24 -0400 Subject: [PATCH] feat: performant, dynamic-animation hook --- packages/core/src/index.ts | 1 + packages/core/src/types.ts | 54 ++++++++++++- .../core/src/use-dynamic-animation/index.ts | 81 +++++++++++++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/use-dynamic-animation/index.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ed27ec1..1ed7025 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,5 +4,6 @@ export { AnimatePresence } from 'framer-motion' export * from './types' // export * from './use-animator/types' export { default as useAnimationState } from './use-animator' +export { default as useDynamicAnimation } from './use-dynamic-animation' export * from './use-map-animate-to-style' export * from './constants' diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 608ea7e..593c9b7 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -206,7 +206,7 @@ export interface MotiProps< * * If you know your styles in advance, and will be changing them throughout a component's lifecycle, then this is the preferred method to animate with. */ - state?: UseAnimationState + state?: Pick, '__state'> /** * This is not a prop you will likely find yourself using. * @@ -313,3 +313,55 @@ export type UseAnimationStateConfig< */ to?: ToKey } + +/** + * Used for `useDynamicAnimation` + */ +export type DynamicStyleProp< + // Style props of the component + // defaults to any styles, so that generics aren't Required + // in component usage, it will extract these from the style prop ideally + AnimateType = ImageStyle & ViewStyle & TextStyle, + // edit the style props to remove transform array, flattening it + // AnimateWithTransitions = Omit & Partial, + AnimateWithTransitions = StyleValueWithReplacedTransforms + // allow the style values to be arrays for sequences, where values are primitives or objects with configs +> = NonNullable> + +export type UseDynamicAnimationState = { + /** + * @private + * Internal state used to drive animations. You shouldn't use this. Use `.current` instead to read the current state. Use `animateTo` to edit it. + */ + __state: Animated.SharedValue + /** + * Read the current "state" (i.e. style object) + */ + current: null | DynamicStyleProp + /** + * Set a new animation state using dynamic values. + * + * ```js + * const dynamicAnimation = useDynamicAnimation({ opacity: 0 }) + * + * const onPress = () => { + * dynamicAnimation.animateTo({ opacity: 1 }) + * } + * + * const onMergeStyle = () => { + * // or, merge your styles + * // this uses the previous state, like useState from react + * dynamicAnimation.animateTo((current) => ({ ...current, scale: 1 })) + * + * // you can also synchronously read the current value + * // these two options are the same! + * dynamicAnimation.animateTo({ ...dynamicAnimation.current, scale: 1 }) + * } + * ``` + */ + animateTo: ( + key: + | DynamicStyleProp + | ((currentState: DynamicStyleProp) => DynamicStyleProp) + ) => void +} diff --git a/packages/core/src/use-dynamic-animation/index.ts b/packages/core/src/use-dynamic-animation/index.ts new file mode 100644 index 0000000..5b00827 --- /dev/null +++ b/packages/core/src/use-dynamic-animation/index.ts @@ -0,0 +1,81 @@ +import type { DynamicStyleProp, UseDynamicAnimationState } from './../types' +import { useSharedValue } from 'react-native-reanimated' +import { useRef } from 'react' + +type InitialState = () => DynamicStyleProp + +const fallback = () => ({}) + +/** + * A hook that acts like `useAnimationState`, except that it allows for dynamic values rather than static variants. + * + * This is useful when you want to update styles on the fly the way you do with `useState`. + * + * You can change the state by calling `state.animateTo()`, and access the current state by calling `state.current`. + * + * This hook has high performance, triggers no state changes, and runs fully on the native thread! + * + * ```js + * const dynamicAnimation = useDynamicAnimation({ opacity: 0 }) + * + * const onPress = () => { + * dynamicAnimation.animateTo({ opacity: 1 }) + * } + * + * const onMergeStyle = () => { + * // or, merge your styles + * // this uses the previous state, like useState from react + * dynamicAnimation.animateTo((current) => ({ ...current, scale: 1 })) + * + * // you can also synchronously read the current value + * // these two options are the same! + * dynamicAnimation.animateTo({ ...dynamicAnimation.current, scale: 1 }) + * } + * ``` + * + * @param initialState A function that returns your initial style. Similar to `useState`'s initial style. + */ +export default function useDynamicAnimation( + initialState: InitialState = fallback +) { + const activeStyle = useRef<{ value: DynamicStyleProp }>({ + value: null as any, + }) + if (activeStyle.current.value === null) { + // use a .value to be certain it's never been set + activeStyle.current.value = initialState() + } + + const __state = useSharedValue( + activeStyle.current.value, + false // don't rebuild it (for older versions) + ) + + const controller = useRef() + + if (controller.current === null) { + controller.current = { + __state, + get current(): DynamicStyleProp { + return activeStyle.current.value + }, + animateTo(nextStateOrFunction) { + const runAnimation = (nextStyleObject: DynamicStyleProp) => { + if (nextStyleObject) { + activeStyle.current.value = nextStyleObject + __state.value = nextStyleObject as any + } + } + + if (typeof nextStateOrFunction === 'function') { + // similar to setState, let people compose a function that takes in the current value and returns the next one + runAnimation(nextStateOrFunction(this.current as DynamicStyleProp)) + } else { + runAnimation(nextStateOrFunction) + } + }, + } + } + + return controller.current as UseDynamicAnimationState +}