Skip to content

Commit 33992dd

Browse files
authored
Merge pull request #2169 from dos1in/master
feat: 添加 rn 模式下的 progress 组件
2 parents cfa8fa1 + fef0033 commit 33992dd

File tree

4 files changed

+298
-1
lines changed

4 files changed

+298
-1
lines changed

docs-vitepress/guide/platform/rn.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,34 @@ movable-view的可移动区域。
980980
| bindlinechange | 输入框行数变化时调用,`event.detail = { height: 0, lineCount: 0 }`,不支持 `heightRpx` |
981981
| bind:selectionchange | 选区改变事件, `event.detail = {selectionStart, selectionEnd}` |
982982

983+
#### progress
984+
进度条。
985+
986+
属性
987+
988+
| 属性名 | 类型 | 默认值 | 说明 |
989+
| ----------------------- | ------- | ------------- | ---------------------------------------------------------- |
990+
| percent | Number | `0` | 百分比进度,范围0-100 |
991+
| stroke-width | Number\|String | `6` | 进度条线的宽度,单位px |
992+
| color | String | | 进度条颜色(已废弃,请使用 activeColor) |
993+
| activeColor | String | `#09BB07` | 已选择的进度条的颜色 |
994+
| backgroundColor | String | `#EBEBEB` | 未选择的进度条的颜色 |
995+
| active | Boolean | `false` | 进度条从左往右的动画 |
996+
| active-mode | String | `backwards` | 动画播放模式,`backwards`: 从头开始播放;`forwards`: 从上次结束点接着播放 |
997+
| duration | Number | `30` | 进度增加1%所需毫秒数 |
998+
999+
事件
1000+
1001+
| 事件名 | 说明 |
1002+
| ----------------| --------------------------------------------------- |
1003+
| bindactiveend | 动画完成时触发,`event.detail = { percent }` |
1004+
1005+
注意事项
1006+
1007+
1. 不支持 `show-info` 属性,即不支持在进度条右侧显示百分比
1008+
2. 不支持 `border-radius` 属性自定义圆角大小
1009+
3. 不支持 `font-size` 属性设置右侧百分比字体大小
1010+
9831011
#### picker-view
9841012

9851013
嵌入页面的滚动选择器。其中只可放置 [*picker-view-column*](#picker-view-column) 组件,其它节点不会显示

packages/webpack-plugin/lib/platform/template/wx/component-config/progress.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ module.exports = function ({ print }) {
1717
el.isBuiltIn = true
1818
return 'mpx-progress'
1919
},
20+
android (tag, { el }) {
21+
el.isBuiltIn = true
22+
return 'mpx-progress'
23+
},
24+
harmony (tag, { el }) {
25+
el.isBuiltIn = true
26+
return 'mpx-progress'
27+
},
28+
ios (tag, { el }) {
29+
el.isBuiltIn = true
30+
return 'mpx-progress'
31+
},
2032
props: [
2133
{
2234
test: /^(border-radius|font-size|color|active-mode|duration)$/,

packages/webpack-plugin/lib/platform/template/wx/component-config/unsupported.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const JD_UNSUPPORTED_TAG_NAME_ARR = ['functional-page-navigator', 'live-pusher',
1111
// 快应用不支持的标签集合
1212
const QA_UNSUPPORTED_TAG_NAME_ARR = ['movable-view', 'movable-area', 'open-data', 'official-account', 'editor', 'functional-page-navigator', 'live-player', 'live-pusher', 'ad', 'cover-image']
1313
// RN不支持的标签集合
14-
const RN_UNSUPPORTED_TAG_NAME_ARR = ['open-data', 'official-account', 'editor', 'functional-page-navigator', 'live-player', 'live-pusher', 'ad', 'progress', 'slider', 'audio', 'camera', 'match-media', 'page-container', 'editor', 'keyboard-accessory', 'map']
14+
const RN_UNSUPPORTED_TAG_NAME_ARR = ['open-data', 'official-account', 'editor', 'functional-page-navigator', 'live-player', 'live-pusher', 'ad', 'slider', 'audio', 'camera', 'match-media', 'page-container', 'editor', 'keyboard-accessory', 'map']
1515

1616
/**
1717
* @param {function(object): function} print
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

0 commit comments

Comments
 (0)