Progress 鸿蒙 进度条组件完全指南:线性/环形进度、下载管理器与实时更新
Progress 进度条组件完全指南:线性/环形进度、下载管理器与实时更新
本文基于 HarmonyOS(ArkTS 声明式开发范式,API 12 / 5.0.0)写作,所有示例均可在 DevEco Studio 模拟器中验证。配套演示工程位于本文同级目录
ohos/,包含完整可运行的EntryAbility.ets与Index.ets。



一、引言
安装应用时那条缓慢前进的蓝条,下载文件时那个逐渐闭合的圆环,运动 App 里每天逼你走完的圆圈——进度条大概是用户最先感知到的"应用有生命"的证据。它回答的不是"现在能干什么",而是"还要等多久",这恰恰是等待体验里最稀缺的信息。
Progress 是 ArkUI 的基础进度组件,用一条线、一个环或一段胶囊,把"任务完成的程度"可视化为 0~100 的比例。它存在的价值分三层:告知——任务进行到什么位置;预估——剩余多少时间;安抚——让用户相信任务仍在推进而非卡死。与之对照的是 022 的 LoadingProgress:确定进度用 Progress,不确定进度(转圈)用 LoadingProgress——"知道要等多久"与"不知道要等多久"是两种完全不同的反馈策略。
Progress 的使用门槛不高,value、total、type 三个参数就能画出进度条。真正的工程难点在两端:前端是形态与配色——五种 ProgressType 的适用场景;后端是数据与性能——进度从哪来、多久更新一次、如何避免高频重绘。这两端对应本文 3.2 与第六章。
本文的路线:先讲透 Progress 的构造、样式参数与五种类型,用公式说明百分比与剩余时间,再给出完整演示工程,覆盖线性进度(0~100% 下载动画)、环形进度(运动目标)、并行任务(下载管理器)三个场景,最后谈验证、常见问题与进度反馈设计模式。
二、环境准备
Progress 属于 ArkUI 基础组件,API 8 起提供,ProgressType 的五个成员(Linear/Ring/Eclipse/ScaleRing/Capsule)与 ProgressStyleOptions 在 API 12 上全部稳定。
| 项目 | 推荐配置 | 说明 |
|---|---|---|
| DevEco Studio | 5.0 及以上 | 需支持 API 12 的 SDK |
| HarmonyOS SDK | 5.0.0(12) | compatibleSdkVersion 与之对应 |
| 设备 | Phone 模拟器或真机 | 本文以模拟器验证为主 |
工程落地路径与之前几篇一致,两种方式任选:
- 方式一:DevEco Studio 新建 Empty Ability 工程,直接写 ArkTS 原生页面(本文演示工程即采用此方式)。
- 方式二:在 Flutter·鸿蒙壳工程里把
ohos/entry/src/main/ets/下的页面与组件放入原生工程(EntryAbility通常继承FlutterAbility),组件代码不受影响。
若用方式二,只需关注
pages/Index.ets、model/ProgressModel.ets与components/下的组件代码。
三、核心 API 与原理解析
3.1 构造参数:value、total 与百分比
Progress 的构造参数只有三个:
Progress({ value: 45, total: 100, type: ProgressType.Linear })
| 参数 | 类型 | 说明 |
|---|---|---|
value |
number | 当前进度值,必须 ≤ total |
total |
number | 总量,默认 100 |
type |
ProgressType | Linear(默认)/ Ring / Eclipse / ScaleRing / Capsule |
展示百分比即由二者推导:
[
p = \frac{value}{total} \times 100%
]
所以"0~100%"只是 total = 100 的特例:文件总大小 256 MB、已下载 115 MB,可以直接写 Progress({ value: 115, total: 256 }),组件自动按比例绘制,业务层永远拿真实数量级说话。下载场景的剩余时间估算:
[
ETA = \frac{(total - value) \times size}{total \times speed}
]
LinearDemo 的"剩余时间约 x 秒"就由这个公式实时推导。值得固化的习惯:value 永远由业务数据推导,不要另存一个"百分比"状态——数据源头只有一个,UI 只是投影。
3.2 样式体系:type、strokeWidth 与 color
类型与样式是 Progress 表现力的全部:
| ProgressType | 形态 | 典型场景 |
|---|---|---|
Linear |
水平条 | 文件下载、上传、页面加载进度 |
Capsule |
圆角胶囊 | 任务列表行内进度、按钮化进度 |
Ring |
完整圆环 | 运动目标、健康指标、磁盘占用 |
ScaleRing |
刻度环 | 以 10% 为粒度离散展示的目标 |
Eclipse |
椭圆 | 极简装饰场景 |
样式通过链式 .style() 与 .color() 设置:
Progress({ value: 45, total: 100, type: ProgressType.Ring })
.style({
strokeWidth: 14, // 进度条粗细(vp 或百分比)
color: '#0A59F7', // 进度颜色
backgroundColor: '#EEF3FB', // 轨道背景色
enableSmoothEffect: true // 值变化时平滑过渡
})
ProgressStyleOptions 的关键字段:
| 字段 | 类型 | 说明 |
|---|---|---|
strokeWidth |
number | string | 粗细,vp 或百分比 |
color |
ResourceColor | 进度颜色 |
backgroundColor |
ResourceColor | 轨道底色 |
enableSmoothEffect |
boolean | 平滑过渡,默认 true |
两个实践结论:其一,轨道底色要显式设置——浅色轨道与进度色形成对比;其二,enableSmoothEffect 默认开启——value 变化时组件自动插值,业务只需低频更新真实数据(性能细节见第六章)。
3.3 下载管理器的数据流
下载管理器的进度更新是一条单向数据流:
任务状态机是这条链的源头——idle → downloading → paused/done → idle,暂停/继续/重置驱动状态迁移。
注意图中的两个关键点:progress 是唯一状态——百分比、已下载量、剩余时间全部由它推导;平滑由框架承担——业务侧只做低频推进,视觉补帧交给 enableSmoothEffect。
3.4 与 LoadingProgress 的职责分工
| 场景 | 组件 | 语义 |
|---|---|---|
| 下载 256MB 文件,知道总量 | Progress |
“还剩 78%” |
| 正在登录,不知道要多久 | LoadingProgress |
“正在处理,请稍候” |
| 环形目标达成度 | Progress(Ring) |
“今天走了 65%” |
一句话:确定的进度给数字,不确定的进度给动画。进度条最大的敌人不是慢,而是"看起来卡死"——没有可靠进度来源时,宁可转圈,也不要伪造进度。
四、完整代码实现
下面给出演示工程的完整可运行代码。工程以 Tabs 组织三个模块:线性进度(下载动画)、环形进度(运动目标)、并行任务(下载管理器)。数据模型 ProgressModel.ets 提供下载任务。
4.1 入口:EntryAbility.ets
import { UIAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
private readonly TAG: string = 'ProgressGuideAbility';
onCreate(want: object, launchParam: object): void {
hilog.info(0x0000, this.TAG, '%{public}s', 'Ability onCreate');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(0x0000, this.TAG, 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(0x0000, this.TAG, '%{public}s', 'Succeeded in loading the content.');
});
}
// onForeground / onBackground / onDestroy / onWindowStageDestroy 生命周期
// 回调仅打 hilog 日志,完整版见演示工程源码
}
4.2 数据模型:ProgressModel.ets
export class DownloadTask {
id: number;
name: string;
size: number;
progress: number;
status: string;
constructor(id: number, name: string, size: number, progress: number = 0, status: string = 'idle') {
this.id = id;
this.name = name;
this.size = size;
this.progress = progress;
this.status = status;
}
}
// TASKS:三个并行下载任务(PDF 128MB / 手册 96MB / 视频 512MB)
4.3 主页面:Index.ets
import { LinearDemo } from '../components/LinearDemo';
import { RingDemo } from '../components/RingDemo';
import { TaskListDemo } from '../components/TaskListDemo';
@Entry
@Component
struct Index {
@State currentIndex: number = 0;
build() {
Column() {
Tabs({ barPosition: BarPosition.Start, index: this.currentIndex }) {
TabContent() { LinearDemo() }.tabBar('线性进度')
TabContent() { RingDemo() }.tabBar('环形进度')
TabContent() { TaskListDemo() }.tabBar('并行任务')
}
.vertical(false)
.scrollable(true)
.barMode(BarMode.Scrollable)
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
}
}
4.4 核心场景一:线性进度(LinearDemo.ets)
单个大文件下载演示线性进度的完整闭环:定时器推进 progress、派生文本(已下载量/ETA)实时刷新、暂停/继续/重置控制状态机、Capsule 与默认 Linear 样式并排对比:
import { promptAction } from '@kit.ArkUI';
@Component
export struct LinearDemo {
@State progress: number = 0;
@State running: boolean = false;
@State speed: number = 8;
private timerId: number = -1;
private readonly totalSize: number = 256;
aboutToDisappear(): void {
this.stopTimer();
}
build() {
Column({ space: 20 }) {
Text('鸿蒙开发框架指南.pdf')
.fontSize(16).fontWeight(FontWeight.Bold)
.width('92%').textAlign(TextAlign.Start)
Row({ space: 12 }) {
Text('📄').fontSize(26)
Column({ space: 4 }) {
Text(`已下载 ${this.downloadedMB()} MB / ${this.totalSize} MB`)
.fontSize(13).fontColor('#666666')
Text(`剩余时间约 ${this.etaSeconds()} 秒`)
.fontSize(12).fontColor('#999999')
}
.alignItems(HorizontalAlign.Start)
Blank()
Text(`${this.progress}%`)
.fontSize(22).fontWeight(FontWeight.Bold).fontColor('#0A59F7')
}
.width('92%').padding(16).borderRadius(14)
.backgroundColor(Color.White).border({ width: 1, color: '#F0F0F0' })
Progress({ value: this.progress, total: 100, type: ProgressType.Linear })
.style({
strokeWidth: 10,
color: '#0A59F7',
enableSmoothEffect: true,
backgroundColor: '#EEF3FB'
})
.width('92%')
Row({ space: 10 }) {
Button(this.running ? '暂停' : '继续')
.layoutWeight(1).height(42).fontSize(15)
.backgroundColor('#0A59F7')
.enabled(this.progress < 100)
.onClick(() => { this.toggle(); })
Button('重置')
.layoutWeight(1).height(42).fontSize(15)
.backgroundColor('#F0F0F0').fontColor('#333333')
.onClick(() => {
this.stopTimer();
this.progress = 0;
this.running = false;
})
}
.width('92%')
Text('说明:定时器每 100ms 推进 8 个单位,平滑过渡由 enableSmoothEffect 插值完成;'
+ '进度、已下载量与剩余时间均由同一个 progress 推导。'
+ '(工程内另附默认 Linear 与 Capsule 样式对比)')
.fontSize(13).fontColor('#999999').width('92%').textAlign(TextAlign.Start)
}
.width('100%')
.padding({ top: 16, bottom: 24 })
}
toggle(): void {
if (this.running) {
this.stopTimer();
this.running = false;
return;
}
this.running = true;
this.timerId = setInterval(() => {
if (this.progress >= 100) {
this.stopTimer();
this.running = false;
return;
}
this.progress = Math.min(100, this.progress + this.speed);
if (this.progress >= 100) {
this.stopTimer();
this.running = false;
promptAction.showToast({ message: '下载完成', duration: 1200 });
}
}, 100);
}
stopTimer(): void {
if (this.timerId >= 0) {
clearInterval(this.timerId);
this.timerId = -1;
}
}
downloadedMB(): number {
return Math.floor(this.totalSize * this.progress / 100);
}
etaSeconds(): number {
return Math.ceil((100 - this.progress) / this.speed);
}
}
值得注意的三点:定时器生命周期——aboutToDisappear 里必须清理;到达 100 的收尾——置 running = false、停表、提示完成;派生值只读——downloadedMB/etaSeconds 由 progress 现算,数据只有一份。### 4.5 场景二:环形进度(RingDemo.ets)
运动目标演示环形进度的三种形态(Ring/ScaleRing/Eclipse)与样式实时调节。环形进度的"中央文字"用 Stack 叠放实现——Progress 只画环,百分比与步数文本由业务层叠加:
import { promptAction } from '@kit.ArkUI';
@Component
export struct RingDemo {
@State steps: number = 0;
@State running: boolean = false;
@State ringWidth: number = 14;
@State ringColor: string = '#0A59F7';
private readonly goalSteps: number = 10000;
private timerId: number = -1;
aboutToDisappear(): void {
if (this.timerId >= 0) {
clearInterval(this.timerId);
this.timerId = -1;
}
}
build() {
Scroll() {
Column({ space: 20 }) {
Text('今日运动目标').fontSize(16).fontWeight(FontWeight.Bold)
.width('92%').textAlign(TextAlign.Start)
Column({ space: 12 }) {
Stack() {
Progress({
value: this.steps,
total: this.goalSteps,
type: ProgressType.Ring
})
.style({
strokeWidth: this.ringWidth,
color: this.ringColor,
backgroundColor: '#EEF3FB'
})
.width(150).height(150)
Column({ space: 2 }) {
Text(`${this.stepPercent()}%`)
.fontSize(26).fontWeight(FontWeight.Bold).fontColor(this.ringColor)
Text(`${this.steps} / ${this.goalSteps} 步`)
.fontSize(12).fontColor('#999999')
}
}
.width(150).height(150)
Row({ space: 10 }) {
Button(this.running ? '暂停' : '开始')
.height(40).fontSize(14).backgroundColor('#0A59F7')
.enabled(this.steps < this.goalSteps)
.onClick(() => { this.toggle(); })
Button('归零')
.height(40).fontSize(14)
.backgroundColor('#F0F0F0').fontColor('#333333')
.onClick(() => {
if (this.timerId >= 0) {
clearInterval(this.timerId);
this.timerId = -1;
}
this.running = false;
this.steps = 0;
})
}
}
.width('92%').padding(18).borderRadius(14)
.backgroundColor(Color.White).border({ width: 1, color: '#F0F0F0' })
// Slider 调节 strokeWidth(4~24)、三色按钮切换 ringColor、
// 底部 Ring/ScaleRing/Eclipse 三形态对比,完整代码见演示工程
// (样式参数全部走 @State 绑定,value 直接用真实步数 0~10000)
}
.width('100%')
.padding({ top: 16, bottom: 24 })
}
.width('100%')
.height('100%')
}
toggle(): void {
if (this.running) {
if (this.timerId >= 0) {
clearInterval(this.timerId);
this.timerId = -1;
}
this.running = false;
return;
}
this.running = true;
this.timerId = setInterval(() => {
if (this.steps >= this.goalSteps) {
if (this.timerId >= 0) {
clearInterval(this.timerId);
this.timerId = -1;
}
this.running = false;
promptAction.showToast({ message: '目标达成,太棒了!', duration: 1200 });
return;
}
this.steps = Math.min(this.goalSteps, this.steps + 200);
}, 100);
}
stepPercent(): number {
return Math.floor(this.steps * 100 / this.goalSteps);
}
}
环形进度的两个细节:value 直接用真实步数(0~10000),组件内部换算成环的角度,业务侧不必先算百分比;Slider 调节 strokeWidth、按钮切换 ringColor——样式参数全部走状态,直观演示"样式即数据"。
4.6 场景三:并行任务(TaskListDemo.ets)
下载管理器把三个任务放进一个 100ms 定时器里并行推进,每行是 Capsule 胶囊进度加状态徽标,底部聚合进度条由各任务进度求平均:
import { promptAction } from '@kit.ArkUI';
import { DownloadTask, TASKS } from '../model/ProgressModel';
@Component
export struct TaskListDemo {
@State tasks: DownloadTask[] = [];
@State running: boolean = false;
private timerId: number = -1;
aboutToAppear(): void {
// map 复制任务数据,避免直接引用全局 TASKS
this.tasks = TASKS.map((item: DownloadTask) => {
return new DownloadTask(item.id, item.name, item.size, 0, 'idle');
});
}
// aboutToDisappear 中清理定时器(与 LinearDemo 一致,见演示工程源码)
build() {
Scroll() {
Column({ space: 16 }) {
Text('下载管理器').fontSize(16).fontWeight(FontWeight.Bold)
.width('92%').textAlign(TextAlign.Start)
ForEach(this.tasks, (task: DownloadTask) => {
Column({ space: 8 }) {
Row({ space: 10 }) {
Text(task.name).fontSize(14)
.fontColor(task.status === 'done' ? '#999999' : '#333333')
Blank()
Text(this.statusText(task.status))
.fontSize(12).fontColor(this.statusColor(task.status))
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8).backgroundColor(this.statusBg(task.status))
}
.width('100%')
Row({ space: 10 }) {
Progress({ value: task.progress, total: 100, type: ProgressType.Capsule })
.style({
strokeWidth: 12,
color: task.status === 'done' ? '#07C160' : '#0A59F7',
backgroundColor: '#EEF3FB'
})
.layoutWeight(1)
Text(`${task.progress}%`)
.fontSize(13).fontWeight(FontWeight.Bold)
.fontColor(task.status === 'done' ? '#07C160' : '#0A59F7')
}
.width('100%')
}
.width('100%').padding(14).borderRadius(12)
.backgroundColor(Color.White).border({ width: 1, color: '#F0F0F0' })
}, (task: DownloadTask) => task.id.toString())
}
.width('92%')
Column({ space: 8 }) {
Row({ space: 10 }) {
Text('整体进度').fontSize(14).fontWeight(FontWeight.Medium)
Blank()
Text(`已下载 ${this.overallPercent()}%`)
.fontSize(13).fontColor('#0A59F7')
}
.width('100%')
Progress({ value: this.overallPercent(), total: 100, type: ProgressType.Linear })
.style({ strokeWidth: 12, color: '#0A59F7', backgroundColor: '#EEF3FB' })
.width('100%')
}
.width('92%').padding(14).borderRadius(12).backgroundColor('#F7F9FF')
Row({ space: 10 }) {
Button(this.running ? '暂停全部' : '开始全部')
.layoutWeight(1).height(42).fontSize(15)
.backgroundColor('#0A59F7')
.enabled(this.overallPercent() < 100)
.onClick(() => { this.toggleAll(); })
Button('重置')
.layoutWeight(1).height(42).fontSize(15)
.backgroundColor('#F0F0F0').fontColor('#333333')
.onClick(() => {
if (this.timerId >= 0) {
clearInterval(this.timerId);
this.timerId = -1;
}
this.running = false;
this.tasks = this.tasks.map((item: DownloadTask) => {
return new DownloadTask(item.id, item.name, item.size, 0, 'idle');
});
})
}
.width('92%')
Text('说明:三个任务共用一个 100ms 定时器依次推进,整体进度 = 各任务进度的算术平均;'
+ '任务状态用 DownloadTask 对象承载,map 换新数组保证 @State 感知更新。')
.fontSize(13).fontColor('#999999').width('92%').textAlign(TextAlign.Start)
}
.width('100%')
.padding({ top: 16, bottom: 24 })
}
.width('100%')
.height('100%')
}
toggleAll(): void {
if (this.running) {
if (this.timerId >= 0) {
clearInterval(this.timerId);
this.timerId = -1;
}
this.running = false;
return;
}
this.running = true;
this.timerId = setInterval(() => {
let allDone: boolean = true;
this.tasks = this.tasks.map((item: DownloadTask) => {
if (item.status === 'done') {
return item;
}
const next: number = Math.min(100, item.progress + this.stepOf(item.id));
if (next >= 100) {
return new DownloadTask(item.id, item.name, item.size, 100, 'done');
}
allDone = false;
return new DownloadTask(item.id, item.name, item.size, next, 'downloading');
});
if (allDone) {
if (this.timerId >= 0) {
clearInterval(this.timerId);
this.timerId = -1;
}
this.running = false;
promptAction.showToast({ message: '全部任务完成', duration: 1500 });
}
}, 100);
}
stepOf(id: number): number {
// 各任务不同速率:任务1 每次 +2、任务2 每次 +3、任务3 每次 +1
return id === 1 ? 2 : (id === 2 ? 3 : 1);
}
overallPercent(): number {
// 整体进度 = 各任务进度算术平均
if (this.tasks.length === 0) {
return 0;
}
let sum: number = 0;
for (let i = 0; i < this.tasks.length; i++) {
sum += this.tasks[i].progress;
}
return Math.floor(sum / this.tasks.length);
}
overallSize(): number {
return this.tasks.reduce((sum: number, item: DownloadTask) => sum + item.size, 0);
}
// overallMB:按整体进度折算的已下载量,见演示工程源码
statusText(status: string): string {
return status === 'done' ? '已完成' : (status === 'downloading' ? '下载中' : '等待中');
}
// statusColor / statusBg:已完成绿 / 下载中蓝 / 等待中灰 的配色,见演示工程源码
}
并行任务的工程要点:单定时器轮询 + map 批量换新——所有任务的状态更新在同一 tick 完成,一次赋值触发一次列表重绘;聚合进度是纯函数推导——overallPercent 由任务数组现算,不做增量维护;状态机完备——完成的任务在后续 tick 原样返回,不再参与推进。
五、模拟器运行与效果展示
启动模拟器,点击 Run 部署演示工程,依次切换线性/环形/并行任务三个 Tab 验证进度动画与状态联动:
截图占位(模拟器实机拍摄后替换):
| 占位图 | 场景 |
|---|---|
screenshot_01_zero.png |
初始状态:线性进度 0%,任务全部等待中 |
screenshot_02_mid.png |
中间状态:线性进度约 45%,文本联动已下载量/ETA |
screenshot_03_done.png |
完成状态:进度 100%,任务全部完成 |
screenshot_04_ring.png |
环形进度:Ring/ScaleRing/Eclipse 三形态与中央百分比 |
六、调试与常见问题
| 现象 | 原因 | 解决 |
|---|---|---|
| 进度条不增长 | 定时器没启动,或 value 没绑到状态 |
确认 value: this.progress 受控绑定 |
| 切 Tab 后进度还在后台跑 | 定时器未清理 | aboutToDisappear 里 clearInterval |
| 进度"跳变"不连贯 | 平滑被关闭 | 开启 enableSmoothEffect,降低更新频率 |
| value 超过 total | 累加越界 | 用 Math.min(100, ...) 夹取 |
| 改了任务对象字段不刷新 | @State 引用比较 |
map 生成新对象数组再赋值 |
| 进度条闪烁/卡顿 | 更新频率过高 | 100ms 级低频更新 + 平滑动画 |
| 圆环文字不居中 | Stack 尺寸没约束 | Stack 显式 width/height |
| 百分比与条不一致 | 百分比独立维护 | 统一由 value/total 推导 |
调试技巧:进度异常时先在定时器回调里 hilog 打印 progress;环形角度错乱时,检查 total 与 value 单位是否一致(如 value 用步数、total 用目标步数)。
七、总结与扩展
-
数据单一来源
- progress 是唯一真相源,百分比、已下载量、剩余时间全部由它推导,不存副本。 样式即数据
- type/strokeWidth/color 走状态绑定,运行时可调,响应式地表达进度形态。 平滑交给框架
- enableSmoothEffect 负责插值补帧,业务侧只做低频推进,避免高频重绘。 确定给数字,不确定给动画
- 可确定总量的任务用 Progress,未知时长的操作用 LoadingProgress,绝不伪造进度。
扩展方向:① 真实网络下载:用 request 的进度回调驱动 value;② 上传场景的进度回退与错误重试;③ 多级进度(阶段进度 + 总进度双条);④ 深色模式下的轨道/进度配色;⑤ 结合 animateTo 做目标值非匀速推进。下一篇将进入 LoadingProgress 加载指示器,与本章 Progress 形成"确定/不确定"完整闭环。
更多推荐


所有评论(0)