在这里插入图片描述

案例概述

本案例展示如何使用线性进度条和圆形进度条来展示任务进度。进度条是展示任务进度的重要组件,在现代应用中广泛应用于文件下载、数据加载、表单提交、数据处理、视频播放等多种场景。通过进度条,用户可以清晰地了解操作的进展情况,减少等待时的焦虑感,提高用户体验。

Flutter 提供了 LinearProgressIndicator(线性进度条)和 CircularProgressIndicator(圆形进度条)两种进度条组件,支持自定义样式、颜色、大小和动画效果。在企业应用中,进度条需要处理实时更新、多任务并行显示、缓冲进度、错误状态处理等复杂需求。

此外,进度条还应支持响应式设计以适应不同屏幕尺寸、提供无障碍支持以满足屏幕阅读器需求、支持键盘导航等功能。在 PC 端应用中,进度条需要充分利用屏幕空间,提供清晰的进度提示和详细的状态信息。

核心概念

1. LinearProgressIndicator(线性进度条)

线性进度条是最常见的进度指示器类型,适合展示单一任务的进度。其主要特点包括:

  • value 参数:接受 0-1 之间的浮点数,表示进度百分比。当 value 为 null 时,显示不确定进度的动画
  • backgroundColor:进度条背景颜色,通常为浅灰色
  • valueColor:进度条前景颜色,表示已完成的部分
  • minHeight:进度条的最小高度,默认为 4 像素
  • semanticsLabel:用于无障碍访问的标签,屏幕阅读器会读取此文本
  • 响应式特性:宽度自动填充父容器,高度可自定义

2. CircularProgressIndicator(圆形进度条)

圆形进度条适合展示圆形或紧凑的进度指示,常用于加载动画或进度展示。其主要特点包括:

  • value 参数:0-1 之间的浮点数。当 value 为 null 时,显示旋转的不确定进度动画
  • strokeWidth:进度圆形的线条宽度,默认为 4 像素
  • backgroundColor:背景圆形颜色
  • valueColor:进度圆形颜色
  • radius:圆形的半径,可用于控制整体大小
  • 动画特性:支持平滑的进度变化动画

3. 进度显示与用户反馈

进度条的显示方式直接影响用户体验:

  • 百分比文字:在进度条旁边或中心显示具体的百分比数值,如"45%"
  • 动画效果:使用 AnimatedBuilder 实现平滑的进度变化,避免生硬的跳跃
  • 状态提示:显示当前状态(未开始、进行中、已完成、出错等)
  • 多任务显示:同时展示多个任务的进度,便于用户了解整体进度
  • 缓冲进度:显示已缓冲和已加载的进度,如视频播放中的缓冲进度
  • 剩余时间:估算并显示剩余时间,如"预计还需 2 分钟"

代码详解

1. 基础线性进度条

最简单的线性进度条实现,直接显示进度值。这种方式适合快速原型开发或简单的进度展示场景。LinearProgressIndicator 会自动填充父容器的宽度,高度由 minHeight 参数控制。

LinearProgressIndicator(value: _progress)

这个基础实现使用默认的蓝色进度条和灰色背景。当 value 为 null 时,会显示一个不确定进度的动画效果,适合用于不知道具体进度的场景,如网络请求等待。

2. 自定义样式的线性进度条

通过自定义颜色、高度等参数,可以创建更符合应用设计的进度条。这种方式提供了更多的控制权,允许开发者根据应用的设计风格调整进度条的外观。

LinearProgressIndicator(
  value: _progress,
  minHeight: 8,
  backgroundColor: Colors.grey.shade300,
  valueColor: AlwaysStoppedAnimation(Colors.green),
)

在这个例子中,我们设置了进度条的高度为 8 像素,背景颜色为浅灰色,进度颜色为绿色。AlwaysStoppedAnimation 用于指定一个固定的颜色值,而不是使用主题中的颜色。

3. 圆形进度条与百分比显示

圆形进度条常用于展示圆形的进度指示,通常与百分比文字结合使用。通过 Stack 组件可以将进度条和文字叠加显示,创建一个完整的进度指示器。

Stack(
  alignment: Alignment.center,
  children: [
    SizedBox(
      width: 120,
      height: 120,
      child: CircularProgressIndicator(
        value: _progress,
        strokeWidth: 8,
        backgroundColor: Colors.grey.shade300,
        valueColor: AlwaysStoppedAnimation(Colors.blue),
      ),
    ),
    Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(
          '${(_progress * 100).toStringAsFixed(0)}%',
          style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
        ),
        Text('进行中...', style: TextStyle(fontSize: 12, color: Colors.grey)),
      ],
    ),
  ],
)

