Progress 进度条组件完全指南:线性/环形进度、下载管理器与实时更新

本文基于 HarmonyOS(ArkTS 声明式开发范式,API 12 / 5.0.0)写作,所有示例均可在 DevEco Studio 模拟器中验证。配套演示工程位于本文同级目录 ohos/,包含完整可运行的 EntryAbility.etsIndex.ets


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、引言

安装应用时那条缓慢前进的蓝条,下载文件时那个逐渐闭合的圆环,运动 App 里每天逼你走完的圆圈——进度条大概是用户最先感知到的"应用有生命"的证据。它回答的不是"现在能干什么",而是"还要等多久",这恰恰是等待体验里最稀缺的信息。

Progress 是 ArkUI 的基础进度组件,用一条线、一个环或一段胶囊,把"任务完成的程度"可视化为 0~100 的比例。它存在的价值分三层:告知——任务进行到什么位置;预估——剩余多少时间;安抚——让用户相信任务仍在推进而非卡死。与之对照的是 022 的 LoadingProgress:确定进度用 Progress,不确定进度(转圈)用 LoadingProgress——"知道要等多久"与"不知道要等多久"是两种完全不同的反馈策略。

Progress 的使用门槛不高,valuetotaltype 三个参数就能画出进度条。真正的工程难点在两端:前端是形态与配色——五种 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.etsmodel/ProgressModel.etscomponents/ 下的组件代码。


三、核心 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 下载管理器的数据流

下载管理器的进度更新是一条单向数据流:

任务状态机

定时器/事件推进 progress

value 变化

enableSmoothEffect 插值补帧

进度条重绘

文本派生值重算: 百分比/已下载量/ETA

文本更新

暂停/继续/重置

任务状态机是这条链的源头——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/etaSecondsprogress 现算,数据只有一份。### 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 后进度还在后台跑 定时器未清理 aboutToDisappearclearInterval
进度"跳变"不连贯 平滑被关闭 开启 enableSmoothEffect,降低更新频率
value 超过 total 累加越界 Math.min(100, ...) 夹取
改了任务对象字段不刷新 @State 引用比较 map 生成新对象数组再赋值
进度条闪烁/卡顿 更新频率过高 100ms 级低频更新 + 平滑动画
圆环文字不居中 Stack 尺寸没约束 Stack 显式 width/height
百分比与条不一致 百分比独立维护 统一由 value/total 推导

调试技巧:进度异常时先在定时器回调里 hilog 打印 progress;环形角度错乱时,检查 totalvalue 单位是否一致(如 value 用步数、total 用目标步数)。


七、总结与扩展

数据单一来源
progress 是唯一真相源,百分比、已下载量、剩余时间全部由它推导,不存副本。
样式即数据
type/strokeWidth/color 走状态绑定,运行时可调,响应式地表达进度形态。
平滑交给框架
enableSmoothEffect 负责插值补帧,业务侧只做低频推进,避免高频重绘。
确定给数字,不确定给动画
可确定总量的任务用 Progress,未知时长的操作用 LoadingProgress,绝不伪造进度。
进度条的本质不是"画一条线",而是"管理一个状态机"——状态机稳定,界面自然顺滑。

扩展方向:① 真实网络下载:用 request 的进度回调驱动 value;② 上传场景的进度回退与错误重试;③ 多级进度(阶段进度 + 总进度双条);④ 深色模式下的轨道/进度配色;⑤ 结合 animateTo 做目标值非匀速推进。下一篇将进入 LoadingProgress 加载指示器,与本章 Progress 形成"确定/不确定"完整闭环。

Logo

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

更多推荐