在 Flutter 开发中,进度展示(加载、上传、流程引导等)是高频场景,但原生进度条存在类型单一、样式固化、适配性差的问题。本文参考 ActionSheetWidget 的成熟封装思路,优化升级 ProgressWidget------ 整合线性、环形、步骤三大核心类型,强化样式自定义、状态适配与交互体验,一行代码即可集成,完美覆盖加载中、任务进度、流程引导等 90%+ 业务场景。
一、核心优势(精准解决开发痛点)
- 多类型开箱即用:枚举封装线性、环形、步骤三种进度条,默认样式贴合 Material 设计规范,无需复杂配置即可直接落地
- 全维度样式自定义:进度色、背景色、尺寸、圆角、文本样式均可细粒度配置,支持自定义进度文本(百分比 / 数值 / 状态提示),适配不同品牌主题
- 原生级交互体验:内置进度动画(支持自定义时长与曲线),步骤进度条自动匹配当前步骤状态,文本与进度实时联动,符合用户操作认知
- 高适配强鲁棒 :支持深色模式自动适配、宽高自适应(线性进度条支持
double.infinity),参数添加断言校验,避免非法值导致 UI 异常 - 低侵入高复用:组件无冗余依赖,静态方法简化调用,统一项目进度展示样式,降低维护成本
二、核心配置速览(关键参数一目了然)
| 配置分类 | 核心参数 | 核心作用 |
|---|---|---|
| 必选配置 | progress: double(0-1)、type: ProgressType(枚举) |
进度值(强制 0-1 范围)、进度条类型 |
| 基础样式配置 | width/height、radius、progressColor/bgColor |
尺寸、圆角、进度 / 背景色 |
| 文本配置 | showText、textBuilder、textStyle |
显示进度文本、自定义文本格式、文本样式 |
| 动画配置 | animationDuration、animationCurve |
进度动画时长、动画曲线 |
| 步骤专属配置 | stepCount、stepTitles、stepSize、completedIcon |
步骤总数、步骤标题、步骤点大小、完成图标 |
三、生产级完整代码(可直接复制,开箱即用)
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);
});
}
五、核心封装技巧(复用成熟设计思路)
- 类型枚举化 :用
ProgressType统一管理三大类型,扩展新类型只需新增枚举 + 构建方法,降低维护成本 - 静态调用简化 :参考
ActionSheetWidget的静态方法设计,提供linear()/circular()/step()快捷调用,无需手动指定type,简化代码 - 参数断言校验:对进度值、步骤数、步骤标题长度添加断言,提前规避非法参数导致的 UI 异常,便于开发调试
- 动画内置化 :默认集成
AnimatedContainer进度动画,支持自定义时长与曲线,无需外部包装动画组件 - 适配细节优化 :深色模式自动切换颜色、环形进度条
strokeCap: StrokeCap.round优化视觉、步骤点状态自动区分,提升用户体验
六、避坑指南(解决 90% 开发痛点)
- 进度值边界处理 :进度值强制限制在 0-1 之间,外部赋值时建议用
progress.clamp(0, 1)避免超出范围,例如:ProgressWidget(progress: downloadProgress.clamp(0, 1)) - 环形尺寸一致性 :环形进度条的宽高通过
width统一控制(height仅控制圆环厚度),建议width与size保持一致,避免圆形变形 - 步骤标题长度控制:步骤标题建议不超过 4 个字符,过长会导致步骤点间距挤压,可通过缩小字体或精简文本解决
- 列表中性能优化 :在
ListView等可滚动组件中批量使用时,建议关闭showText或复用组件实例,避免频繁重建文本组件 - 动画时长适配:进度变化较快(如倒计时)时,动画时长建议设为 300-500ms;流程步骤切换时,可设为 600-800ms,提升视觉流畅度
- 深色模式兼容性 :自定义颜色时,建议通过
_adaptDarkMode方法适配深色模式,避免浅色文本配浅色背景导致不可见