Skip to content

Commit

Permalink
feat: performant, dynamic-animation hook
Browse files Browse the repository at this point in the history
  • Loading branch information
nandorojo committed Mar 22, 2021
1 parent 5c9618c commit 8f65da7
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 1 deletion.
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
54 changes: 53 additions & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>
state?: Pick<UseAnimationState<any>, '__state'>
/**
* This is not a prop you will likely find yourself using.
*
Expand Down Expand Up @@ -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<AnimateType, 'transform'> & Partial<Transforms>,
AnimateWithTransitions = StyleValueWithReplacedTransforms<AnimateType>
// allow the style values to be arrays for sequences, where values are primitives or objects with configs
> = NonNullable<StyleValueWithSequenceArrays<AnimateWithTransitions>>

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<any>
/**
* 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
}
81 changes: 81 additions & 0 deletions packages/core/src/use-dynamic-animation/index.ts
Original file line number Diff line number Diff line change
@@ -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<UseDynamicAnimationState>()

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
}

0 comments on commit 8f65da7

Please sign in to comment.