这个实现在圆形进度条中心显示百分比和状态文字,提供了更完整的进度信息。strokeWidth 参数控制圆形线条的宽度,值越大线条越粗。

4. 多任务进度条列表

在实际应用中,经常需要同时显示多个任务的进度。这种场景下,可以使用 ListView 或 Column 来展示多个进度条。

Column(
  children: List.generate(_tasks.length, (index) {
    final task = _tasks[index];
    return Padding(
      padding: EdgeInsets.symmetric(vertical: 12),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text(task['name'], style: TextStyle(fontWeight: FontWeight.bold)),
              Text('${(task['progress'] * 100).toStringAsFixed(0)}%'),
            ],
          ),
          SizedBox(height: 8),
          LinearProgressIndicator(
            value: task['progress'],
            minHeight: 6,
            backgroundColor: Colors.grey.shade300,
            valueColor: AlwaysStoppedAnimation(_getProgressColor(task['progress'])),
          ),
        ],
      ),
    );
  }),
)

这个实现展示了如何在列表中显示多个任务的进度,每个任务都有自己的进度条和百分比显示。通过 _getProgressColor 方法,可以根据进度值动态改变颜色,如进度低于 50% 时显示红色,50-80% 显示黄色,80% 以上显示绿色。

5. 带缓冲进度的进度条

在某些场景下(如视频播放),需要显示两种进度:已加载的进度和已播放的进度。这可以通过两个进度条叠加实现。

Stack(
  children: [
    LinearProgressIndicator(
      value: _bufferProgress,
      minHeight: 6,
      backgroundColor: Colors.grey.shade300,
      valueColor: AlwaysStoppedAnimation(Colors.grey.shade400),
    ),
    LinearProgressIndicator(
      value: _playProgress,
      minHeight: 6,
      backgroundColor: Colors.transparent,
      valueColor: AlwaysStoppedAnimation(Colors.blue),
    ),
  ],
)

这个实现中,第一个进度条显示缓冲进度(浅灰色),第二个进度条显示播放进度(蓝色)。通过设置第二个进度条的背景为透明,可以实现两个进度条的叠加显示。

高级话题:进度条的企业级应用

1. 动态/响应式设计与多屏幕适配

在企业应用中,进度条需要在不同屏幕尺寸下提供一致的用户体验。对于移动设备,进度条应该较细以节省空间;对于 PC 端,可以使用更粗的进度条以提高可见性。响应式设计确保进度条在任何设备上都清晰可见。

class ResponsiveProgressIndicator extends StatelessWidget {
  final double progress;
  
  const ResponsiveProgressIndicator({required this.progress});
  
  
  Widget build(BuildContext context) {
    final screenWidth = MediaQuery.of(context).size.width;
    final isMobile = screenWidth < 600;
    final isTablet = screenWidth < 1200;
    
    double minHeight;
    double fontSize;
    
    if (isMobile) {
      minHeight = 4;
      fontSize = 12;
    } else if (isTablet) {
      minHeight = 6;
      fontSize = 14;
    } else {
      minHeight = 8;
      fontSize = 16;
    }
    
    return Column(
      children: [
        LinearProgressIndicator(
          value: progress,
          minHeight: minHeight,
          backgroundColor: Colors.grey.shade300,
          valueColor: AlwaysStoppedAnimation(Colors.blue),
        ),
        SizedBox(height: 8),
        Text(
          '进度: ${(progress * 100).toStringAsFixed(0)}%',
          style: TextStyle(fontSize: fontSize),
        ),
      ],
    );
  }
}

这个实现根据屏幕宽度动态调整进度条的高度和文字大小,确保在所有设备上都有最佳的视觉效果。

2. 动画与过渡效果

平滑的动画效果可以提升用户体验,使进度变化看起来更自然。通过 AnimatedBuilder 和 AnimationController,可以实现进度条的平滑动画。

class AnimatedProgressIndicator extends StatefulWidget {
  final double targetProgress;
  
  const AnimatedProgressIndicator({required this.targetProgress});
  
  
  State<AnimatedProgressIndicator> createState() => _AnimatedProgressIndicatorState();
}

