Skip to content

Commit c983123

Browse files
committed
支持 reanimated 4.x 改造
1 parent b6b789c commit c983123

File tree

5 files changed

+245
-35
lines changed

5 files changed

+245
-35
lines changed
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { error, dash2hump } from '@mpxjs/utils'
2+
import { Easing } from 'react-native-reanimated'
3+
// ms s 单位匹配
4+
const secondRegExp = /^\s*(\d*(?:\.\d+)?)(s|ms)\s*$/
5+
const cubicBezierExp = /cubic-bezier\(["']?(.*?)["']?\)/
6+
const behaviorExp = /^(allow-discrete|normal)$/
7+
// const percentExp = /^((-?(\d+(\.\d+)?|\.\d+))%)$/
8+
const defaultValueExp = /^(inherit|initial|revert|revert-layer|unset)$/
9+
// Todo linear() https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/easing-function/linear
10+
const timingFunctionExp = /^(step-start|step-end|steps|(linear\())/
11+
const directionExp = /^(normal|reverse|alternate|alternate-reverse)$/
12+
const fillModeExp = /^(none|forwards|backwards|both)$/
13+
const easingKey = {
14+
linear: Easing.linear,
15+
ease: Easing.inOut(Easing.ease),
16+
'ease-in': Easing.in(Easing.poly(3)),
17+
'ease-in-out': Easing.inOut(Easing.poly(3)),
18+
'ease-out': Easing.out(Easing.poly(3))
19+
// 'step-start': '',
20+
// 'step-end': ''
21+
}
22+
// cubic-bezier 参数解析
23+
function getBezierParams (str) {
24+
// ease 0.25, 0.1, 0.25, 1.0
25+
return str.match(cubicBezierExp)?.[1]?.split(',').map(item => +item)
26+
}
27+
// 解析动画时长
28+
function getUnit (duration) {
29+
const match = secondRegExp.exec(duration)
30+
return match ? match[2] === 's' ? +match[1] * 1000 : +match[1] : 0
31+
}
32+
// 多value解析
33+
function parseValues (str, char = ' ') {
34+
let stack = 0
35+
let temp = ''
36+
const result = []
37+
for (let i = 0; i < str.length; i++) {
38+
if (str[i] === '(') {
39+
stack++
40+
} else if (str[i] === ')') {
41+
stack--
42+
}
43+
// 非括号内 或者 非分隔字符且非空
44+
if (stack !== 0 || str[i] !== char) {
45+
temp += str[i]
46+
}
47+
if ((stack === 0 && str[i] === char) || i === str.length - 1) {
48+
result.push(temp.trim())
49+
temp = ''
50+
}
51+
}
52+
return result
53+
}
54+
// 解析 animation-prop
55+
function parseAnimationSingleProp (vals, property = '') {
56+
let setDuration = false
57+
return vals.map(value => {
58+
// global values
59+
if (defaultValueExp.test(value)) {
60+
error('[Mpx runtime error]: global values is not supported')
61+
return undefined
62+
}
63+
if (timingFunctionExp.test(value)) {
64+
error('[Mpx runtime error]: the timingFunction in step-start,step-end,steps(),liner() is not supported')
65+
return undefined
66+
}
67+
// timingFunction
68+
if (Object.keys(easingKey).includes(value) || cubicBezierExp.test(value)) {
69+
const bezierParams = getBezierParams(value)
70+
return {
71+
prop: 'animationTimingFunction',
72+
value: bezierParams?.length
73+
? Easing.bezier(bezierParams[0], bezierParams[1], bezierParams[2], bezierParams[3])
74+
: easingKey[value] || Easing.inOut(Easing.ease)
75+
}
76+
}
77+
// direction
78+
if (directionExp.test(value)) {
79+
return {
80+
prop: 'animationDirection',
81+
value
82+
}
83+
}
84+
// fill-mode
85+
if (fillModeExp.test(value)) {
86+
return {
87+
prop: 'animationFillMode',
88+
value
89+
}
90+
}
91+
// duration & delay
92+
if (secondRegExp.test(value)) {
93+
const newProperty = property || (!setDuration ? 'animationDuration' : 'animationDelay')
94+
setDuration = true
95+
return {
96+
prop: newProperty,
97+
value: getUnit(value)
98+
}
99+
}
100+
if (!isNaN(+value)) {
101+
return {
102+
prop: 'animationIterationCount',
103+
value: +value
104+
}
105+
}
106+
// name
107+
return {
108+
prop: 'animationName',
109+
value
110+
}
111+
}).filter(item => item !== undefined)
112+
}
113+
// 解析 transition-prop
114+
function parseTransitionSingleProp (vals, property = '') {
115+
let setDuration = false
116+
return vals.map(value => {
117+
// global values
118+
if (defaultValueExp.test(value)) {
119+
error('[Mpx runtime error]: global values is not supported')
120+
return undefined
121+
}
122+
if (timingFunctionExp.test(value)) {
123+
error('[Mpx runtime error]: the timingFunction in step-start,step-end,steps() is not supported')
124+
return undefined
125+
}
126+
// timingFunction
127+
if (Object.keys(easingKey).includes(value) || cubicBezierExp.test(value)) {
128+
const bezierParams = getBezierParams(value)
129+
return {
130+
prop: 'transitionTimingFunction',
131+
value: bezierParams?.length ? Easing.bezier(bezierParams[0], bezierParams[1], bezierParams[2], bezierParams[3]) : easingKey[val] || Easing.inOut(Easing.ease)
132+
}
133+
}
134+
// behavior
135+
if (behaviorExp.test(value)) {
136+
return {
137+
prop: 'transitionBehavior',
138+
value
139+
}
140+
}
141+
// duration & delay
142+
if (secondRegExp.test(value)) {
143+
const newProperty = property || (!setDuration ? 'transitionDuration' : 'transitionDelay')
144+
setDuration = true
145+
return {
146+
prop: newProperty,
147+
value: getUnit(value)
148+
}
149+
}
150+
// property
151+
return {
152+
prop: 'transitionProperty',
153+
value: dash2hump(value)
154+
}
155+
}).filter(item => item !== undefined)
156+
}
157+
// animation & transition 解析
158+
export function parseAnimationStyle (originalStyle, cssProp = 'animation') {
159+
let animationData = {}
160+
Object.entries(originalStyle).forEach(([propName, value]) => {
161+
if (!propName.includes(cssProp)) return
162+
if (propName === cssProp) {
163+
const vals = parseValues(value, ',').reduce((map, item, idx) => {
164+
(cssProp === 'animation' ? parseAnimationSingleProp(parseValues(item)) : parseTransitionSingleProp(parseValues(item))).forEach(({ prop, value }) => {
165+
if (map[prop]) {
166+
map[prop].push(value)
167+
} else {
168+
map[prop] = [value]
169+
}
170+
})
171+
return map
172+
}, {})
173+
Object.entries(vals).forEach(([prop, vals]) => {
174+
if (animationData[prop]?.length) {
175+
(animationData[prop].length >= vals.length ? animationData[prop] : vals).forEach((item,idx) => {
176+
if (animationData[prop][idx] && vals[idx]) {
177+
animationData[prop][idx] = vals[idx]
178+
} else if (vals[idx]) {
179+
animationData[prop].push(vals[idx])
180+
}
181+
})
182+
} else {
183+
console.error(prop, vals, 999333)
184+
animationData[prop] = vals
185+
}
186+
})
187+
console.error('animation style, ', vals)
188+
} else {
189+
const vals = (cssProp === 'transition' ? parseTransitionSingleProp(parseValues(value, ','), propName) : parseAnimationSingleProp(parseValues(value, ','), propName)).reduce((acc, { prop, value }) => {
190+
acc.push(value)
191+
return acc
192+
}, [])
193+
console.error(`${propName} style, `, vals)
194+
if (animationData[propName]?.length) {
195+
(animationData[propName].length >= vals.length ? animationData[propName] : vals).forEach((item,idx) => {
196+
if (animationData[propName][idx] && vals[idx]) {
197+
animationData[propName][idx] = vals[idx]
198+
} else if (vals[idx]) {
199+
animationData[propName].push(vals[idx])
200+
}
201+
})
202+
// console.error(animationData[prop], 999111)
203+
} else {
204+
animationData[propName] = vals
205+
}
206+
}
207+
})
208+
return animationData
209+
}

packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { isObject, isArray, dash2hump, cached, isEmptyObject, hasOwn, getFocused
22
import { StyleSheet, Dimensions } from 'react-native'
33
import { reactive } from '../../observer/reactive'
44
import Mpx from '../../index'
5+
import { parseAnimationStyle } from './parse-animation'
56

67
global.__mpxAppDimensionsInfo = {
78
window: Dimensions.get('window'),
@@ -320,20 +321,28 @@ export default function styleHelperMixin () {
320321
})
321322
}
322323
const isEmpty = isNativeStaticStyle ? !result.length : isEmptyObject(result)
323-
if (hasOwn(result, 'animation') || hasOwn(result, 'animationName')) {
324-
let keyframes, appKeyframes
325-
if (keyframes = this.__getClassStyle?.('keyframes')) {
326-
mergeResult({
327-
keyframes
328-
})
329-
console.error(result)
330-
} else if (appKeyframes = global.__getAppClassStyle?.('keyframes')) {
331-
// console.error('appStyle=', appStyle)
332-
mergeResult({
333-
keyframes
324+
// parse animation
325+
if (hasOwn(result, 'animationName') || hasOwn(result, 'animation')) {
326+
const animationData = parseAnimationStyle(result)
327+
// console.error('parse animation result: ', animationData)
328+
const keyframes = {}
329+
if (animationData.animationName?.length) {
330+
animationData.animationName.forEach(animationName => {
331+
const _keyframe = this.__getClassStyle?.(animationName) || global.__getAppClassStyle?.(animationName)
332+
_keyframe && (keyframes[animationName] = _keyframe)
334333
})
335334
}
335+
mergeResult(animationData, keyframes)
336+
delete result.animation
337+
}
338+
// parse transition
339+
if (hasOwn(result, 'transitionName') || hasOwn(result, 'transition')) {
340+
const transitionData = parseAnimationStyle(result, 'transition')
341+
// console.error('parse transition result: ', transitionData)
342+
mergeResult(transitionData)
343+
delete result.transition
336344
}
345+
// console.error('styleHelperMixin: result=', result)
337346
return isEmpty ? empty : result
338347
}
339348
}

packages/webpack-plugin/lib/react/style-helper.js

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const getRulesRunner = require('../platform/index')
55
const dash2hump = require('../utils/hump-dash').dash2hump
66
const parseValues = require('../utils/string').parseValues
77
const unitRegExp = /^\s*(-?\d+(?:\.\d+)?)(rpx|vw|vh|px)?\s*$/
8-
const percentExp = /^((-?(\d+(\.\d+)?|\.\d+))%)$/
8+
// const percentExp = /^((-?(\d+(\.\d+)?|\.\d+))%)$/
99
const hairlineRegExp = /^\s*hairlineWidth\s*$/
1010
const varRegExp = /^--/
1111
const cssPrefixExp = /^-(webkit|moz|ms|o)-/
@@ -119,17 +119,18 @@ function getClassMap ({ content, filename, mode, srcMode, ctorType, formatValueN
119119
if (selector.nodes.length === 1 && (selector.nodes[0].type === 'class')) {
120120
classMapKeys.push(selector.nodes[0].value)
121121
} else if (ruleName === 'keyframes' && selector.nodes[0].type === 'tag') {
122+
// 动画帧参数
122123
const value = selector.nodes[0].value
123-
const val = value.match(percentExp)?.[2] / 100
124+
// const val = value.match(percentExp)?.[2] / 100
124125
if (value === 'from') {
125126
// from
126-
classMapKeys.push(0)
127+
classMapKeys.push('0%')
127128
} else if (value === 'to') {
128129
// to
129-
classMapKeys.push(1)
130-
} else if (!isNaN(val)) {
130+
classMapKeys.push('100%')
131+
} else {
131132
// 百分比
132-
classMapKeys.push(val)
133+
classMapKeys.push(value)
133134
}
134135
} else {
135136
error('Only single class selector is supported in react native mode temporarily.')
@@ -189,12 +190,9 @@ function getClassMap ({ content, filename, mode, srcMode, ctorType, formatValueN
189190
})
190191
})
191192
if (ruleName === 'keyframes') {
192-
if (Object.keys(ruleClassMap).length > 0) {
193-
const animationName = rule.params
194-
if (!classMap.keyframes) {
195-
classMap.keyframes = {}
196-
}
197-
classMap.keyframes[animationName] = ruleClassMap
193+
const animationName = rule.params
194+
if (Object.keys(ruleClassMap).length > 0 && animationName) {
195+
classMap[animationName] = ruleClassMap
198196
}
199197
}
200198
})

packages/webpack-plugin/lib/runtime/components/react/animationHooks/index.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { error, collectDataset, hasOwn } from '@mpxjs/utils'
22
import { useRef } from 'react'
33
import useAnimationAPIHooks from './useAnimationAPIHooks'
4-
import useTransitionHooks from './useTransitionHooks'
5-
import useCssAnimationHooks from './useCssAnimationHooks'
64
import type { AnimatableValue } from 'react-native-reanimated'
75
import type { MutableRefObject } from 'react'
86
import type { NativeSyntheticEvent } from 'react-native'
@@ -15,15 +13,15 @@ export default function useAnimationHooks<T, P> (props: _ViewProps & { enableAni
1513
const { style: originalStyle = {}, enableAnimation, animation, transitionend, layoutRef } = props
1614
// 记录动画类型
1715
let animationType = ''
18-
if (hasOwn(originalStyle, 'animation') || (hasOwn(originalStyle, 'animationName') && hasOwn(originalStyle, 'animationDuration'))) {
16+
if (hasOwn(originalStyle, 'animationName') && hasOwn(originalStyle, 'animationDuration')) {
1917
// css animation 只做检测提示
2018
animationType = 'animation'
2119
}
2220
if (!!animation || enableAnimation === true) {
2321
animationType = 'api'
2422
}
2523
// 优先级 css transition > API
26-
if (hasOwn(originalStyle, 'transition') || (hasOwn(originalStyle, 'transitionProperty') && hasOwn(originalStyle, 'transitionDuration'))) {
24+
if (hasOwn(originalStyle, 'transitionProperty') && hasOwn(originalStyle, 'transitionDuration')) {
2725
animationType = 'transition'
2826
}
2927
// 优先以 enableAnimation 定义类型为准
@@ -67,13 +65,9 @@ export default function useAnimationHooks<T, P> (props: _ViewProps & { enableAni
6765
}
6866
return {
6967
enableStyleAnimation: !!animationTypeRef.current,
70-
animationStyle: animationTypeRef.current === 'animation'
68+
animationStyle: animationTypeRef.current === 'api'
7169
// eslint-disable-next-line react-hooks/rules-of-hooks
72-
? useCssAnimationHooks(hooksProps)
73-
: animationTypeRef.current === 'api'
74-
// eslint-disable-next-line react-hooks/rules-of-hooks
75-
? useAnimationAPIHooks(hooksProps)
76-
// eslint-disable-next-line react-hooks/rules-of-hooks
77-
: useTransitionHooks(hooksProps)
70+
? useAnimationAPIHooks(hooksProps)
71+
: undefined
7872
}
7973
}

packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -775,7 +775,7 @@ const _View = forwardRef<HandlerRef<View, _ViewProps>, _ViewProps>((viewProps, r
775775
layoutProps,
776776
{
777777
ref: nodeRef,
778-
style: enableStyleAnimation ? [viewStyle, animationStyle] : viewStyle
778+
style: animationStyle ? [viewStyle, animationStyle] : viewStyle
779779
}
780780

781781
),

0 commit comments

Comments
 (0)