Flutter 通用进度条组件:ProgressWidget 一键实现多类型进度展示

在 Flutter 开发中,进度展示(加载、上传、流程引导等)是高频场景,但原生进度条存在类型单一、样式固化、适配性差的问题。本文参考 ActionSheetWidget 的成熟封装思路,优化升级 ProgressWidget------ 整合线性、环形、步骤三大核心类型,强化样式自定义、状态适配与交互体验,一行代码即可集成,完美覆盖加载中、任务进度、流程引导等 90%+ 业务场景。

一、核心优势(精准解决开发痛点)

  1. 多类型开箱即用:枚举封装线性、环形、步骤三种进度条,默认样式贴合 Material 设计规范,无需复杂配置即可直接落地
  2. 全维度样式自定义:进度色、背景色、尺寸、圆角、文本样式均可细粒度配置,支持自定义进度文本(百分比 / 数值 / 状态提示),适配不同品牌主题
  3. 原生级交互体验:内置进度动画(支持自定义时长与曲线),步骤进度条自动匹配当前步骤状态,文本与进度实时联动,符合用户操作认知
  4. 高适配强鲁棒 :支持深色模式自动适配、宽高自适应(线性进度条支持 double.infinity),参数添加断言校验,避免非法值导致 UI 异常
  5. 低侵入高复用:组件无冗余依赖,静态方法简化调用,统一项目进度展示样式,降低维护成本

二、核心配置速览(关键参数一目了然)

配置分类 核心参数 核心作用
必选配置 progress: double(0-1)、type: ProgressType(枚举) 进度值(强制 0-1 范围)、进度条类型
基础样式配置 width/heightradiusprogressColor/bgColor 尺寸、圆角、进度 / 背景色
文本配置 showTexttextBuildertextStyle 显示进度文本、自定义文本格式、文本样式
动画配置 animationDurationanimationCurve 进度动画时长、动画曲线
步骤专属配置 stepCountstepTitlesstepSizecompletedIcon 步骤总数、步骤标题、步骤点大小、完成图标

三、生产级完整代码(可直接复制,开箱即用)

dart

复制代码
import 'package:flutter/material.dart';

/// 进度条核心类型枚举(覆盖三大高频场景)
enum ProgressType {
  linear,   // 线性进度条(上传/下载/加载进度)
  circular, // 环形进度条(加载中/倒计时/完成度展示)
  step      // 步骤进度条(流程引导:注册/下单/审核)
}

/// 通用进度条组件:兼顾易用性、灵活性与适配性
class ProgressWidget extends StatelessWidget {
  // 必选参数:核心配置
  final double progress; // 进度值(0-1,超出会触发断言)
  final ProgressType type; // 进度条类型

  // 基础样式配置(通用)
  final double width; // 宽度(线性/环形:整体宽度;步骤:容器宽度)
  final double height; // 高度(线性:进度条厚度;环形:圆环厚度;步骤:无效)
  final double radius; // 圆角(仅线性进度条有效)
  final Color progressColor; // 进度颜色(默认蓝色)
  final Color bgColor; // 背景颜色(默认浅灰)

  // 文本配置
  final bool showText; // 是否显示进度文本(默认true)
  final String Function(double progress)? textBuilder; // 自定义进度文本(优先级最高)
  final TextStyle? textStyle; // 进度文本样式(默认14/16号字)

  // 动画配置
  final Duration animationDuration; // 进度动画时长(默认500ms)
  final Curve animationCurve; // 进度动画曲线(默认线性匀速)

  // 步骤进度条专属配置
  final int stepCount; // 步骤总数(仅步骤类型,至少2步)
  final List<String>? stepTitles; // 步骤标题(长度需与stepCount一致)
  final double stepSize; // 步骤点大小(默认24)
  final Widget? completedIcon; // 已完成步骤图标(默认对勾)
  final TextStyle? stepTitleStyle; // 步骤标题样式(默认12号字)