class _AnimatedProgressIndicatorState extends State<AnimatedProgressIndicator>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;
  
  
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(milliseconds: 800),
      vsync: this,
    );
    _setupAnimation();
  }
  
  void _setupAnimation() {
    _animation = Tween<double>(begin: 0, end: widget.targetProgress).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
    _controller.forward();
  }
  
  
  void didUpdateWidget(AnimatedProgressIndicator oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.targetProgress != widget.targetProgress) {
      _controller.forward(from: 0);
    }
  }
  
  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  
  
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        return Column(
          children: [
            LinearProgressIndicator(
              value: _animation.value,
              minHeight: 8,
              backgroundColor: Colors.grey.shade300,
              valueColor: AlwaysStoppedAnimation(Colors.blue),
            ),
            SizedBox(height: 8),
            Text('${(_animation.value * 100).toStringAsFixed(0)}%'),
          ],
        );
      },
    );
  }
}

这个实现使用 CurvedAnimation 提供平滑的缓动效果,使进度条的变化看起来更加自然流畅。

3. 搜索/过滤/排序功能

在显示多个任务的进度时,可能需要根据状态、优先级或其他条件进行过滤和排序。这对于大型项目管理应用特别有用。

class ProgressFilterManager {
  List<TaskProgress> _allTasks;
  List<TaskProgress> _filteredTasks;
  
  String _filterStatus = '';
  String _sortBy = 'name';
  bool _sortAscending = true;
  
  ProgressFilterManager(this._allTasks) : _filteredTasks = _allTasks;
  
  void setStatusFilter(String status) {
    _filterStatus = status;
    _applyFilters();
  }
  
  void setSortBy(String sortBy, bool ascending) {
    _sortBy = sortBy;
    _sortAscending = ascending;
    _applyFilters();
  }
  
  void _applyFilters() {
    _filteredTasks = _allTasks.where((task) {
      if (_filterStatus.isNotEmpty && task.status != _filterStatus) {
        return false;
      }
      return true;
    }).toList();
    
    _filteredTasks.sort((a, b) {
      int comparison = 0;
      switch (_sortBy) {
        case 'name':
          comparison = a.name.compareTo(b.name);
          break;
        case 'progress':
          comparison = a.progress.compareTo(b.progress);
          break;
        case 'priority':
          comparison = a.priority.compareTo(b.priority);
          break;
      }
      return _sortAscending ? comparison : -comparison;
    });
  }
  
  List<TaskProgress> get filteredTasks => _filteredTasks;
}

class TaskProgress {
  final String name;
  final double progress;
  final String status;
  final int priority;
  
  TaskProgress({
    required this.name,
    required this.progress,
    required this.status,
    required this.priority,
  });
}

4. 选择与批量操作

在任务管理应用中,用户可能需要选择多个任务进行批量操作,如批量暂停、批量删除等。

class SelectableProgressListWidget extends StatefulWidget {
  final List<TaskProgress> tasks;
  
  const SelectableProgressListWidget({required this.tasks});
  
  
  State<SelectableProgressListWidget> createState() => _SelectableProgressListWidgetState();
}

class _SelectableProgressListWidgetState extends State<SelectableProgressListWidget> {
  final Set<String> _selectedTaskIds = {};
  
  void _toggleTaskSelection(String taskId) {
    setState(() {
      if (_selectedTaskIds.contains(taskId)) {
        _selectedTaskIds.remove(taskId);
      } else {
        _selectedTaskIds.add(taskId);
      }
    });
  }
  
  void _batchPauseSelected() {
    print('暂停 ${_selectedTaskIds.length} 个任务');
    // 实现批量暂停逻辑
  }
  
  
  Widget build(BuildContext context) {
    return Column(
      children: [
        if (_selectedTaskIds.isNotEmpty)
          Container(
            padding: EdgeInsets.all(16),
            color: Colors.blue.shade50,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text('已选择 ${_selectedTaskIds.length} 个任务'),
                ElevatedButton(
                  onPressed: _batchPauseSelected,
                  child: Text('批量暂停'),
                ),
              ],
            ),
          ),
        Expanded(
          child: ListView.builder(
            itemCount: widget.tasks.length,
            itemBuilder: (context, index) {
              final task = widget.tasks[index];
              final isSelected = _selectedTaskIds.contains(task.name);
              
              return CheckboxListTile(
                value: isSelected,
                onChanged: (_) => _toggleTaskSelection(task.name),
                title: Text(task.name),
                subtitle: LinearProgressIndicator(
                  value: task.progress,
                  minHeight: 4,
                ),
              );
            },
          ),
        ),
      ],
    );
  }
}

5. 加载与缓存策略

对于需要从网络加载数据的进度条,应该实现缓存机制以避免重复加载。同时,应该处理加载失败的情况。

