@mpxjs/webpack-plugin 2.10.15-5 → 2.10.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +433 -0
- package/lib/index.js +2 -5
- package/lib/platform/template/wx/component-config/progress.js +12 -0
- package/lib/platform/template/wx/component-config/unsupported.js +1 -1
- package/lib/platform/template/wx/index.js +3 -1
- package/lib/runtime/components/react/dist/getInnerListeners.js +35 -21
- package/lib/runtime/components/react/dist/mpx-movable-view.jsx +102 -34
- package/lib/runtime/components/react/dist/mpx-portal/portal-manager.jsx +3 -5
- package/lib/runtime/components/react/dist/mpx-progress.jsx +159 -0
- package/lib/runtime/components/react/dist/mpx-scroll-view.jsx +38 -7
- package/lib/runtime/components/react/dist/mpx-swiper.jsx +9 -16
- package/lib/runtime/components/react/dist/mpx-web-view.jsx +20 -1
- package/lib/runtime/components/react/getInnerListeners.ts +41 -22
- package/lib/runtime/components/react/mpx-movable-view.tsx +156 -48
- package/lib/runtime/components/react/mpx-portal/portal-manager.tsx +4 -8
- package/lib/runtime/components/react/mpx-progress.tsx +257 -0
- package/lib/runtime/components/react/mpx-scroll-view.tsx +39 -7
- package/lib/runtime/components/react/mpx-swiper.tsx +9 -16
- package/lib/runtime/components/react/mpx-web-view.tsx +22 -1
- package/lib/runtime/components/react/types/getInnerListeners.d.ts +7 -2
- package/lib/runtime/components/web/mpx-movable-area.vue +43 -19
- package/lib/runtime/components/web/mpx-movable-view.vue +93 -3
- package/lib/runtime/components/web/mpx-swiper.vue +1 -2
- package/lib/runtime/components/web/mpx-web-view.vue +3 -3
- package/lib/template-compiler/compiler.js +61 -31
- package/lib/wxss/utils.js +1 -1
- package/package.json +3 -3
|
@@ -10,11 +10,13 @@ import {
|
|
|
10
10
|
RemoveProps,
|
|
11
11
|
InnerRef,
|
|
12
12
|
LayoutRef,
|
|
13
|
-
ExtendedNativeTouchEvent
|
|
13
|
+
ExtendedNativeTouchEvent,
|
|
14
|
+
GlobalEventState
|
|
14
15
|
} from './types/getInnerListeners'
|
|
15
16
|
|
|
16
|
-
const globalEventState = {
|
|
17
|
-
needPress: true
|
|
17
|
+
const globalEventState: GlobalEventState = {
|
|
18
|
+
needPress: true,
|
|
19
|
+
identifier: null
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
const getTouchEvent = (
|
|
@@ -137,39 +139,54 @@ function checkIsNeedPress (e: ExtendedNativeTouchEvent, type: 'bubble' | 'captur
|
|
|
137
139
|
}
|
|
138
140
|
}
|
|
139
141
|
|
|
142
|
+
function shouldHandleTapEvent (e: ExtendedNativeTouchEvent, eventConfig: EventConfig) {
|
|
143
|
+
const { identifier } = e.nativeEvent.changedTouches[0]
|
|
144
|
+
return eventConfig.tap && globalEventState.identifier === identifier
|
|
145
|
+
}
|
|
146
|
+
|
|
140
147
|
function handleTouchstart (e: ExtendedNativeTouchEvent, type: EventType, eventConfig: EventConfig) {
|
|
141
|
-
// 阻止事件被释放放回对象池,导致对象复用 _stoppedEventTypes 状态被保留
|
|
142
148
|
e.persist()
|
|
143
149
|
const { innerRef } = eventConfig
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
150
|
+
const touch = e.nativeEvent.changedTouches[0]
|
|
151
|
+
const { identifier } = touch
|
|
152
|
+
|
|
153
|
+
const isSingle = e.nativeEvent.touches.length <= 1
|
|
154
|
+
|
|
155
|
+
if (isSingle) {
|
|
156
|
+
// 仅在 touchstart 记录第一个单指触摸点
|
|
157
|
+
globalEventState.identifier = identifier
|
|
158
|
+
globalEventState.needPress = true
|
|
159
|
+
innerRef.current.mpxPressInfo.detail = {
|
|
160
|
+
x: touch.pageX,
|
|
161
|
+
y: touch.pageY
|
|
162
|
+
}
|
|
148
163
|
}
|
|
149
164
|
|
|
150
165
|
handleEmitEvent('touchstart', e, type, eventConfig)
|
|
151
166
|
|
|
152
167
|
if (eventConfig.longpress) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
168
|
+
// 只有单指触摸时才启动长按定时器
|
|
169
|
+
if (isSingle) {
|
|
170
|
+
if (e._stoppedEventTypes?.has('longpress')) {
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
if (eventConfig.longpress.hasCatch) {
|
|
174
|
+
e._stoppedEventTypes = e._stoppedEventTypes || new Set()
|
|
175
|
+
e._stoppedEventTypes.add('longpress')
|
|
176
|
+
}
|
|
177
|
+
innerRef.current.startTimer[type] && clearTimeout(innerRef.current.startTimer[type] as unknown as number)
|
|
178
|
+
innerRef.current.startTimer[type] = setTimeout(() => {
|
|
179
|
+
globalEventState.needPress = false
|
|
180
|
+
handleEmitEvent('longpress', e, type, eventConfig)
|
|
181
|
+
}, 350)
|
|
159
182
|
}
|
|
160
|
-
innerRef.current.startTimer[type] && clearTimeout(innerRef.current.startTimer[type] as unknown as number)
|
|
161
|
-
innerRef.current.startTimer[type] = setTimeout(() => {
|
|
162
|
-
// 只要触发过longpress, 全局就不再触发tap
|
|
163
|
-
globalEventState.needPress = false
|
|
164
|
-
handleEmitEvent('longpress', e, type, eventConfig)
|
|
165
|
-
}, 350)
|
|
166
183
|
}
|
|
167
184
|
}
|
|
168
185
|
|
|
169
186
|
function handleTouchmove (e: ExtendedNativeTouchEvent, type: EventType, eventConfig: EventConfig) {
|
|
170
187
|
const { innerRef } = eventConfig
|
|
171
188
|
handleEmitEvent('touchmove', e, type, eventConfig)
|
|
172
|
-
if (eventConfig
|
|
189
|
+
if (shouldHandleTapEvent(e, eventConfig)) {
|
|
173
190
|
checkIsNeedPress(e, type, innerRef)
|
|
174
191
|
}
|
|
175
192
|
}
|
|
@@ -178,7 +195,9 @@ function handleTouchend (e: ExtendedNativeTouchEvent, type: EventType, eventConf
|
|
|
178
195
|
const { innerRef, disableTap } = eventConfig
|
|
179
196
|
handleEmitEvent('touchend', e, type, eventConfig)
|
|
180
197
|
innerRef.current.startTimer[type] && clearTimeout(innerRef.current.startTimer[type] as unknown as number)
|
|
181
|
-
|
|
198
|
+
|
|
199
|
+
// 只有单指触摸结束时才触发 tap
|
|
200
|
+
if (shouldHandleTapEvent(e, eventConfig)) {
|
|
182
201
|
checkIsNeedPress(e, type, innerRef)
|
|
183
202
|
if (!globalEventState.needPress || (type === 'bubble' && disableTap) || e._stoppedEventTypes?.has('tap')) {
|
|
184
203
|
return
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* ✔ out-of-bounds
|
|
5
5
|
* ✔ x
|
|
6
6
|
* ✔ y
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* ✔ damping
|
|
8
|
+
* ✔ friction
|
|
9
9
|
* ✔ disabled
|
|
10
10
|
* ✘ scale
|
|
11
11
|
* ✘ scale-min
|
|
@@ -27,13 +27,125 @@ import { GestureDetector, Gesture, GestureTouchEvent, GestureStateChangeEvent, P
|
|
|
27
27
|
import Animated, {
|
|
28
28
|
useSharedValue,
|
|
29
29
|
useAnimatedStyle,
|
|
30
|
-
withDecay,
|
|
31
30
|
runOnJS,
|
|
32
31
|
runOnUI,
|
|
33
|
-
|
|
32
|
+
withTiming,
|
|
33
|
+
Easing
|
|
34
34
|
} from 'react-native-reanimated'
|
|
35
35
|
import { collectDataset, noop } from '@mpxjs/utils'
|
|
36
36
|
|
|
37
|
+
// 超出边界处理函数,参考微信小程序的超出边界衰减效果
|
|
38
|
+
const applyBoundaryDecline = (
|
|
39
|
+
newValue: number,
|
|
40
|
+
range: [min: number, max: number]
|
|
41
|
+
): number => {
|
|
42
|
+
'worklet'
|
|
43
|
+
|
|
44
|
+
const decline = (distance: number): number => {
|
|
45
|
+
'worklet'
|
|
46
|
+
return Math.sqrt(Math.abs(distance))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (newValue < range[0]) {
|
|
50
|
+
const overDistance = range[0] - newValue
|
|
51
|
+
return range[0] - decline(overDistance)
|
|
52
|
+
} else if (newValue > range[1]) {
|
|
53
|
+
const overDistance = newValue - range[1]
|
|
54
|
+
return range[1] + decline(overDistance)
|
|
55
|
+
}
|
|
56
|
+
return newValue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 参考微信小程序的弹簧阻尼系统实现
|
|
60
|
+
const withWechatSpring = (
|
|
61
|
+
toValue: number,
|
|
62
|
+
dampingParam = 20,
|
|
63
|
+
callback?: () => void
|
|
64
|
+
) => {
|
|
65
|
+
'worklet'
|
|
66
|
+
|
|
67
|
+
// 弹簧参数计算
|
|
68
|
+
const m = 1 // 质量
|
|
69
|
+
const k = 9 * Math.pow(dampingParam, 2) / 40 // 弹簧系数
|
|
70
|
+
const c = dampingParam // 阻尼系数
|
|
71
|
+
|
|
72
|
+
// 判别式:r = c² - 4mk
|
|
73
|
+
const discriminant = c * c - 4 * m * k
|
|
74
|
+
|
|
75
|
+
// 计算动画持续时间和缓动函数
|
|
76
|
+
let duration: number
|
|
77
|
+
let easingFunction: any
|
|
78
|
+
|
|
79
|
+
if (Math.abs(discriminant) < 0.01) {
|
|
80
|
+
// 临界阻尼 (discriminant ≈ 0)
|
|
81
|
+
// 使用cubic-out模拟临界阻尼的平滑过渡
|
|
82
|
+
duration = Math.max(350, Math.min(800, 2000 / dampingParam))
|
|
83
|
+
easingFunction = Easing.out(Easing.cubic)
|
|
84
|
+
} else if (discriminant > 0) {
|
|
85
|
+
// 过阻尼 (discriminant > 0)
|
|
86
|
+
// 使用指数缓动模拟过阻尼的缓慢收敛
|
|
87
|
+
duration = Math.max(450, Math.min(1000, 2500 / dampingParam))
|
|
88
|
+
easingFunction = Easing.out(Easing.exp)
|
|
89
|
+
} else {
|
|
90
|
+
// 欠阻尼 (discriminant < 0) - 会产生振荡
|
|
91
|
+
// 计算振荡频率和衰减率
|
|
92
|
+
const dampingRatio = c / (2 * Math.sqrt(m * k)) // 阻尼比
|
|
93
|
+
|
|
94
|
+
// 根据阻尼比调整动画参数
|
|
95
|
+
if (dampingRatio < 0.7) {
|
|
96
|
+
// 明显振荡
|
|
97
|
+
duration = Math.max(600, Math.min(1200, 3000 / dampingParam))
|
|
98
|
+
// 创建带振荡的贝塞尔曲线
|
|
99
|
+
easingFunction = Easing.bezier(0.175, 0.885, 0.32, 1.275)
|
|
100
|
+
} else {
|
|
101
|
+
// 轻微振荡
|
|
102
|
+
duration = Math.max(400, Math.min(800, 2000 / dampingParam))
|
|
103
|
+
easingFunction = Easing.bezier(0.25, 0.46, 0.45, 0.94)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return withTiming(toValue, {
|
|
108
|
+
duration,
|
|
109
|
+
easing: easingFunction
|
|
110
|
+
}, callback)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 参考微信小程序friction的惯性动画
|
|
114
|
+
const withWechatDecay = (
|
|
115
|
+
velocity: number,
|
|
116
|
+
currentPosition: number,
|
|
117
|
+
clampRange: [min: number, max: number],
|
|
118
|
+
frictionValue = 2,
|
|
119
|
+
callback?: () => void
|
|
120
|
+
) => {
|
|
121
|
+
'worklet'
|
|
122
|
+
|
|
123
|
+
// 微信小程序friction算法: delta = -1.5 * v² / a, 其中 a = -f * v / |v|
|
|
124
|
+
// 如果friction小于等于0,设置为默认值2
|
|
125
|
+
const validFriction = frictionValue <= 0 ? 2 : frictionValue
|
|
126
|
+
const f = 1000 * validFriction
|
|
127
|
+
const acceleration = velocity !== 0 ? -f * velocity / Math.abs(velocity) : 0
|
|
128
|
+
const delta = acceleration !== 0 ? (-1.5 * velocity * velocity) / acceleration : 0
|
|
129
|
+
|
|
130
|
+
let finalPosition = currentPosition + delta
|
|
131
|
+
|
|
132
|
+
// 边界限制
|
|
133
|
+
if (finalPosition < clampRange[0]) {
|
|
134
|
+
finalPosition = clampRange[0]
|
|
135
|
+
} else if (finalPosition > clampRange[1]) {
|
|
136
|
+
finalPosition = clampRange[1]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 计算动画时长
|
|
140
|
+
const distance = Math.abs(finalPosition - currentPosition)
|
|
141
|
+
const duration = Math.min(1500, Math.max(200, distance * 8))
|
|
142
|
+
|
|
143
|
+
return withTiming(finalPosition, {
|
|
144
|
+
duration,
|
|
145
|
+
easing: Easing.out(Easing.cubic)
|
|
146
|
+
}, callback)
|
|
147
|
+
}
|
|
148
|
+
|
|
37
149
|
interface MovableViewProps {
|
|
38
150
|
children: ReactNode
|
|
39
151
|
style?: Record<string, any>
|
|
@@ -42,6 +154,8 @@ interface MovableViewProps {
|
|
|
42
154
|
y?: number
|
|
43
155
|
disabled?: boolean
|
|
44
156
|
animation?: boolean
|
|
157
|
+
damping?: number
|
|
158
|
+
friction?: number
|
|
45
159
|
id?: string
|
|
46
160
|
changeThrottleTime?:number
|
|
47
161
|
bindchange?: (event: unknown) => void
|
|
@@ -95,6 +209,8 @@ const _MovableView = forwardRef<HandlerRef<View, MovableViewProps>, MovableViewP
|
|
|
95
209
|
inertia = false,
|
|
96
210
|
disabled = false,
|
|
97
211
|
animation = true,
|
|
212
|
+
damping = 20,
|
|
213
|
+
friction = 2,
|
|
98
214
|
'out-of-bounds': outOfBounds = false,
|
|
99
215
|
'enable-var': enableVar,
|
|
100
216
|
'external-var-context': externalVarContext,
|
|
@@ -206,18 +322,12 @@ const _MovableView = forwardRef<HandlerRef<View, MovableViewProps>, MovableViewP
|
|
|
206
322
|
const { x: newX, y: newY } = checkBoundaryPosition({ positionX: Number(x), positionY: Number(y) })
|
|
207
323
|
if (direction === 'horizontal' || direction === 'all') {
|
|
208
324
|
offsetX.value = animation
|
|
209
|
-
?
|
|
210
|
-
duration: 1500,
|
|
211
|
-
dampingRatio: 0.8
|
|
212
|
-
})
|
|
325
|
+
? withWechatSpring(newX, damping)
|
|
213
326
|
: newX
|
|
214
327
|
}
|
|
215
328
|
if (direction === 'vertical' || direction === 'all') {
|
|
216
329
|
offsetY.value = animation
|
|
217
|
-
?
|
|
218
|
-
duration: 1500,
|
|
219
|
-
dampingRatio: 0.8
|
|
220
|
-
})
|
|
330
|
+
? withWechatSpring(newY, damping)
|
|
221
331
|
: newY
|
|
222
332
|
}
|
|
223
333
|
if (bindchange) {
|
|
@@ -404,7 +514,7 @@ const _MovableView = forwardRef<HandlerRef<View, MovableViewProps>, MovableViewP
|
|
|
404
514
|
})
|
|
405
515
|
const runOnJSCallback = useRunOnJSCallback(runOnJSCallbackRef)
|
|
406
516
|
|
|
407
|
-
// 节流版本的
|
|
517
|
+
// 节流版本的change事件触发
|
|
408
518
|
const handleTriggerChangeThrottled = useCallback(({ x, y, type }: { x: number; y: number; type?: string }) => {
|
|
409
519
|
'worklet'
|
|
410
520
|
const now = Date.now()
|
|
@@ -468,7 +578,7 @@ const _MovableView = forwardRef<HandlerRef<View, MovableViewProps>, MovableViewP
|
|
|
468
578
|
const { x } = checkBoundaryPosition({ positionX: newX, positionY: offsetY.value })
|
|
469
579
|
offsetX.value = x
|
|
470
580
|
} else {
|
|
471
|
-
offsetX.value = newX
|
|
581
|
+
offsetX.value = applyBoundaryDecline(newX, draggableXRange.value)
|
|
472
582
|
}
|
|
473
583
|
}
|
|
474
584
|
if (direction === 'vertical' || direction === 'all') {
|
|
@@ -477,7 +587,7 @@ const _MovableView = forwardRef<HandlerRef<View, MovableViewProps>, MovableViewP
|
|
|
477
587
|
const { y } = checkBoundaryPosition({ positionX: offsetX.value, positionY: newY })
|
|
478
588
|
offsetY.value = y
|
|
479
589
|
} else {
|
|
480
|
-
offsetY.value = newY
|
|
590
|
+
offsetY.value = applyBoundaryDecline(newY, draggableYRange.value)
|
|
481
591
|
}
|
|
482
592
|
}
|
|
483
593
|
if (bindchange) {
|
|
@@ -506,18 +616,12 @@ const _MovableView = forwardRef<HandlerRef<View, MovableViewProps>, MovableViewP
|
|
|
506
616
|
if (x !== offsetX.value || y !== offsetY.value) {
|
|
507
617
|
if (x !== offsetX.value) {
|
|
508
618
|
offsetX.value = animation
|
|
509
|
-
?
|
|
510
|
-
duration: 1500,
|
|
511
|
-
dampingRatio: 0.8
|
|
512
|
-
})
|
|
619
|
+
? withWechatSpring(x, damping)
|
|
513
620
|
: x
|
|
514
621
|
}
|
|
515
622
|
if (y !== offsetY.value) {
|
|
516
623
|
offsetY.value = animation
|
|
517
|
-
?
|
|
518
|
-
duration: 1500,
|
|
519
|
-
dampingRatio: 0.8
|
|
520
|
-
})
|
|
624
|
+
? withWechatSpring(y, damping)
|
|
521
625
|
: y
|
|
522
626
|
}
|
|
523
627
|
if (bindchange) {
|
|
@@ -528,38 +632,42 @@ const _MovableView = forwardRef<HandlerRef<View, MovableViewProps>, MovableViewP
|
|
|
528
632
|
}
|
|
529
633
|
}
|
|
530
634
|
} else if (inertia) {
|
|
531
|
-
// 惯性处理
|
|
635
|
+
// 惯性处理 - 使用微信小程序friction算法
|
|
532
636
|
if (direction === 'horizontal' || direction === 'all') {
|
|
533
637
|
xInertialMotion.value = true
|
|
534
|
-
offsetX.value =
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
638
|
+
offsetX.value = withWechatDecay(
|
|
639
|
+
e.velocityX / 10,
|
|
640
|
+
offsetX.value,
|
|
641
|
+
draggableXRange.value,
|
|
642
|
+
friction,
|
|
643
|
+
() => {
|
|
644
|
+
xInertialMotion.value = false
|
|
645
|
+
if (bindchange) {
|
|
646
|
+
runOnJS(runOnJSCallback)('handleTriggerChange', {
|
|
647
|
+
x: offsetX.value,
|
|
648
|
+
y: offsetY.value
|
|
649
|
+
})
|
|
650
|
+
}
|
|
545
651
|
}
|
|
546
|
-
|
|
652
|
+
)
|
|
547
653
|
}
|
|
548
654
|
if (direction === 'vertical' || direction === 'all') {
|
|
549
655
|
yInertialMotion.value = true
|
|
550
|
-
offsetY.value =
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
656
|
+
offsetY.value = withWechatDecay(
|
|
657
|
+
e.velocityY / 10,
|
|
658
|
+
offsetY.value,
|
|
659
|
+
draggableYRange.value,
|
|
660
|
+
friction,
|
|
661
|
+
() => {
|
|
662
|
+
yInertialMotion.value = false
|
|
663
|
+
if (bindchange) {
|
|
664
|
+
runOnJS(runOnJSCallback)('handleTriggerChange', {
|
|
665
|
+
x: offsetX.value,
|
|
666
|
+
y: offsetY.value
|
|
667
|
+
})
|
|
668
|
+
}
|
|
561
669
|
}
|
|
562
|
-
|
|
670
|
+
)
|
|
563
671
|
}
|
|
564
672
|
}
|
|
565
673
|
})
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { useState, useCallback, forwardRef, ForwardedRef, useImperativeHandle, ReactNode, ReactElement } from 'react'
|
|
2
|
-
import { View, StyleSheet } from 'react-native'
|
|
1
|
+
import { useState, useCallback, forwardRef, ForwardedRef, useImperativeHandle, ReactNode, ReactElement, Fragment } from 'react'
|
|
3
2
|
|
|
4
3
|
export type State = {
|
|
5
4
|
portals: Array<{
|
|
@@ -48,13 +47,10 @@ const _PortalManager = forwardRef((props: PortalManagerProps, ref:ForwardedRef<u
|
|
|
48
47
|
|
|
49
48
|
return (
|
|
50
49
|
<>
|
|
51
|
-
{state.portals.map(({ key, children }
|
|
52
|
-
<
|
|
53
|
-
key={key}
|
|
54
|
-
collapsable={false} // Need collapsable=false here to clip the elevations
|
|
55
|
-
style={[StyleSheet.absoluteFill, { zIndex: 1000 + i, pointerEvents: 'box-none' }]}>
|
|
50
|
+
{state.portals.map(({ key, children }) => (
|
|
51
|
+
<Fragment key={key}>
|
|
56
52
|
{children}
|
|
57
|
-
</
|
|
53
|
+
</Fragment>
|
|
58
54
|
))}
|
|
59
55
|
</>
|
|
60
56
|
)
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ✔ percent 进度百分比 0-100
|
|
3
|
+
* ✘ show-info 在进度条右侧显示百分比
|
|
4
|
+
* ✘ border-radius 圆角大小
|
|
5
|
+
* ✘ font-size 右侧百分比字体大小
|
|
6
|
+
* ✔ stroke-width 进度条线的宽度
|
|
7
|
+
* ✔ color 进度条颜色(请使用activeColor)
|
|
8
|
+
* ✔ activeColor 已选择的进度条的颜色
|
|
9
|
+
* ✔ backgroundColor 未选择的进度条的颜色
|
|
10
|
+
* ✔ active 进度条从左往右的动画
|
|
11
|
+
* ✔ active-mode backwards: 动画从头播;forwards:动画从上次结束点接着播
|
|
12
|
+
* ✔ duration 进度增加1%所需毫秒数
|
|
13
|
+
* ✔ bindactiveend 动画完成事件
|
|
14
|
+
*/
|
|
15
|
+
import {
|
|
16
|
+
JSX,
|
|
17
|
+
useRef,
|
|
18
|
+
forwardRef,
|
|
19
|
+
useEffect,
|
|
20
|
+
useState,
|
|
21
|
+
createElement,
|
|
22
|
+
ForwardedRef
|
|
23
|
+
} from 'react'
|
|
24
|
+
import {
|
|
25
|
+
View,
|
|
26
|
+
ViewStyle
|
|
27
|
+
} from 'react-native'
|
|
28
|
+
import Animated, {
|
|
29
|
+
useSharedValue,
|
|
30
|
+
useAnimatedStyle,
|
|
31
|
+
withTiming,
|
|
32
|
+
Easing,
|
|
33
|
+
runOnJS
|
|
34
|
+
} from 'react-native-reanimated'
|
|
35
|
+
|
|
36
|
+
import useInnerProps from './getInnerListeners'
|
|
37
|
+
import useNodesRef, { HandlerRef } from './useNodesRef'
|
|
38
|
+
import { useLayout, useTransformStyle, extendObject } from './utils'
|
|
39
|
+
import Portal from './mpx-portal'
|
|
40
|
+
|
|
41
|
+
export interface ProgressProps {
|
|
42
|
+
percent?: number
|
|
43
|
+
'stroke-width'?: number | string
|
|
44
|
+
color?: string
|
|
45
|
+
activeColor?: string
|
|
46
|
+
backgroundColor?: string
|
|
47
|
+
active?: boolean
|
|
48
|
+
'active-mode'?: 'backwards' | 'forwards'
|
|
49
|
+
duration?: number
|
|
50
|
+
bindactiveend?: (event: any) => void
|
|
51
|
+
style?: ViewStyle & Record<string, any>
|
|
52
|
+
'enable-offset'?: boolean
|
|
53
|
+
'enable-var'?: boolean
|
|
54
|
+
'external-var-context'?: Record<string, any>
|
|
55
|
+
'parent-font-size'?: number
|
|
56
|
+
'parent-width'?: number
|
|
57
|
+
'parent-height'?: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const Progress = forwardRef<
|
|
61
|
+
HandlerRef<View, ProgressProps>,
|
|
62
|
+
ProgressProps
|
|
63
|
+
>((props: ProgressProps, ref: ForwardedRef<HandlerRef<View, ProgressProps>>): JSX.Element => {
|
|
64
|
+
const {
|
|
65
|
+
percent = 0,
|
|
66
|
+
'stroke-width': strokeWidth = 6,
|
|
67
|
+
color,
|
|
68
|
+
activeColor = color || '#09BB07',
|
|
69
|
+
backgroundColor = '#EBEBEB',
|
|
70
|
+
active = false,
|
|
71
|
+
'active-mode': activeMode = 'backwards',
|
|
72
|
+
duration = 30,
|
|
73
|
+
bindactiveend,
|
|
74
|
+
style = {},
|
|
75
|
+
'enable-var': enableVar,
|
|
76
|
+
'external-var-context': externalVarContext,
|
|
77
|
+
'parent-font-size': parentFontSize,
|
|
78
|
+
'parent-width': parentWidth,
|
|
79
|
+
'parent-height': parentHeight
|
|
80
|
+
} = props
|
|
81
|
+
|
|
82
|
+
const nodeRef = useRef(null)
|
|
83
|
+
const propsRef = useRef({})
|
|
84
|
+
propsRef.current = props
|
|
85
|
+
|
|
86
|
+
// 进度值状态
|
|
87
|
+
const [lastPercent, setLastPercent] = useState(0)
|
|
88
|
+
const progressWidth = useSharedValue(0)
|
|
89
|
+
|
|
90
|
+
const {
|
|
91
|
+
normalStyle,
|
|
92
|
+
hasSelfPercent,
|
|
93
|
+
setWidth,
|
|
94
|
+
setHeight,
|
|
95
|
+
hasPositionFixed
|
|
96
|
+
} = useTransformStyle(style, {
|
|
97
|
+
enableVar,
|
|
98
|
+
externalVarContext,
|
|
99
|
+
parentFontSize,
|
|
100
|
+
parentWidth,
|
|
101
|
+
parentHeight
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const { layoutRef, layoutStyle, layoutProps } = useLayout({
|
|
105
|
+
props,
|
|
106
|
+
hasSelfPercent,
|
|
107
|
+
setWidth,
|
|
108
|
+
setHeight,
|
|
109
|
+
nodeRef
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
useNodesRef(props, ref, nodeRef, {
|
|
113
|
+
style: normalStyle
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// 进度条动画函数
|
|
117
|
+
const startProgressAnimation = (targetPercent: number, startPercent: number, animationDuration: number, onFinished?: () => void) => {
|
|
118
|
+
// 根据 active-mode 设置起始位置
|
|
119
|
+
progressWidth.value = startPercent
|
|
120
|
+
progressWidth.value = withTiming(
|
|
121
|
+
targetPercent,
|
|
122
|
+
{
|
|
123
|
+
duration: animationDuration,
|
|
124
|
+
easing: Easing.linear
|
|
125
|
+
},
|
|
126
|
+
(finished) => {
|
|
127
|
+
if (finished && onFinished) {
|
|
128
|
+
// 在动画回调中,需要使用runOnJS回到主线程
|
|
129
|
+
runOnJS(onFinished)()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 创建在主线程执行的事件回调函数
|
|
136
|
+
const triggerActiveEnd = (percent: number) => {
|
|
137
|
+
if (bindactiveend) {
|
|
138
|
+
bindactiveend({
|
|
139
|
+
type: 'activeend',
|
|
140
|
+
detail: {
|
|
141
|
+
percent: percent
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 进度变化时的动画效果
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
const targetPercent = Math.max(0, Math.min(100, percent))
|
|
150
|
+
if (active) {
|
|
151
|
+
// 根据 active-mode 确定起始位置
|
|
152
|
+
let startPercent
|
|
153
|
+
if (activeMode === 'backwards') {
|
|
154
|
+
startPercent = 0
|
|
155
|
+
} else {
|
|
156
|
+
// forwards 模式:使用上次记录的百分比作为起始位置
|
|
157
|
+
startPercent = lastPercent
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 计算动画持续时间
|
|
161
|
+
const percentDiff = Math.abs(targetPercent - startPercent)
|
|
162
|
+
const animationDuration = percentDiff * duration
|
|
163
|
+
|
|
164
|
+
// 创建动画完成回调
|
|
165
|
+
const onAnimationFinished = () => {
|
|
166
|
+
triggerActiveEnd(targetPercent)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 执行动画
|
|
170
|
+
startProgressAnimation(targetPercent, startPercent, animationDuration, onAnimationFinished)
|
|
171
|
+
} else {
|
|
172
|
+
progressWidth.value = targetPercent
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
setLastPercent(targetPercent)
|
|
176
|
+
}, [percent, active, activeMode, duration, bindactiveend])
|
|
177
|
+
|
|
178
|
+
// 初始化时设置进度值
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!active) {
|
|
181
|
+
progressWidth.value = Math.max(0, Math.min(100, percent))
|
|
182
|
+
}
|
|
183
|
+
}, [])
|
|
184
|
+
|
|
185
|
+
// 进度条动画样式
|
|
186
|
+
const animatedProgressStyle = useAnimatedStyle(() => {
|
|
187
|
+
return {
|
|
188
|
+
width: `${progressWidth.value}%`
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
// 确保数值类型正确
|
|
193
|
+
const strokeWidthNum = typeof strokeWidth === 'number' ? strokeWidth : parseInt(strokeWidth as string, 10) || 6
|
|
194
|
+
|
|
195
|
+
// 容器样式
|
|
196
|
+
const containerStyle: ViewStyle = extendObject({} as ViewStyle, {
|
|
197
|
+
flexDirection: 'row' as const,
|
|
198
|
+
alignItems: 'center' as const,
|
|
199
|
+
width: '100%',
|
|
200
|
+
minHeight: Math.max(strokeWidthNum, 20)
|
|
201
|
+
}, normalStyle, layoutStyle)
|
|
202
|
+
|
|
203
|
+
// 进度条背景样式
|
|
204
|
+
const progressBgStyle: ViewStyle = {
|
|
205
|
+
width: '100%',
|
|
206
|
+
height: strokeWidthNum,
|
|
207
|
+
backgroundColor,
|
|
208
|
+
overflow: 'hidden'
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 进度条填充样式
|
|
212
|
+
const progressFillStyle: ViewStyle = {
|
|
213
|
+
height: '100%',
|
|
214
|
+
backgroundColor: activeColor
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const innerProps = useInnerProps(
|
|
218
|
+
extendObject({}, props, layoutProps, {
|
|
219
|
+
ref: nodeRef
|
|
220
|
+
}),
|
|
221
|
+
[
|
|
222
|
+
'percent',
|
|
223
|
+
'stroke-width',
|
|
224
|
+
'color',
|
|
225
|
+
'activeColor',
|
|
226
|
+
'backgroundColor',
|
|
227
|
+
'active',
|
|
228
|
+
'active-mode',
|
|
229
|
+
'duration',
|
|
230
|
+
'bindactiveend'
|
|
231
|
+
],
|
|
232
|
+
{ layoutRef }
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
const progressComponent = createElement(
|
|
236
|
+
View,
|
|
237
|
+
extendObject({}, innerProps, { style: containerStyle }),
|
|
238
|
+
// 进度条背景
|
|
239
|
+
createElement(
|
|
240
|
+
View,
|
|
241
|
+
{ style: progressBgStyle },
|
|
242
|
+
// 进度条填充
|
|
243
|
+
createElement(Animated.View, {
|
|
244
|
+
style: [progressFillStyle, animatedProgressStyle]
|
|
245
|
+
})
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if (hasPositionFixed) {
|
|
250
|
+
return createElement(Portal, null, progressComponent)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return progressComponent
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
Progress.displayName = 'MpxProgress'
|
|
257
|
+
export default Progress
|