  const ProgressWidget({
    super.key,
    required this.progress,
    required this.type,
    // 基础样式配置
    this.width = 200,
    this.height = 6.0,
    this.radius = 3.0,
    this.progressColor = Colors.blue,
    this.bgColor = const Color(0xFFE0E0E0),
    // 文本配置
    this.showText = true,
    this.textBuilder,
    this.textStyle,
    // 动画配置
    this.animationDuration = const Duration(milliseconds: 500),
    this.animationCurve = Curves.linear,
    // 步骤专属配置
    this.stepCount = 3,
    this.stepTitles,
    this.stepSize = 24.0,
    this.completedIcon = const Icon(Icons.check, size: 14, color: Colors.white),
    this.stepTitleStyle = const TextStyle(fontSize: 12, color: Color(0xFF6B7280)),
  })  : assert(progress >= 0 && progress <= 1, "进度值必须在 0-1 之间"),
        assert(type != ProgressType.step || stepCount >= 2, "步骤进度条至少需要2步"),
        assert(type != ProgressType.step || (stepTitles == null || stepTitles.length == stepCount), 
               "步骤标题长度必须与步骤数一致");

  /// 深色模式颜色适配(核心辅助方法)
  Color _adaptDarkMode(Color lightColor, Color darkColor) {
    return MediaQuery.platformBrightnessOf(context) == Brightness.dark
        ? darkColor
        : lightColor;
  }

  /// 获取默认进度文本(百分比/步骤提示)
  String _getDefaultText() {
    if (type == ProgressType.step) {
      final currentStep = (progress * (stepCount - 1)).round() + 1;
      return "第 $currentStep/$stepCount 步";
    }
    return "${(progress * 100).toInt()}%";
  }