class ProgressDataCache {
  final Map<String, TaskProgress> _cache = {};
  bool _isLoading = false;
  
  Future<List<TaskProgress>> loadTaskProgress() async {
    if (_cache.isNotEmpty) {
      return _cache.values.toList();
    }
    
    _isLoading = true;
    try {
      await Future.delayed(Duration(seconds: 2));
      
      final tasks = [
        TaskProgress(name: '任务1', progress: 0.3, status: 'running', priority: 1),
        TaskProgress(name: '任务2', progress: 0.7, status: 'running', priority: 2),
        TaskProgress(name: '任务3', progress: 1.0, status: 'completed', priority: 3),
      ];
      
      for (var task in tasks) {
        _cache[task.name] = task;
      }
      
      return tasks;
    } finally {
      _isLoading = false;
    }
  }
  
  void updateTaskProgress(String taskName, double progress) {
    if (_cache.containsKey(taskName)) {
      _cache[taskName] = TaskProgress(
        name: _cache[taskName]!.name,
        progress: progress,
        status: progress >= 1.0 ? 'completed' : 'running',
        priority: _cache[taskName]!.priority,
      );
    }
  }
  
  void clearCache() {
    _cache.clear();
  }
  
  bool get isLoading => _isLoading;
}

6. 键盘导航与快捷键

为了提高 PC 端的可用性,应该支持键盘导航。用户可以使用方向键在任务之间导航,使用 Enter 键选择任务。

class KeyboardNavigableProgressList extends StatefulWidget {
  final List<TaskProgress> tasks;
  
  const KeyboardNavigableProgressList({required this.tasks});
  
  
  State<KeyboardNavigableProgressList> createState() => _KeyboardNavigableProgressListState();
}

class _KeyboardNavigableProgressListState extends State<KeyboardNavigableProgressList> {
  int _focusedIndex = 0;
  late FocusNode _focusNode;
  
  
  void initState() {
    super.initState();
    _focusNode = FocusNode();
  }
  
  
  void dispose() {
    _focusNode.dispose();
    super.dispose();
  }
  
  void _handleKeyEvent(RawKeyEvent event) {
    if (event.isKeyPressed(LogicalKeyboardKey.arrowDown)) {
      setState(() {
        _focusedIndex = (_focusedIndex + 1) % widget.tasks.length;
      });
    } else if (event.isKeyPressed(LogicalKeyboardKey.arrowUp)) {
      setState(() {
        _focusedIndex = (_focusedIndex - 1 + widget.tasks.length) % widget.tasks.length;
      });
    } else if (event.isKeyPressed(LogicalKeyboardKey.enter)) {
      print('选择任务: ${widget.tasks[_focusedIndex].name}');
    }
  }
  
  
  Widget build(BuildContext context) {
    return RawKeyboardListener(
      focusNode: _focusNode,
      onKey: _handleKeyEvent,
      child: ListView.builder(
        itemCount: widget.tasks.length,
        itemBuilder: (context, index) {
          final task = widget.tasks[index];
          final isFocused = index == _focusedIndex;
          
          return Container(
            decoration: BoxDecoration(
              border: isFocused ? Border.all(color: Colors.blue, width: 2) : null,
            ),
            child: ListTile(
              title: Text(task.name),
              subtitle: LinearProgressIndicator(value: task.progress),
            ),
          );
        },
      ),
    );
  }
}

7. 无障碍支持与屏幕阅读器

为了确保应用对所有用户都可用,应该为进度条添加适当的无障碍标签。屏幕阅读器用户可以通过这些标签了解进度信息。

class AccessibleProgressIndicator extends StatelessWidget {
  final double progress;
  final String taskName;
  
  const AccessibleProgressIndicator({
    required this.progress,
    required this.taskName,
  });
  
  
  Widget build(BuildContext context) {
    final percentage = (progress * 100).toStringAsFixed(0);
    
    return Semantics(
      label: '$taskName 进度 $percentage 百分比',
      slider: true,
      onIncrease: () => print('增加进度'),
      onDecrease: () => print('减少进度'),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Semantics(
            label: '任务名称',
            child: Text(taskName, style: TextStyle(fontWeight: FontWeight.bold)),
          ),
          SizedBox(height: 8),
          Semantics(
            label: '进度条',
            child: LinearProgressIndicator(
              value: progress,
              minHeight: 8,
              semanticsLabel: '$percentage% 完成',
            ),
          ),
          SizedBox(height: 8),
          Semantics(
            label: '百分比',
            child: Text('$percentage%'),
          ),
        ],
      ),
    );
  }
}

