HarmonyOS 6.1 进度指示器不简单 — Progress五种样式与加载状态管理
对应Demo: ProgressDemo | 难度: 中级 | 关键词: Progress, LoadingProgress, ProgressType, setInterval, 加载状态
你以为进度条就是设个value就完了?我之前也这么想,直到一个下载进度页面被测试报了"进度条到100%还在转"和"返回页面后定时器没停导致后台狂刷"两个bug,才发现Progress组件的水比想象中深。HarmonyOS NEXT给了Progress五种样式加一个LoadingProgress,每种都有它的适用场景和暗坑,今天一起讲清楚。

Progress的五种样式
先看Progress组件的基本用法,它通过type参数区分样式:
// Linear - 条形进度条
Progress({ value: this.progressValue, total: 100, type: ProgressType.Linear })
.width('90%')
.color('#1a73e8')
// Ring - 环形进度
Progress({ value: this.ringValue, total: 100, type: ProgressType.Ring })
.width(80).height(80)
.color('#e65100')
// ScaleRing - 刻度环
Progress({ value: this.ringValue, total: 100, type: ProgressType.ScaleRing })
.width(80).height(80)
.color('#34a853')
// Capsule - 胶囊进度
Progress({ value: this.ringValue, total: 100, type: ProgressType.Capsule })
.width(80).height(80)
.color('#9c27b0')
四种确定进度的样式,各有各的视觉风格和适用场景:
- Linear:最常见的条形进度条,适合文件下载/上传、表单填写这类线性场景。宽度随容器走,高度默认不大,不会太抢视觉空间。
- Ring:环形进度,中间空心,适合放在卡片角落或仪表区域。电量显示、评分环形图、加载中的百分比提示,用Ring最合适。
- ScaleRing:带刻度的环形,比Ring多了外圈刻度线。运动健康类App的步数环、仪表盘就是这种风格。
- Capsule:胶囊形状,两端圆角,比较紧凑。空间有限又想显示进度的时候用它,比如列表项里的微型进度指示。
还有一个ProgressType.Eclipse,但这个已经废弃了,官方推荐用Ring替代。如果你在老代码里看到Eclipse,直接改成Ring就行,视觉上最接近。
LoadingProgress:不确定进度的专属组件
跟上面四种不同,LoadingProgress是独立组件,不是Progress的type。它只有一个效果:一个不停旋转的圆弧动画。
LoadingProgress()
.width(60).height(60)
.color('#1a73e8')
关键区别:LoadingProgress没有value和total属性,你控制不了它的进度。 它就是转,一直转,直到你把它隐藏。适用于那些"不知道要等多久"的场景——网络请求、初始化加载、后台计算。
很多新手会犯一个错:用Progress({ value: 0, type: ProgressType.Ring })来模拟不确定加载,然后自己用定时器慢慢加value。这做法不优雅但能跑,问题是你给了一个"假进度",用户以为在推进,其实你根本不知道真实进度。更好的方式是:知道进度的用Progress,不知道的用LoadingProgress,别混着来。
控制LoadingProgress的显示隐藏,最简单的方式是配合条件渲染:
if (this.isLoading) {
LoadingProgress()
.width(40).height(40)
.color('#1a73e8')
}