  /// 构建线性进度条(适配上传/下载/加载进度)
  Widget _buildLinearProgress() {
    // 深色模式适配颜色
    final adaptedProgressColor = _adaptDarkMode(progressColor, Colors.blueAccent);
    final adaptedBgColor = _adaptDarkMode(bgColor, const Color(0xFF374151));
    // 进度文本(自定义优先,无则用默认)
    final progressText = textBuilder?.call(progress) ?? _getDefaultText();
    // 文本样式(自定义优先,无则用默认)
    final adaptedTextStyle = textStyle ?? TextStyle(
      fontSize: 14,
      color: _adaptDarkMode(const Color(0xFF1F2937), const Color(0xFFE0E0E0)),
    );

    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 线性进度条主体
        Container(
          width: width,
          height: height,
          decoration: BoxDecoration(
            color: adaptedBgColor,
            borderRadius: BorderRadius.circular(radius),
          ),
          child: AnimatedContainer(
            width: width * progress,
            decoration: BoxDecoration(
              color: adaptedProgressColor,
              borderRadius: BorderRadius.circular(radius),
            ),
            duration: animationDuration,
            curve: animationCurve,
          ),
        ),
        // 进度文本(按需显示)
        if (showText)
          Padding(
            padding: const EdgeInsets.only(top: 8.0),
            child: Text(progressText, style: adaptedTextStyle),
          ),
      ],
    );
  }

  /// 构建环形进度条(适配加载中/倒计时/完成度)
  Widget _buildCircularProgress() {
    // 深色模式适配颜色
    final adaptedProgressColor = _adaptDarkMode(progressColor, Colors.blueAccent);
    final adaptedBgColor = _adaptDarkMode(bgColor, const Color(0xFF374151));
    // 进度文本(自定义优先,无则用默认)
    final progressText = textBuilder?.call(progress) ?? _getDefaultText();
    // 文本样式(自定义优先,无则用默认)
    final adaptedTextStyle = textStyle ?? TextStyle(
      fontSize: 16,
      fontWeight: FontWeight.w500,
      color: _adaptDarkMode(const Color(0xFF1F2937), const Color(0xFFE0E0E0)),
    );

    return SizedBox(
      width: width,
      height: width, // 环形宽高一致,保证圆形
      child: Stack(
        alignment: Alignment.center,
        children: [
          // 环形进度条主体
          CircularProgressIndicator(
            value: progress,
            strokeWidth: height,
            valueColor: AlwaysStoppedAnimation(adaptedProgressColor),
            backgroundColor: adaptedBgColor,
            strokeCap: StrokeCap.round, // 圆环两端圆角,优化视觉效果
          ),
          // 进度文本(按需显示)
          if (showText) Text(progressText, style: adaptedTextStyle),
        ],
      ),
    );
  }

  /// 构建步骤进度条(适配流程引导)
  Widget _buildStepProgress() {
    // 深色模式适配颜色
    final adaptedProgressColor = _adaptDarkMode(progressColor, Colors.blueAccent);
    final adaptedBgColor = _adaptDarkMode(bgColor, const Color(0xFF374151));
    final adaptedStepTitleStyle = stepTitleStyle?.copyWith(
      color: _adaptDarkMode(stepTitleStyle!.color, const Color(0xFF9E9E9E)),
    ) ?? const TextStyle(fontSize: 12, color: Color(0xFF9E9E9E));

    // 当前步骤(进度值映射为步骤数)
    final currentStep = (progress * (stepCount - 1)).round() + 1;

    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        // 步骤连接线
        Container(
          width: width,
          height: 2,
          decoration: BoxDecoration(
            color: adaptedBgColor,
            borderRadius: BorderRadius.circular(1),
          ),
          child: AnimatedContainer(
            width: width * progress,
            color: adaptedProgressColor,
            duration: animationDuration,
            curve: animationCurve,
          ),
        ),
        const SizedBox(height: 12),
        // 步骤点 + 标题
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: List.generate(stepCount, (index) {
            final stepIndex = index + 1;
            final isCompleted = stepIndex < currentStep; // 已完成步骤
            final isCurrent = stepIndex == currentStep; // 当前步骤
            final isPending = stepIndex > currentStep; // 未完成步骤

            return Column(
              children: [
                // 步骤点
                Container(
                  width: stepSize,
                  height: stepSize,
                  decoration: BoxDecoration(
                    color: isCompleted || isCurrent
                        ? adaptedProgressColor
                        : adaptedBgColor,
                    border: isPending ? Border.all(color: adaptedBgColor) : null,
                    borderRadius: BorderRadius.circular(stepSize / 2),
                  ),
                  alignment: Alignment.center,
                  child: isCompleted
                      ? completedIcon // 已完成显示自定义图标
                      : Text(
                          stepIndex.toString(),
                          style: TextStyle(
                            color: isCurrent ? Colors.white : adaptedStepTitleStyle.color,
                            fontSize: 14,
                            fontWeight: isCurrent ? FontWeight.w500 : FontWeight.normal,
                          ),
                        ),
                ),
                // 步骤标题(按需显示)
                if (stepTitles != null)
                  Padding(
                    padding: const EdgeInsets.only(top: 4.0),
                    child: Text(
                      stepTitles![index],
                      style: adaptedStepTitleStyle,
                    ),
                  ),
              ],
            );
          }),
        ),
      ],
    );
  }

  /// 静态调用方法(简化调用,参考 ActionSheetWidget 设计)
  static Widget linear({
    required double progress,
    double width = double.infinity,
    double height = 6.0,
    double radius = 3.0,
    Color progressColor = Colors.blue,
    Color bgColor = const Color(0xFFE0E0E0),
    bool showText = true,
    String Function(double progress)? textBuilder,
    TextStyle? textStyle,
    Duration animationDuration = const Duration(milliseconds: 500),
  }) {
    return ProgressWidget(
      type: ProgressType.linear,
      progress: progress,
      width: width,
      height: height,
      radius: radius,
      progressColor: progressColor,
      bgColor: bgColor,
      showText: showText,
      textBuilder: textBuilder,
      textStyle: textStyle,
      animationDuration: animationDuration,
    );
  }

  static Widget circular({
    required double progress,
    double size = 80,
    double strokeWidth = 4.0,
    Color progressColor = Colors.blue,
    Color bgColor = const Color(0xFFE0E0E0),
    bool showText = true,
    String Function(double progress)? textBuilder,
    TextStyle? textStyle,
    Duration animationDuration = const Duration(milliseconds: 500),
  }) {
    return ProgressWidget(
      type: ProgressType.circular,
      progress: progress,
      width: size,
      height: strokeWidth,
      progressColor: progressColor,
      bgColor: bgColor,
      showText: showText,
      textBuilder: textBuilder,
      textStyle: textStyle,
      animationDuration: animationDuration,
    );
  }

  static Widget step({
    required double progress,
    required int stepCount,
    List<String>? stepTitles,
    double width = double.infinity,
    Color progressColor = Colors.blue,
    Color bgColor = const Color(0xFFE0E0E0),
    double stepSize = 24.0,
    Widget? completedIcon,
    TextStyle? stepTitleStyle,
    Duration animationDuration = const Duration(milliseconds: 800),
  }) {
    return ProgressWidget(
      type: ProgressType.step,
      progress: progress,
      stepCount: stepCount,
      stepTitles: stepTitles,
      width: width,
      progressColor: progressColor,
      bgColor: bgColor,
      stepSize: stepSize,
      completedIcon: completedIcon,
      stepTitleStyle: stepTitleStyle,
      animationDuration: animationDuration,
      showText: false, // 步骤进度条默认不显示文本,如需显示可通过textBuilder自定义
    );
  }

  @override
  Widget build(BuildContext context) {
    switch (type) {
      case ProgressType.linear:
        return _buildLinearProgress();
      case ProgressType.circular:
        return _buildCircularProgress();
      case ProgressType.step:
        return _buildStepProgress();
    }
  }
}