8. 样式自定义与主题适配

不同的应用可能需要不同的进度条样式。通过创建可配置的主题类,可以轻松适配不同的设计风格。

class ProgressIndicatorTheme {
  final Color progressColor;
  final Color backgroundColor;
  final double minHeight;
  final Curve animationCurve;
  final Duration animationDuration;
  
  const ProgressIndicatorTheme({
    this.progressColor = Colors.blue,
    this.backgroundColor = const Color(0xFFE0E0E0),
    this.minHeight = 4,
    this.animationCurve = Curves.easeInOut,
    this.animationDuration = const Duration(milliseconds: 800),
  });
  
  static ProgressIndicatorTheme light() {
    return ProgressIndicatorTheme(
      progressColor: Colors.blue,
      backgroundColor: Colors.grey.shade300,
    );
  }
  
  static ProgressIndicatorTheme dark() {
    return ProgressIndicatorTheme(
      progressColor: Colors.blue.shade300,
      backgroundColor: Colors.grey.shade700,
    );
  }
  
  static ProgressIndicatorTheme success() {
    return ProgressIndicatorTheme(
      progressColor: Colors.green,
      backgroundColor: Colors.green.shade100,
    );
  }
}

9. 数据持久化与导出

在某些应用中,可能需要保存进度信息以便后续查看或分析。这可以通过将数据导出为 JSON 或 CSV 格式实现。

class ProgressDataExporter {
  static String exportToJSON(List<TaskProgress> tasks) {
    final jsonList = tasks.map((task) => {
      'name': task.name,
      'progress': task.progress,
      'status': task.status,
      'priority': task.priority,
    }).toList();
    
    return jsonEncode(jsonList);
  }
  
  static String exportToCSV(List<TaskProgress> tasks) {
    final buffer = StringBuffer();
    buffer.writeln('任务名,进度,状态,优先级');
    
    for (var task in tasks) {
      buffer.writeln('${task.name},${task.progress},${task.status},${task.priority}');
    }
    
    return buffer.toString();
  }
}

10. 单元测试与集成测试

为了确保进度条功能的正确性,应该编写全面的测试用例。

void main() {
  group('Progress Indicator Tests', () {
    test('进度条值更新', () {
      double progress = 0.0;
      expect(progress, 0.0);
      progress = 0.5;
      expect(progress, 0.5);
      progress = 1.0;
      expect(progress, 1.0);
    });
    
    test('百分比计算', () {
      double progress = 0.75;
      final percentage = (progress * 100).toStringAsFixed(0);
      expect(percentage, '75');
    });
    
    test('任务过滤', () {
      final tasks = [
        TaskProgress(name: '任务1', progress: 0.3, status: 'running', priority: 1),
        TaskProgress(name: '任务2', progress: 0.7, status: 'running', priority: 2),
        TaskProgress(name: '任务3', progress: 1.0, status: 'completed', priority: 3),
      ];
      
      final completed = tasks.where((t) => t.status == 'completed').toList();
      expect(completed.length, 1);
      expect(completed[0].name, '任务3');
    });
  });
  
  testWidgets('Progress Indicator Widget 集成测试', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: LinearProgressIndicator(value: 0.5),
        ),
      ),
    );
    
    expect(find.byType(LinearProgressIndicator), findsOneWidget);
  });
}

OpenHarmony PC 端适配要点

  1. 屏幕宽度检测:根据不同屏幕宽度调整进度条高度和文字大小
  2. 响应式布局:在 PC 端使用更粗的进度条以提高可见性
  3. 键盘导航:支持方向键在任务之间导航
  4. 鼠标交互:支持点击进度条直接跳转到指定进度
  5. 无障碍支持:为屏幕阅读器提供完整的语义标签

实际应用场景

  1. 文件下载:显示下载进度和剩余时间
  2. 数据加载:显示数据加载进度
  3. 视频播放:显示播放进度和缓冲进度
  4. 任务管理:显示多个任务的进度
  5. 安装程序:显示安装进度

扩展建议

  1. 支持暂停和恢复进度
  2. 实现进度预测和剩余时间估算
  3. 添加进度历史记录
  4. 支持进度通知和提醒
  5. 实现进度数据的导出和分析

总结

进度条是提升用户体验的重要组件。通过合理的设计和实现,可以创建出功能完整、用户体验良好的进度条系统。在 PC 端应用中,充分利用屏幕空间、提供键盘导航和无障碍支持是关键。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