对应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')![请添加图片描述](https://i-blog.csdnimg.cn/direct/be603d34c37d44aba70f91483d2cb279.png)


// 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也类似,太窄了看不出进度。


写在最后

进度指示器看着简单,但"什么时候显示、什么时候隐藏、用什么样式、定时器怎么管理"这四个问题回答清楚了,你的加载体验才能从"能用"变成"好用"。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