四、三大高频场景落地示例(直接复制到项目可用)

场景 1:线性进度条(文件上传 / 下载)

适用场景:文件上传、资源下载、数据加载进度展示

dart

复制代码
// 上传页面进度展示
Column(
  children: [
    const Text("文件上传中..."),
    const SizedBox(height: 16),
    ProgressWidget.linear(
      progress: 0.75, // 75% 进度
      width: double.infinity, // 自适应父容器宽度
      height: 8.0,
      radius: 4.0,
      progressColor: Colors.greenAccent,
      bgColor: const Color(0xFFF5F5F5),
      showText: true,
      // 自定义进度文本(显示百分比+状态)
      textBuilder: (progress) => "${(progress * 100).toInt()}% 上传中(剩余3秒)",
      textStyle: const TextStyle(fontSize: 14, color: Colors.green),
      animationDuration: const Duration(milliseconds: 800),
    ),
  ],
);

场景 2:环形进度条(加载中 / 倒计时)

适用场景:页面加载、倒计时、任务完成度展示

dart

复制代码
// 登录页面加载中状态
Center(
  child: ProgressWidget.circular(
    progress: 0.3, // 加载中(无明确进度可设为null,显示无限循环)
    size: 80, // 环形整体大小
    strokeWidth: 4.0,
    progressColor: Colors.blueAccent,
    bgColor: const Color(0xFFE8E8E8),
    showText: true,
    textBuilder: (_) => "加载中", // 自定义文本,不显示百分比
    textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Colors.blueAccent),
    animationDuration: const Duration(milliseconds: 1000),
  ),
);

// 倒计时场景(配合StatefulWidget使用)
// 示例:60秒倒计时
ProgressWidget.circular(
  progress: _countdown / 60, // _countdown为当前剩余秒数(0-60)
  size: 60,
  strokeWidth: 3.0,
  progressColor: Colors.orangeAccent,
  showText: true,
  textBuilder: (progress) => "${(progress * 60).toInt()}s",
  textStyle: const TextStyle(fontSize: 14, color: Colors.orangeAccent),
);

场景 3:步骤进度条(流程引导)

适用场景:注册流程、订单提交、审核流程、表单分步填写

dart