模拟下载:完整的加载状态管理
理论知识看完了,来个实际场景——模拟文件下载。这个例子涵盖了启动、暂停、重置三个状态,是Progress最典型的应用:
@State downloadValue: number = 0
@State isDownloading: boolean = false
private downloadTimer: number = -1
private startDownload(): void {
if (this.isDownloading) {
return
}
this.isDownloading = true
this.addLog('下载开始')
this.downloadTimer = setInterval(() => {
this.downloadValue = this.downloadValue + 1
if (this.downloadValue >= 100) {
this.downloadValue = 100
this.isDownloading = false
clearInterval(this.downloadTimer)
this.downloadTimer = -1
this.addLog('下载完成')
}
}, 50)
}
private pauseDownload(): void {
if (!this.isDownloading) {
return
}
this.isDownloading = false
if (this.downloadTimer >= 0) {
clearInterval(this.downloadTimer)
this.downloadTimer = -1
}
this.addLog('下载暂停: ' + this.downloadValue.toString() + '%')
}
private resetDownload(): void {
this.isDownloading = false
if (this.downloadTimer >= 0) {
clearInterval(this.downloadTimer)
this.downloadTimer = -1
}
this.downloadValue = 0
this.addLog('下载重置')
}
UI部分:
Progress({ value: this.downloadValue, total: 100, type: ProgressType.Linear })
.width('90%')
.color(this.downloadValue >= 100 ? '#34a853' : '#1a73e8')
Text(this.downloadValue.toString() + '%')
.fontSize(14).fontColor('#333333').margin({ top: 4 })
Row({ space: 8 }) {
Button('开始')
.enabled(!this.isDownloading && this.downloadValue < 100)
.onClick(() => { this.startDownload() })
Button('暂停')
.enabled(this.isDownloading)
.onClick(() => { this.pauseDownload() })
Button('重置')
.onClick(() => { this.resetDownload() })
}
注意几个设计细节:开始按钮在下载中或下载完成时禁用(!this.isDownloading && this.downloadValue < 100),暂停按钮只在下载中可用,重置按钮始终可用。进度条颜色在完成时从蓝色变绿色,给用户明确的完成反馈。
setInterval必须在aboutToDisappear中清除
这是我开头提到的那个bug——用户离开页面后,定时器还在后台跑。
aboutToDisappear(): void {
if (this.downloadTimer >= 0) {
clearInterval(this.downloadTimer)
}
}
这个不是可选的,是必须的。ArkUI的组件销毁不会自动清除setInterval,如果用户在下载进行中按返回键,定时器会继续执行,回调里访问的State已经失效,轻则UI错乱,重则crash。
初始化timer为-1是个好习惯,这样你可以在clearInterval后把它重置为-1,通过>= 0来判断是否正在运行,而不是用一个额外的boolean。当然你用boolean也行,关键是别忘了清。
有些同学可能会问:用setTimeout递归调用不就行了,干嘛用setInterval?可以,但在Progress场景下setInterval更直观——每50ms加1%,就是固定间隔的定时任务。setTimeout递归需要自己管理下一次调用,逻辑更复杂,而且同样需要在aboutToDisappear里取消。
value超过total会怎样
答案是不会报错,但显示会很奇怪。如果你设了total: 100然后把value设成150,Linear进度条会溢出容器,Ring会画出一圈多。
这看起来是"你自己的bug",但在实际开发中比你想的容易踩。比如你的网络请求返回的进度百分比有时候会超过100(某些后端的坑),或者多个数据源汇总的时候计算错误。Progress组件不做校验,它信任你给的值。
防御性写法:在赋值value的时候做一次clamp:
this.progressValue = Math.min(rawValue, this.totalValue)
一行代码避免一个诡异的UI问题,值得。
样式选择决策
| 样式 | 形态 | 确定进度 | 适用场景 |
|---|---|---|---|
| Linear | 条形 | 支持 | 下载/上传进度 |
| Ring | 环形 | 支持 | 加载/电量/评分 |
| ScaleRing | 刻度环 | 支持 | 仪表盘/运动环 |
| Capsule | 胶囊 | 支持 | 小空间进度 |
| LoadingProgress | 圆弧 | 不支持 | 不确定时长加载 |
选型原则就一句话:能确定进度用Progress,不能确定用LoadingProgress。 在Progress内部,Linear给线性场景(宽度够的地方),Ring/ScaleRing给环形场景(仪表盘或卡片),Capsule给空间紧张的地方。不要用Ring去做本该用Linear的下载进度条,也不要用Linear去做仪表盘——选错样式不只是丑的问题,用户对进度形态有预期,违反预期就是体验问题。
坑点汇总
坑1:Progress的value必须 <= total。 超出不会报错,但显示异常。赋值时做Math.min防护。
坑2:ProgressType.Eclipse已废弃。 用Ring替代,API写法一样,视觉效果接近。
坑3:LoadingProgress无法控制进度值。 它只能显示/隐藏,别想通过value控制它。不确定时长的加载才用它。
坑4:setInterval必须在aboutToDisappear中clearInterval。 这是最容易忽略的坑,组件销毁不等于定时器停止。不清理的后果是后台疯狂执行回调,访问已销毁的State,crash概率极高。
坑5:Progress颜色不能做渐变。 .color()只接受单色,想要渐变效果得自己用Canvas画。如果你对进度条样式有高要求,Progress可能满足不了你。
坑6:Ring和ScaleRing的尺寸。 给太小了刻度线糊成一团,建议最小60x60,理想80x80以上。Capsule也类似,太窄了看不出进度。
写在最后
进度指示器看着简单,但"什么时候显示、什么时候隐藏、用什么样式、定时器怎么管理"这四个问题回答清楚了,你的加载体验才能从"能用"变成"好用"。
更多推荐

所有评论(0)