-
Notifications
You must be signed in to change notification settings - Fork 390
Expand file tree
/
Copy pathuseTransitionHooks.ts
More file actions
335 lines (331 loc) · 13.8 KB
/
Copy pathuseTransitionHooks.ts
File metadata and controls
335 lines (331 loc) · 13.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
import { hasOwn, dash2hump, error, warn } from '@mpxjs/utils'
import { useMemo, useRef, useEffect } from 'react'
import {
Easing,
makeMutable,
runOnJS,
useSharedValue,
useAnimatedStyle,
cancelAnimation
} from 'react-native-reanimated'
import {
easingKey,
transitionSupportedProperty,
transformInitial,
cubicBezierExp,
secondRegExp,
percentExp,
getTransformObj,
getUnit,
getInitialVal,
getAnimation,
isTransform
} from './utils'
import { parseValues, useRunOnJSCallback } from '../utils'
import type { SharedValue, AnimatableValue, EasingFunction } from 'react-native-reanimated'
import type { TransformsStyle } from 'react-native'
import type { ExtendedViewStyle } from '../types/common'
import type { AnimationHooksPropsType, TransitionMap, TimingFunction } from './utils'
type AnimationDataType = {
property?: string
duration?: number
delay?: number
easing: EasingFunction
}
const propName = {
transition: '',
transitionDuration: 'duration',
transitionProperty: 'property',
transitionTimingFunction: 'easing',
transitionDelay: 'delay'
}
const behaviorExp = /^(allow-discrete|normal)$/
const defaultValueExp = /^(inherit|initial|revert|revert-layer|unset)$/
const timingFunctionExp = /^(step-start|step-end|steps)/
// cubic-bezier 参数解析
function getBezierParams (str: string) {
// ease 0.25, 0.1, 0.25, 1.0
return str.match(cubicBezierExp)?.[1]?.split(',').map(item => +item)
}
// 解析 transition-prop
function parseTransitionSingleProp (vals: string[], property: string) {
let setDuration = false
property = propName[property as keyof typeof propName]
return vals.map(val => {
// transition-property all
if (val === 'all') {
error('[Mpx runtime error]: the value of transition-property is not supported \'all\'')
return undefined
}
// behavior
if (behaviorExp.test(val)) {
error('[Mpx runtime error]: transition-behavior is not supported')
return undefined
}
// global values
if (defaultValueExp.test(val)) {
error('[Mpx runtime error]: global values is not supported')
return undefined
}
if (timingFunctionExp.test(val)) {
error('[Mpx runtime error]: the timingFunction in step-start,step-end,steps() is not supported')
return undefined
}
// timingFunction
if (Object.keys(easingKey).includes(val) || cubicBezierExp.test(val)) {
const bezierParams = getBezierParams(val)
return {
easing: bezierParams?.length ? Easing.bezier(bezierParams[0], bezierParams[1], bezierParams[2], bezierParams[3]) : easingKey[val as TimingFunction] || Easing.inOut(Easing.ease)
}
}
// duration & delay
if (secondRegExp.test(val)) {
const newProperty = property || (!setDuration ? 'duration' : 'delay')
setDuration = true
// console.log('parseTransitionSingleProp val=', val, property, setDuration)
return {
[newProperty]: getUnit(val)
}
}
// property
return {
property: dash2hump(val)
}
}).filter(item => item !== undefined)
}
// transition 解析
function parseTransitionStyle (originalStyle: ExtendedViewStyle) {
let transitionData: AnimationDataType[] = []
Object.entries(originalStyle).filter(arr => arr[0].includes('transition')).forEach(([prop, value]) => {
if (prop === 'transition') {
const vals = parseValues(value, ',').map(item => {
return parseTransitionSingleProp(parseValues(item), prop).reduce((map, subItem) => {
return Object.assign(map, subItem)
}, {} as AnimationDataType)
})
// console.log(`parseTransitionStyle ${prop}=${value} formatVal=`, vals)
if (transitionData.length) {
transitionData = (vals.length > transitionData.length ? vals : transitionData).map((transitionItem, i) => {
const valItem = vals[i] || {}
const current = transitionData[i] || {}
// console.log('parseTransitionStyle current=', current)
// console.log('parseTransitionStyle valItem=', valItem)
// console.log('parseTransitionStyle mergeObj=', Object.assign({}, current, valItem))
return Object.assign({}, current, valItem)
})
// console.log(`parseTransitionStyle ${prop}=${value}, transitionData=`, transitionData)
} else {
transitionData = vals
}
} else {
const vals = parseTransitionSingleProp(parseValues(value, ','), prop)
// console.log(`parseTransitionStyle ${prop}=${value} formatVal=`, vals)
// formatVal [{"property": "transform"}, {"property": "marginLeft"}]
if (transitionData.length) {
transitionData = (vals.length > transitionData.length ? vals : transitionData).map((transitionItem, i) => {
const valItem = vals[i] || vals[vals.length - 1]
const current = transitionData[i] || transitionData[transitionData.length - 1]
// console.log('parseTransitionStyle current=', current)
// console.log('parseTransitionStyle valItem=', valItem)
// console.log('parseTransitionStyle mergeObj=', Object.assign({}, current, valItem))
return Object.assign({}, current, valItem)
})
// console.log(`parseTransitionStyle ${prop}=${value}, transitionData=`, transitionData)
} else {
transitionData = vals as AnimationDataType[]
}
// transitionData.push(...vals)
}
})
// console.log(`parseTransitionStyle transitionData=`, transitionData)
// 从style 中解析的动画数据,结构如下:
// transitionMap= {"marginLeft": {"delay": 0, "duration": 3000, "easing": []}, "transform": {"delay": 0, "duration": 3000, "easing": []}}
const transitionMap = transitionData.reduce((acc, cur) => {
// hasOwn(transitionSupportedProperty, dash2hump(val)) || val === Transform
const { property = '', duration = 0, delay = 0, easing = Easing.inOut(Easing.ease) } = cur
if ((hasOwn(transitionSupportedProperty, dash2hump(property)) || property === 'transform') && duration >= 0) {
acc[property] = {
duration,
delay,
easing
}
}
return acc
}, {} as TransitionMap)
// console.log(`parseTransitionStyle transitionMap=`, transitionMap)
return transitionMap
}
const transitionKeys = ['transition', 'transitionDuration', 'transitionTimingFunction', 'transitionDelay', 'transitionProperty'] as const
function getTransitionPropertyKeys (map: TransitionMap): string {
return Object.keys(map).sort().join(',')
}
export default function useTransitionHooks<T, P> (props: AnimationHooksPropsType) {
// console.log(`useTransitionHooks, props=`, props)
const { style: originalStyle = {}, transitionend } = props
// style变更标识(首次render不执行),初始值为0,首次渲染后为1
const animationDeps = useRef(0)
// transition 时序属性动态更新追踪
const lastTransitionStyleRef = useRef<Record<string, any>>(
transitionKeys.reduce((acc, key) => { acc[key] = originalStyle[key]; return acc }, {} as Record<string, any>)
)
const prevTransitionMapRef = useRef<TransitionMap | null>(null)
// ** 从 style 中获取动画数据(支持动态更新 transitionDuration/transitionDelay/transitionTimingFunction)
const transitionMap = useMemo(() => {
const prevStyle = lastTransitionStyleRef.current
// 检测 transition 时序属性是否变化并更新
let hasChanged = false
transitionKeys.forEach(key => {
if (prevStyle[key] !== originalStyle[key]) {
hasChanged = true
prevStyle[key] = originalStyle[key]
}
})
// 时序属性未变化且非首次计算,跳过重新解析
if (!hasChanged && prevTransitionMapRef.current) {
return prevTransitionMapRef.current
}
const newTransitionMap = parseTransitionStyle(originalStyle)
if (!prevTransitionMapRef.current) {
// 首次计算
prevTransitionMapRef.current = newTransitionMap
return newTransitionMap
}
// 检测 transitionProperty 是否变化,变化时直接返回上一次的 transitionMap
if (getTransitionPropertyKeys(newTransitionMap) !== getTransitionPropertyKeys(prevTransitionMapRef.current)) {
error('[Mpx runtime error]: dynamic setting transitionProperty is not supported')
return prevTransitionMapRef.current
}
prevTransitionMapRef.current = newTransitionMap
return newTransitionMap
}, [originalStyle])
// ** style prop sharedValue interpolateOutput: SharedValue<InterpolateOutput>
const { shareValMap, animatedKeys, animatedStyleKeys } = useMemo(() => {
// 记录需要执行动画的 propName
const animatedKeys = [] as string[]
// 有动画样式的 style key(useAnimatedStyle使用)
const animatedStyleKeys = [] as (string|string[])[]
const transforms = [] as string[]
const shareValMap = Object.keys(transitionMap).reduce((valMap, property) => {
// const { property } = transition || {}
if (property === 'transform') {
Object.keys(originalStyle.transform ? getTransformObj(originalStyle.transform!) : transformInitial).forEach((key) => {
const defaultVal = getInitialVal(originalStyle, key)
// console.log(`shareValMap property=${key} defaultVal=${defaultVal}`)
valMap[key] = makeMutable(defaultVal)
animatedKeys.push(key)
transforms.push(key)
})
} else if (hasOwn(transitionSupportedProperty, property)) {
const defaultVal = getInitialVal(originalStyle, property)
// console.log(`shareValMap property=${property} defaultVal=${defaultVal}`)
valMap[property] = makeMutable(defaultVal)
animatedKeys.push(property)
animatedStyleKeys.push(property)
}
// console.log('shareValMap = ', valMap)
return valMap
}, {} as { [propName: keyof ExtendedViewStyle]: SharedValue<string|number> })
if (transforms.length) animatedStyleKeys.push(transforms)
return {
shareValMap,
animatedKeys,
animatedStyleKeys
}
}, [])
const runOnJSCallbackRef = useRef({})
const runOnJSCallback = useRunOnJSCallback(runOnJSCallbackRef)
// 根据 animation action 创建&驱动动画
function createAnimation () {
let transformTransitionendDone = false
animatedKeys.forEach(key => {
// console.log(`createAnimation key=${key} originalStyle=`, originalStyle)
const isTransformKey = isTransform(key)
let ruleV = originalStyle[key]
if (isTransformKey) {
const transform = getTransformObj(originalStyle.transform!)
ruleV = transform[key]
}
let toVal = ruleV !== undefined
? ruleV
: transitionSupportedProperty[key]
const shareVal = shareValMap[key].value
if (percentExp.test(`${toVal}`) && !percentExp.test(shareVal as string) && !isNaN(+shareVal)) {
// 获取到的toVal为百分比格式化shareValMap为百分比
shareValMap[key].value = `${shareVal as number * 100}%`
} else if (percentExp.test(shareVal as string) && !percentExp.test(toVal as string) && !isNaN(+toVal)) {
// 初始值为百分比则格式化toVal为百分比
toVal = `${toVal * 100}%`
} else if (typeof toVal !== typeof shareVal) {
// 动画起始值和终态值类型不一致报错提示一下
warn(`[Mpx runtime error]: Value types of property ${key} must be consistent during the animation`)
}
if ((toVal === 'auto' && !isNaN(+shareVal)) || (shareVal === 'auto' && !isNaN(+toVal))) {
// 有 auto 直接赋值不做动画
shareValMap[key].value = toVal
} else {
// console.log(`key=${key} oldVal=${shareValMap[key].value} newVal=${toVal}`)
const { delay = 0, duration, easing } = transitionMap[isTransformKey ? 'transform' : key]
// console.log('animationOptions=', { delay, duration, easing })
let callback
if (transitionend && (!isTransformKey || !transformTransitionendDone)) {
runOnJSCallbackRef.current = {
animationCallback: (duration: number, finished: boolean, current?: AnimatableValue) => {
transitionend(finished, current, duration)
}
}
callback = (finished?: boolean, current?: AnimatableValue) => {
'worklet'
// 动画结束后设置下一次transformOrigin
if (finished) {
runOnJS(runOnJSCallback)('animationCallback', duration, finished, current)
}
}
}
const animation = getAnimation({ key, value: toVal! }, { delay, duration, easing }, callback)
// Todo transform 有多个属性时也仅执行一次 transitionend(对齐wx)
if (isTransformKey) {
transformTransitionendDone = true
}
shareValMap[key].value = animation
}
// console.log(`useTransitionHooks, ${key}=`, animation)
})
}
// ** style 更新
useEffect(() => {
// console.log('useEffect originalStyle animationDeps=', animationDeps.current, originalStyle)
// 首次不执行
if (!animationDeps.current) {
animationDeps.current = 1
return
}
createAnimation()
}, [originalStyle])
// ** 清空动画
useEffect(() => {
return () => {
Object.values(shareValMap).forEach((value) => {
cancelAnimation(value)
})
}
}, [])
// ** 生成动画样式
return useAnimatedStyle(() => {
// console.info(`useAnimatedStyle styles=`, originalStyle)
return animatedStyleKeys.reduce((styles, key) => {
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
}
// console.log('animationStyle', styles)
return styles
}, {} as ExtendedViewStyle)
})
}