复制代码
// 订单提交流程(4步:填写信息→上传材料→支付→完成)
ProgressWidget.step(
  progress: 0.75, // 完成3/4步骤(当前第3步:支付)
  stepCount: 4,
  stepTitles: ["填写信息", "上传材料", "支付", "完成"],
  width: double.infinity,
  progressColor: Colors.orange,
  bgColor: const Color(0xFFEEEEEE),
  stepSize: 28.0,
  completedIcon: const Icon(Icons.check_circle, size: 16, color: Colors.white),
  stepTitleStyle: const TextStyle(fontSize: 13, color: Color(0xFF333333)),
  animationDuration: const Duration(milliseconds: 800),
);

// 配合业务逻辑:点击下一步更新进度
// 示例:当前步骤index从1开始,总步骤4步
void _nextStep() {
  setState(() {
    currentStep = min(currentStep + 1, 4);
    // 进度值 = (当前步骤-1)/(总步骤-1)
    progress = (currentStep - 1) / (4 - 1);
  });
}

五、核心封装技巧(复用成熟设计思路)

  1. 类型枚举化 :用 ProgressType 统一管理三大类型,扩展新类型只需新增枚举 + 构建方法,降低维护成本
  2. 静态调用简化 :参考 ActionSheetWidget 的静态方法设计,提供 linear()/circular()/step() 快捷调用,无需手动指定 type,简化代码
  3. 参数断言校验:对进度值、步骤数、步骤标题长度添加断言,提前规避非法参数导致的 UI 异常,便于开发调试
  4. 动画内置化 :默认集成 AnimatedContainer 进度动画,支持自定义时长与曲线,无需外部包装动画组件
  5. 适配细节优化 :深色模式自动切换颜色、环形进度条 strokeCap: StrokeCap.round 优化视觉、步骤点状态自动区分,提升用户体验

六、避坑指南(解决 90% 开发痛点)

  1. 进度值边界处理 :进度值强制限制在 0-1 之间,外部赋值时建议用 progress.clamp(0, 1) 避免超出范围,例如:ProgressWidget(progress: downloadProgress.clamp(0, 1))
  2. 环形尺寸一致性 :环形进度条的宽高通过 width 统一控制(height 仅控制圆环厚度),建议 widthsize 保持一致,避免圆形变形
  3. 步骤标题长度控制:步骤标题建议不超过 4 个字符,过长会导致步骤点间距挤压,可通过缩小字体或精简文本解决
  4. 列表中性能优化 :在 ListView 等可滚动组件中批量使用时,建议关闭 showText 或复用组件实例,避免频繁重建文本组件
  5. 动画时长适配:进度变化较快(如倒计时)时,动画时长建议设为 300-500ms;流程步骤切换时,可设为 600-800ms,提升视觉流畅度
  6. 深色模式兼容性 :自定义颜色时,建议通过 _adaptDarkMode 方法适配深色模式,避免浅色文本配浅色背景导致不可见

https://openharmonycrossplatform.csdn.net/content

相关推荐
庄雨山2 小时前
Flutter 与开源鸿蒙 底部弹窗多项选择实现方案全解析
flutter·开源·openharmonyos
笨小孩7872 小时前
Flutter深度解析:从核心原理到实战开发全攻略
flutter
笨小孩7873 小时前
Flutter深度解析:从原理到实战的跨平台开发指南
flutter
飛6793 小时前
Flutter 状态管理深度实战:从零封装轻量级响应式状态管理器,告别 Provider/Bloc 的臃肿与复杂
前端·javascript·flutter
tangweiguo030519873 小时前
Flutter iOS 风格弹框组件封装
flutter
LYFlied3 小时前
浅谈跨端开发:大前端时代的融合之道
前端·flutter·react native·webview·大前端·跨端开发·hybrid
500844 小时前
鸿蒙 Flutter 分布式数据同步:DistributedData 实时协同实战
分布式·flutter·华为·electron·开源·wpf·音视频
song5014 小时前
鸿蒙 Flutter 图像编辑:原生图像处理与滤镜开发
图像处理·人工智能·分布式·flutter·华为·交互
●VON4 小时前
从零构建可扩展 Flutter 应用:v1.0 → v2.0 全代码详解 -《已适配开源鸿蒙》
学习·flutter·开源·openharmony·开源鸿蒙