import React, { useCallback, useEffect, useImperativeHandle, useRef } from 'react'; import RNAnimated, { AnimatableValue, AnimatableValueObject, Easing, runOnJS, useAnimatedRef, useAnimatedStyle, useSharedValue, withDelay, withSequence, withSpring, withTiming, } from 'react-native-reanimated'; import { useNativeMergeStyles } from '@cleartrip/ct-design-style-manager'; import { IAnimatedComponentProps, IAnimatedStyle, IAnimationConfig, IAnimationOptions, IAnimationTransformValues, ISpringConfig, SpringConfig, } from '../type'; import { IAnimatedStyleClass } from '../AnimatedStyle/IAnimatedStyleClass'; import { ViewStyle, TextStyle, ImageStyle } from '@cleartrip/ct-design-types'; type DefaultStyle = ViewStyle | TextStyle | ImageStyle; const isEmpty = (value: unknown): boolean => { if (value == null) return true; if (Array.isArray(value)) return value.length === 0; if (typeof value === 'object') return Object.keys(value).length === 0; return false; }; const getAnimatedValue = (animatedStyle: IAnimationTransformValues) => { const animatedValues: AnimatableValue = {}; if (typeof animatedStyle?.translateX !== 'undefined') { animatedValues.translateX = animatedStyle.translateX; } if (typeof animatedStyle?.translateY !== 'undefined') { animatedValues.translateY = animatedStyle.translateY; } if (typeof animatedStyle?.scale !== 'undefined') { animatedValues.scale = animatedStyle.scale; } if (typeof animatedStyle?.opacity !== 'undefined') { animatedValues.opacity = animatedStyle.opacity; } if (typeof animatedStyle?.height !== 'undefined') { animatedValues.height = animatedStyle.height; } if (typeof animatedStyle?.width !== 'undefined') { animatedValues.width = animatedStyle.width; } if (typeof animatedStyle?.top !== 'undefined') { animatedValues.top = parseInt(animatedStyle.top as string, 10); } if (typeof animatedStyle?.left !== 'undefined') { animatedValues.left = parseInt(animatedStyle.left as string, 10); } if (typeof animatedStyle?.color !== 'undefined') { animatedValues.color = animatedStyle.color; } if (typeof animatedStyle?.backgroundColor !== 'undefined') { animatedValues.backgroundColor = animatedStyle.backgroundColor; } return animatedValues; }; const getEasing = (timingFunction: string) => { switch (timingFunction) { case 'ease-in-out': return Easing.inOut(Easing.ease); case 'ease-in': return Easing.in(Easing.ease); case 'ease-out': return Easing.out(Easing.ease); } return Easing.linear; }; const getSpringConfig = (springConfig?: ISpringConfig) => { if (springConfig) { const { mass, tension, friction } = springConfig; const config: SpringConfig = {}; if (mass) { config.mass = mass; } if (tension) { config.stiffness = tension; } if (friction) { config.damping = friction; } return config; } return {}; }; const getAnimatedStyle = (animated: IAnimatedStyleClass) => { if (animated.getNativeIsSequence?.()) { const animatedStyles = animated.getNativeAnimatedSequenceStyles(); const firstAnimatedStyle = animatedStyles[0][0]; if (!firstAnimatedStyle) { return null; } return firstAnimatedStyle.getNativeAnimatedStyle(); } return animated.getNativeAnimatedStyle(); }; const getInitialAnimatedValue = (animated: IAnimatedStyleClass) => { let initialAnimatedValue: IAnimationTransformValues = {}; if (animated.getNativeIsSequence()) { const sequenceStyles = animated.getNativeAnimatedSequenceStyles(); for (let i = 0; i < sequenceStyles.length; i++) { const animatedStyle = sequenceStyles[i][0]; const animatedStyleValue = animatedStyle.getNativeAnimatedStyle(); if (animatedStyleValue && animatedStyleValue.from && !isEmpty(animatedStyleValue.from)) { Object.keys(animatedStyleValue.from).forEach((key) => { if ( typeof initialAnimatedValue[key as keyof typeof initialAnimatedValue] === 'undefined' && animatedStyleValue.from?.[key as keyof IAnimationTransformValues] !== 'undefined' ) { initialAnimatedValue[key as keyof IAnimationTransformValues] = animatedStyleValue.from?.[key as keyof IAnimationTransformValues]; } }); } } } else { initialAnimatedValue = animated.getNativeAnimatedStyle()?.from ?? {}; } const initialValue: IAnimationTransformValues = {}; Object.keys(initialAnimatedValue).forEach((key) => { if (typeof initialAnimatedValue[key as keyof typeof initialAnimatedValue] !== 'undefined') { initialValue[key as keyof IAnimationTransformValues] = initialAnimatedValue[key]; } }); return initialValue; }; const Container = ({ children, animated, styleConfig = {}, onTransitionEnd }: IAnimatedComponentProps) => { const { root: rootStyles = [] } = styleConfig || {}; const { to, config: animatedConfig } = getAnimatedStyle(animated) || {}; // ref to onTransitionEnd so it can be accessed from worklet context const onTransitionEndRef = useRef(onTransitionEnd); useEffect(() => { onTransitionEndRef.current = onTransitionEnd; }, [onTransitionEnd]); const animatedValue = useSharedValue(getInitialAnimatedValue(animated)); const animatedStyle = useAnimatedStyle(() => { const style: IAnimatedStyle = {}; if (typeof animatedValue.value.height !== 'undefined') { style.height = animatedValue.value.height; } if (typeof animatedValue.value.width !== 'undefined') { style.width = animatedValue.value.width; } if (typeof animatedValue.value.opacity !== 'undefined') { style.opacity = animatedValue.value.opacity; } if (typeof animatedValue.value.color !== 'undefined') { style.color = animatedValue.value.color; } if (typeof animatedValue.value.backgroundColor !== 'undefined') { style.backgroundColor = animatedValue.value.backgroundColor; } if (typeof animatedValue.value.top !== 'undefined') { style.top = parseInt(String(animatedValue.value.top), 10); } if (typeof animatedValue.value.left !== 'undefined') { style.left = parseInt(String(animatedValue.value.left), 10); } const transform = []; if (typeof animatedValue.value.translateX !== 'undefined') { // @ts-ignore transform.push({ translateX: animatedValue.value.translateX }); } if (typeof animatedValue.value.translateY !== 'undefined') { // @ts-ignore transform.push({ translateY: animatedValue.value.translateY }); } if (typeof animatedValue.value.scale !== 'undefined') { // @ts-ignore transform.push({ scale: animatedValue.value.scale }); } if (transform.length > 0) { style.transform = transform; } return style as DefaultStyle; }); const containerRef = useAnimatedRef(); useEffect(() => { if (animatedConfig?.immediate) { animated.getNativeElementRef()?.current?.start?.(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleAnimationStart = useCallback( (animatedValues: IAnimationTransformValues = {}, config: IAnimationConfig = {}) => { const { duration = 1000, timingFunction = 'ease-in', delay, animationType, springConfig, onComplete, } = config ?? {}; const targetValues = getAnimatedValue(animatedValues); const completeTargetValues = animated.getNativeIsSequence() ? { ...(typeof animatedValue.value.translateX !== 'undefined' && { translateX: animatedValue.value.translateX, }), ...(typeof animatedValue.value.translateY !== 'undefined' && { translateY: animatedValue.value.translateY, }), ...(typeof animatedValue.value.scale !== 'undefined' && { scale: animatedValue.value.scale }), ...(typeof animatedValue.value.opacity !== 'undefined' && { opacity: animatedValue.value.opacity }), ...(typeof animatedValue.value.height !== 'undefined' && { height: animatedValue.value.height }), ...(typeof animatedValue.value.width !== 'undefined' && { width: animatedValue.value.width }), ...targetValues, } : targetValues; // Both callbacks need to be wrapped with runOnJS since they're called from worklet context const handleComplete = (finished?: boolean) => { 'worklet'; if (finished) { if (onComplete) { runOnJS(onComplete)(); } if (onTransitionEndRef.current) { runOnJS(onTransitionEndRef.current)(); } } }; let animation: AnimatableValueObject; if (animationType === 'spring') { animation = withSpring(completeTargetValues, getSpringConfig(springConfig), handleComplete); } else { animation = withTiming(completeTargetValues, { duration, easing: getEasing(timingFunction) }, handleComplete); } const withDelayAnimation = delay ? withDelay(delay, animation) : animation; if (animated.getNativeIsSequence()) { return withDelayAnimation; } animatedValue.value = withDelayAnimation as IAnimationTransformValues; }, // eslint-disable-next-line react-hooks/exhaustive-deps [], ); const handleSequenceAnimationStart = (animated: IAnimatedStyleClass) => { const sequenceStyles = []; let accumulatedToValues = { ...(typeof animatedValue.value.translateX !== 'undefined' && { translateX: animatedValue.value.translateX }), ...(typeof animatedValue.value.translateY !== 'undefined' && { translateY: animatedValue.value.translateY }), ...(typeof animatedValue.value.scale !== 'undefined' && { scale: animatedValue.value.scale }), ...(typeof animatedValue.value.opacity !== 'undefined' && { opacity: animatedValue.value.opacity }), ...(typeof animatedValue.value.height !== 'undefined' && { height: animatedValue.value.height }), ...(typeof animatedValue.value.width !== 'undefined' && { width: animatedValue.value.width }), }; for (let i = 0; i < animated.getNativeAnimatedSequenceStyles().length; i++) { const animatedStyle = animated.getNativeAnimatedSequenceStyles()[i][0]; const animatedStyleValue = animatedStyle.getNativeAnimatedStyle(); const toValues = animatedStyleValue?.to ?? {}; accumulatedToValues = { ...accumulatedToValues, ...toValues, }; // @ts-ignore sequenceStyles.push(handleAnimationStart(accumulatedToValues, animatedStyleValue?.config)); } animatedValue.value = withSequence( ...(sequenceStyles as unknown as AnimatableValue[]), ) as IAnimationTransformValues; }; useImperativeHandle( animated.getNativeElementRef(), () => { return { start(animatedStyle?: IAnimationOptions) { if (containerRef?.current) { if (animated.getNativeIsSequence()) { handleSequenceAnimationStart(animated); } else { if (animatedStyle) { const { to = {}, config = animatedConfig } = animatedStyle; handleAnimationStart(to, config); } else { handleAnimationStart(to, animatedConfig); } } } }, }; }, // eslint-disable-next-line react-hooks/exhaustive-deps [], ); // @ts-ignore const nativeStyle = useNativeMergeStyles(rootStyles, [rootStyles]); return ( {children} ); }; export default React.memo(Container);