UNPKG

@mpxjs/webpack-plugin

Version:

mpx compile core

321 lines (313 loc) 11.9 kB
import { useEffect, useMemo, useRef } from 'react' import type { MutableRefObject } from 'react' import type { NativeSyntheticEvent, TransformsStyle } from 'react-native' import { Easing, useSharedValue, withTiming, useAnimatedStyle, withSequence, withDelay, makeMutable, cancelAnimation, runOnJS } from 'react-native-reanimated' import type { AnimationCallback, WithTimingConfig, SharedValue, AnimatableValue } from 'react-native-reanimated' import { error, hasOwn, collectDataset } from '@mpxjs/utils' import { useRunOnJSCallback } from './utils' import { ExtendedViewStyle } from './types/common' import type { _ViewProps } from './mpx-view' type AnimatedOption = { duration: number delay: number useNativeDriver: boolean timingFunction: 'linear' | 'ease' | 'ease-in' | 'ease-in-out'| 'ease-out' transformOrigin: string } type ExtendWithTimingConfig = WithTimingConfig & { delay: number } export type AnimationStepItem = { animatedOption: AnimatedOption rules: Map<string, number | string> transform: Map<string, number> } export type AnimationProp = { id: number, actions: AnimationStepItem[] } // 微信 timingFunction 和 RN Easing 对应关系 const EasingKey = { linear: Easing.linear, ease: Easing.inOut(Easing.ease), 'ease-in': Easing.in(Easing.poly(3)), 'ease-in-out': Easing.inOut(Easing.poly(3)), 'ease-out': Easing.out(Easing.poly(3)) // 'step-start': '', // 'step-end': '' } const TransformInitial: ExtendedViewStyle = { // matrix: 0, // matrix3d: 0, // rotate: '0deg', rotateX: '0deg', rotateY: '0deg', rotateZ: '0deg', // rotate3d:[0,0,0] // scale: 1, // scale3d: [1, 1, 1], scaleX: 1, scaleY: 1, // scaleZ: 1, // skew: 0, skewX: '0deg', skewY: '0deg', // translate: 0, // translate3d: 0, translateX: 0, translateY: 0 // translateZ: 0, } // 动画默认初始值 const InitialValue: ExtendedViewStyle = Object.assign({ opacity: 1, backgroundColor: 'transparent', width: 0, height: 0, top: 0, right: 0, bottom: 0, left: 0, transformOrigin: ['50%', '50%', 0] }, TransformInitial) const TransformOrigin = 'transformOrigin' // transform const isTransform = (key: string) => Object.keys(TransformInitial).includes(key) // transform 数组转对象 function getTransformObj (transforms: { [propName: string]: string | number }[]) { 'worklet' return transforms.reduce((transformObj, item) => { return Object.assign(transformObj, item) }, {} as { [propName: string]: string | number }) } export default function useAnimationHooks<T, P> (props: _ViewProps & { enableAnimation?: boolean, layoutRef: MutableRefObject<any>, transitionend?: (event: NativeSyntheticEvent<TouchEvent> | unknown) => void }) { const { style: originalStyle = {}, animation, enableAnimation, transitionend, layoutRef } = props const enableStyleAnimation = enableAnimation || !!animation const enableAnimationRef = useRef(enableStyleAnimation) if (enableAnimationRef.current !== enableStyleAnimation) { error('[Mpx runtime error]: animation use should be stable in the component lifecycle, or you can set [enable-animation] with true.') } if (!enableAnimationRef.current) return { enableStyleAnimation: false } // id 标识 const id = animation?.id || -1 // 有动画样式的 style key // eslint-disable-next-line react-hooks/rules-of-hooks const animatedStyleKeys = useSharedValue([] as (string|string[])[]) // 记录动画key的style样式值 没有的话设置为false // eslint-disable-next-line react-hooks/rules-of-hooks const animatedKeys = useRef({} as {[propName: keyof ExtendedViewStyle]: boolean}) // 记录上次style map // eslint-disable-next-line react-hooks/rules-of-hooks const lastStyleRef = useRef({} as {[propName: keyof ExtendedViewStyle]: number|string}) // ** 全量 style prop sharedValue // 不能做增量的原因: // 1 尝试用 useRef,但 useAnimatedStyle 访问后的 ref 不能在增加新的值,被冻结 // 2 尝试用 useSharedValue,因为实际触发的 style prop 需要是 sharedValue 才能驱动动画,若外层 shareValMap 也是 sharedValue,动画无法驱动。 // eslint-disable-next-line react-hooks/rules-of-hooks const shareValMap = useMemo(() => { return Object.keys(InitialValue).reduce((valMap, key) => { const defaultVal = getInitialVal(key, isTransform(key)) valMap[key] = makeMutable(defaultVal) return valMap }, {} as { [propName: keyof ExtendedViewStyle]: SharedValue<string|number> }) }, []) // ** style更新同步 // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { // style 更新后同步更新 lastStyleRef & shareValMap updateStyleVal() }, [originalStyle]) // ** 获取动画样式prop & 驱动动画 // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { if (id === -1) return // 更新动画样式 key map animatedKeys.current = getAnimatedStyleKeys() const keys = Object.keys(animatedKeys.current) animatedStyleKeys.value = formatAnimatedKeys([TransformOrigin, ...keys]) // 驱动动画 createAnimation(keys) }, [id]) // ** 清空动画 // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { return () => { Object.values(shareValMap).forEach((value) => { cancelAnimation(value) }) } }, []) // 根据 animation action 创建&驱动动画 function createAnimation (animatedKeys: string[] = []) { const actions = animation?.actions || [] const sequence = {} as { [propName: keyof ExtendedViewStyle]: (string|number)[] } const lastValueMap = {} as { [propName: keyof ExtendedViewStyle]: string|number } actions.forEach(({ animatedOption, rules, transform }, index) => { const { delay, duration, timingFunction, transformOrigin } = animatedOption const easing = EasingKey[timingFunction] || Easing.inOut(Easing.quad) let needSetCallback = true const setTransformOrigin: AnimationCallback = (finished: boolean) => { 'worklet' // 动画结束后设置下一次transformOrigin if (finished) { if (index < actions.length - 1) { const transformOrigin = actions[index + 1].animatedOption?.transformOrigin transformOrigin && (shareValMap[TransformOrigin].value = transformOrigin) } } } if (index === 0) { // 设置当次中心 shareValMap[TransformOrigin].value = transformOrigin } // 添加每个key的多次step动画 animatedKeys.forEach(key => { const ruleV = isTransform(key) ? transform.get(key) : rules.get(key) // key不存在,第一轮取shareValMap[key]value,非第一轮取上一轮的 const toVal = ruleV !== undefined ? ruleV : index > 0 ? lastValueMap[key] : shareValMap[key].value const animation = getAnimation({ key, value: toVal! }, { delay, duration, easing }, needSetCallback ? setTransformOrigin : undefined) needSetCallback = false if (!sequence[key]) { sequence[key] = [animation] } else { sequence[key].push(animation) } // 更新一下 lastValueMap lastValueMap[key] = toVal! }) // 赋值驱动动画 animatedKeys.forEach((key) => { const animations = sequence[key] shareValMap[key].value = withSequence(...animations) }) }) } function withTimingCallback (finished?: boolean, current?: AnimatableValue, duration?: number) { if (!transitionend) return const target = { id: animation?.id || -1, dataset: collectDataset(props), offsetLeft: layoutRef?.current?.offsetLeft || 0, offsetTop: layoutRef?.current?.offsetTop || 0 } transitionend({ type: 'transitionend', // elapsedTime 对齐wx 单位s detail: { elapsedTime: duration ? duration / 1000 : 0, finished, current }, target, currentTarget: target, timeStamp: Date.now() }) } // eslint-disable-next-line react-hooks/rules-of-hooks const runOnJSCallbackRef = useRef({ withTimingCallback }) // eslint-disable-next-line react-hooks/rules-of-hooks const runOnJSCallback = useRunOnJSCallback(runOnJSCallbackRef) // 创建单个animation function getAnimation ({ key, value }: { key: string, value: string|number }, { delay, duration, easing }: ExtendWithTimingConfig, callback?: AnimationCallback) { const animation = typeof callback === 'function' ? withTiming(value, { duration, easing }, (finished, current) => { callback(finished, current) if (transitionend && finished) { runOnJS(runOnJSCallback)('withTimingCallback', finished, current, duration) } }) : withTiming(value, { duration, easing }) return delay ? withDelay(delay, animation) : animation } // 获取样式初始值(prop style or 默认值) function getInitialVal (key: keyof ExtendedViewStyle, isTransform = false) { if (isTransform && Array.isArray(originalStyle.transform)) { let initialVal = InitialValue[key] // 仅支持 { transform: [{rotateX: '45deg'}, {rotateZ: '0.785398rad'}] } 格式的初始样式 originalStyle.transform.forEach(item => { if (item[key] !== undefined) initialVal = item[key] }) return initialVal } return originalStyle[key] === undefined ? InitialValue[key] : originalStyle[key] } // 循环 animation actions 获取所有有动画的 style prop name function getAnimatedStyleKeys () { return (animation?.actions || []).reduce((keyMap, action) => { const { rules, transform } = action const ruleArr = [...rules.keys(), ...transform.keys()] ruleArr.forEach(key => { if (!keyMap[key]) keyMap[key] = true }) return keyMap }, animatedKeys.current) } // animated key transform 格式化 function formatAnimatedKeys (keys: string[]) { const animatedKeys = [] as (string|string[])[] const transforms = [] as string[] keys.forEach(key => { if (isTransform(key)) { transforms.push(key) } else { animatedKeys.push(key) } }) if (transforms.length) animatedKeys.push(transforms) return animatedKeys } // 设置 lastShareValRef & shareValMap function updateStyleVal () { Object.entries(originalStyle).forEach(([key, value]) => { if (key === 'transform') { Object.entries(getTransformObj(value)).forEach(([key, value]) => { if (value !== lastStyleRef.current[key]) { lastStyleRef.current[key] = value shareValMap[key].value = value } }) } else if (hasOwn(shareValMap, key)) { if (value !== lastStyleRef.current[key]) { lastStyleRef.current[key] = value shareValMap[key].value = value } } }) } // ** 生成动画样式 // eslint-disable-next-line react-hooks/rules-of-hooks const animationStyle = useAnimatedStyle(() => { // console.info(`useAnimatedStyle styles=`, originalStyle) return animatedStyleKeys.value.reduce((styles, key) => { // console.info('getAnimationStyles', key, shareValMap[key].value) if (Array.isArray(key)) { const transformStyle = getTransformObj(originalStyle.transform || []) key.forEach((transformKey) => { transformStyle[transformKey] = shareValMap[transformKey].value }) styles.transform = Object.entries(transformStyle).map(([key, value]) => { return { [key]: value } }) as Extract<'transform', TransformsStyle> } else { styles[key] = shareValMap[key].value } return styles }, {} as ExtendedViewStyle) }) return { enableStyleAnimation: enableAnimationRef.current, animationStyle } }