Flutter for OpenHarmony 跨平台开发:倒计时功能实战指南
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
一、引言
倒计时是移动应用中常见的功能模块,广泛应用于运动健身、烹饪计时、学习提醒、会议倒计时等场景。一个完善的倒计时功能需要支持自定义时间输入、快捷预设、暂停续播、进度展示等特性,对开发者的状态管理能力和用户交互设计提出了较高要求。
Flutter作为Google推出的开源UI框架,凭借其跨平台能力和丰富的组件生态,为倒计时功能的实现提供了便捷的技术方案。Flutter for OpenHarmony的出现,使得Flutter开发者能够将应用部署到鸿蒙设备,进一步拓展了跨平台开发的应用范围。
本文将以倒计时功能为例,详细介绍如何使用Flutter for OpenHarmony实现时间输入、定时器控制、进度展示、完成提醒等功能,为开发者提供完整的技术参考。
二、技术背景
2.1 Flutter for OpenHarmony概述
Flutter是Google于2017年发布的开源UI框架,采用Dart语言进行开发。Flutter通过Skia渲染引擎实现自绘,不依赖平台原生组件,从而保证了不同平台上UI的一致性。
OpenHarmony是由开放原子开源基金会孵化的开源操作系统项目,旨在构建万物智联的操作系统生态。Flutter for OpenHarmony是Flutter在OpenHarmony平台上的适配实现,使Flutter开发者能够将应用无缝部署到鸿蒙设备。
2.2 倒计时的技术架构
实现倒计时功能涉及以下核心技术:
定时器管理:使用Dart的Timer类实现倒计时逻辑,需要处理开始、暂停、继续、重置等操作。
时间格式化:将秒数转换为时:分:秒格式显示,支持不同时长的自适应展示。
状态管理:管理运行状态、暂停状态、剩余时间、总时间等数据。
进度展示:使用LinearProgressIndicator展示倒计时进度。
预设功能:提供常用时间预设,提升用户体验。
2.3 Flutter与原生鸿蒙开发的对比
| 对比维度 | Flutter for OpenHarmony | 原生鸿蒙开发(ArkTS) |
|---|---|---|
| 编程语言 | Dart | ArkTS |
| 定时器 | Timer类简洁易用 | 需要手动实现 |
| 进度组件 | LinearProgressIndicator完善 | 需要手动实现 |
| 输入组件 | TextField功能完善 | 需要适配 |
| 跨平台能力 | 支持多平台 | 仅限鸿蒙平台 |
| 开发效率 | 热重载支持 | 需要重新编译 |
三、功能设计
3.1 需求分析
倒计时功能的核心需求包括:
- 时间输入:支持小时、分钟、秒的自定义输入
- 快捷预设:提供1分钟、5分钟、10分钟、15分钟、30分钟、1小时等常用预设
- 控制操作:支持开始、暂停、继续、重置等操作
- 进度展示:以进度条形式展示剩余时间比例
- 完成提醒:倒计时结束时弹出提示对话框
- 状态显示:清晰展示当前状态(设置中、进行中、已暂停)
3.2 状态变量设计
使用以下状态变量管理倒计时状态:
dart
// 定时器实例
Timer? _timer;
// 总秒数
int _totalSeconds = 0;
// 剩余秒数
int _remainingSeconds = 0;
// 是否正在运行
bool _isRunning = false;
// 是否已暂停
bool _isPaused = false;
// 时间输入控制器
final TextEditingController _hoursController = TextEditingController(text: '0');
final TextEditingController _minutesController = TextEditingController(text: '5');
final TextEditingController _secondsController = TextEditingController(text: '0');
// 预设列表
final List<Map<String, dynamic>> _presets = [
{'name': '1分钟', 'seconds': 60},
{'name': '5分钟', 'seconds': 300},
{'name': '10分钟', 'seconds': 600},
{'name': '15分钟', 'seconds': 900},
{'name': '30分钟', 'seconds': 1800},
{'name': '1小时', 'seconds': 3600},
];
3.3 界面设计
界面分为以下几个部分:
时间显示面板:渐变背景容器,显示格式化的倒计时时间和当前状态
时间输入区域:三个输入框分别设置时、分、秒
快捷预设区域:ActionChip形式的预设按钮
控制按钮区域:开始/暂停/继续/重置按钮
进度条区域:LinearProgressIndicator展示剩余比例
四、核心实现
4.1 开始倒计时
启动倒计时的实现:
dart
void _startCountdown() {
// 如果没有剩余时间,从输入框获取
if (_remainingSeconds <= 0) {
final hours = int.tryParse(_hoursController.text) ?? 0;
final minutes = int.tryParse(_minutesController.text) ?? 0;
final seconds = int.tryParse(_secondsController.text) ?? 0;
_totalSeconds = hours * 3600 + minutes * 60 + seconds;
_remainingSeconds = _totalSeconds;
}
if (_remainingSeconds <= 0) return;
setState(() {
_isRunning = true;
_isPaused = false;
});
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() {
if (_remainingSeconds > 0) {
_remainingSeconds--;
} else {
_completeCountdown();
}
});
});
}
4.2 暂停倒计时
暂停定时器的实现:
dart
void _pauseCountdown() {
_timer?.cancel();
setState(() {
_isRunning = false;
_isPaused = true;
});
}
4.3 继续倒计时
从暂停状态继续:
dart
void _resumeCountdown() {
_startCountdown();
}
4.4 重置倒计时
重置所有状态:
dart
void _resetCountdown() {
_timer?.cancel();
setState(() {
_isRunning = false;
_isPaused = false;
_remainingSeconds = 0;
_totalSeconds = 0;
});
}
4.5 完成处理
倒计时结束时的处理:
dart
void _completeCountdown() {
_timer?.cancel();
setState(() {
_isRunning = false;
_isPaused = false;
});
_showCompleteDialog();
}
4.6 时间格式化
将秒数格式化为显示字符串:
dart
String _formatTime(int totalSeconds) {
final hours = totalSeconds ~/ 3600;
final minutes = (totalSeconds % 3600) ~/ 60;
final seconds = totalSeconds % 60;
if (hours > 0) {
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
4.7 使用预设
点击预设按钮时:
dart
void _usePreset(int seconds) {
_hoursController.text = (seconds ~/ 3600).toString();
_minutesController.text = ((seconds % 3600) ~/ 60).toString();
_secondsController.text = (seconds % 60).toString();
setState(() {
_totalSeconds = seconds;
_remainingSeconds = seconds;
});
}
五、完整代码实现
dart
import 'dart:async';
import 'package:flutter/material.dart';
class CountdownFeature extends StatefulWidget {
const CountdownFeature({super.key});
@override
State<CountdownFeature> createState() => _CountdownFeatureState();
}
class _CountdownFeatureState extends State<CountdownFeature> {
Timer? _timer;
int _totalSeconds = 0;
int _remainingSeconds = 0;
bool _isRunning = false;
bool _isPaused = false;
final TextEditingController _hoursController = TextEditingController(text: '0');
final TextEditingController _minutesController = TextEditingController(text: '5');
final TextEditingController _secondsController = TextEditingController(text: '0');
final List<Map<String, dynamic>> _presets = [
{'name': '1分钟', 'seconds': 60},
{'name': '5分钟', 'seconds': 300},
{'name': '10分钟', 'seconds': 600},
{'name': '15分钟', 'seconds': 900},
{'name': '30分钟', 'seconds': 1800},
{'name': '1小时', 'seconds': 3600},
];
void _startCountdown() {
if (_remainingSeconds <= 0) {
final hours = int.tryParse(_hoursController.text) ?? 0;
final minutes = int.tryParse(_minutesController.text) ?? 0;
final seconds = int.tryParse(_secondsController.text) ?? 0;
_totalSeconds = hours * 3600 + minutes * 60 + seconds;
_remainingSeconds = _totalSeconds;
}
if (_remainingSeconds <= 0) return;
setState(() {
_isRunning = true;
_isPaused = false;
});
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() {
if (_remainingSeconds > 0) {
_remainingSeconds--;
} else {
_completeCountdown();
}
});
});
}
void _pauseCountdown() {
_timer?.cancel();
setState(() {
_isRunning = false;
_isPaused = true;
});
}
void _resumeCountdown() {
_startCountdown();
}
void _resetCountdown() {
_timer?.cancel();
setState(() {
_isRunning = false;
_isPaused = false;
_remainingSeconds = 0;
_totalSeconds = 0;
});
}
void _completeCountdown() {
_timer?.cancel();
setState(() {
_isRunning = false;
_isPaused = false;
});
_showCompleteDialog();
}
void _showCompleteDialog() {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Row(
children: const [
Icon(Icons.alarm, color: Colors.red, size: 28),
SizedBox(width: 8),
Text('时间到!'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('倒计时结束', style: TextStyle(fontSize: 18)),
const SizedBox(height: 16),
const Icon(Icons.timer_off, size: 64, color: Colors.red),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
_resetCountdown();
},
child: const Text('确定'),
),
],
),
);
}
void _usePreset(int seconds) {
_hoursController.text = (seconds ~/ 3600).toString();
_minutesController.text = ((seconds % 3600) ~/ 60).toString();
_secondsController.text = (seconds % 60).toString();
setState(() {
_totalSeconds = seconds;
_remainingSeconds = seconds;
});
}
String _formatTime(int totalSeconds) {
final hours = totalSeconds ~/ 3600;
final minutes = (totalSeconds % 3600) ~/ 60;
final seconds = totalSeconds % 60;
if (hours > 0) {
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
_buildTimerDisplay(),
const SizedBox(height: 32),
if (!_isRunning && !_isPaused) _buildTimeInput(),
if (!_isRunning) ...[
const SizedBox(height: 16),
_buildPresets(),
],
const SizedBox(height: 32),
_buildControls(),
const SizedBox(height: 24),
_buildProgressBar(),
],
),
),
);
}
Widget _buildTimerDisplay() {
return Container(
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: _isRunning
? [Colors.red.shade400, Colors.red.shade600]
: [Colors.blue.shade400, Colors.blue.shade600],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: (_isRunning ? Colors.red : Colors.blue).withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
children: [
Text(
_formatTime(_remainingSeconds > 0 ? _remainingSeconds : 0),
style: const TextStyle(
fontSize: 64,
fontWeight: FontWeight.bold,
color: Colors.white,
fontFamily: 'monospace',
),
),
const SizedBox(height: 8),
Text(
_isRunning
? '倒计时进行中...'
: (_isPaused ? '已暂停' : '设置倒计时'),
style: TextStyle(
fontSize: 16,
color: Colors.white.withOpacity(0.8),
),
),
],
),
);
}
Widget _buildTimeInput() {
return Column(
children: [
const Text('设置时间', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildTimeInputField(_hoursController, '时'),
const Text(':', style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold)),
_buildTimeInputField(_minutesController, '分'),
const Text(':', style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold)),
_buildTimeInputField(_secondsController, '秒'),
],
),
],
);
}
Widget _buildTimeInputField(TextEditingController controller, String label) {
return Column(
children: [
SizedBox(
width: 70,
child: TextField(
controller: controller,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(vertical: 12),
),
),
),
Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)),
],
);
}
Widget _buildPresets() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('快捷预设', style: TextStyle(fontSize: 14, color: Colors.grey)),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: _presets.map((preset) => ActionChip(
label: Text(preset['name']),
backgroundColor: Colors.blue.shade50,
side: BorderSide(color: Colors.blue.shade200),
onPressed: () => _usePreset(preset['seconds']),
)).toList(),
),
],
);
}
Widget _buildControls() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (!_isRunning && !_isPaused)
ElevatedButton.icon(
icon: const Icon(Icons.play_arrow),
label: const Text('开始'),
onPressed: _startCountdown,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
),
),
if (_isRunning)
ElevatedButton.icon(
icon: const Icon(Icons.pause),
label: const Text('暂停'),
onPressed: _pauseCountdown,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
),
),
if (_isPaused) ...[
ElevatedButton.icon(
icon: const Icon(Icons.play_arrow),
label: const Text('继续'),
onPressed: _resumeCountdown,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
),
),
const SizedBox(width: 16),
],
if (_isRunning || _isPaused)
ElevatedButton.icon(
icon: const Icon(Icons.refresh),
label: const Text('重置'),
onPressed: _resetCountdown,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
),
),
],
);
}
Widget _buildProgressBar() {
if (_totalSeconds == 0) return const SizedBox();
final progress = _remainingSeconds / _totalSeconds;
return Column(
children: [
LinearProgressIndicator(
value: progress,
minHeight: 8,
backgroundColor: Colors.grey.shade200,
valueColor: AlwaysStoppedAnimation(
_isRunning ? Colors.red : Colors.blue,
),
borderRadius: BorderRadius.circular(4),
),
const SizedBox(height: 8),
Text(
'剩余 ${(_remainingSeconds / 60).toStringAsFixed(1)} 分钟',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
);
}
}
六、运行效果

七、关键技术点解析
7.1 Timer定时器
Dart的Timer类是实现倒计时的核心:
dart
// 创建周期性定时器
Timer.periodic(const Duration(seconds: 1), (timer) {
// 每秒执行一次
if (_remainingSeconds > 0) {
_remainingSeconds--;
} else {
timer.cancel();
_completeCountdown();
}
});
// 取消定时器
_timer?.cancel();
Timer.periodic创建周期性定时器,适合用于倒计时场景。注意在dispose中取消定时器,避免内存泄漏。
7.2 时间格式化
将秒数转换为可读的时间格式:
dart
String _formatTime(int totalSeconds) {
final hours = totalSeconds ~/ 3600;
final minutes = (totalSeconds % 3600) ~/ 60;
final seconds = totalSeconds % 60;
if (hours > 0) {
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
使用~/进行整数除法,%取余数,padLeft补零对齐。
7.3 LinearProgressIndicator进度条
LinearProgressIndicator用于展示线性进度:
dart
LinearProgressIndicator(
value: progress, // 进度值 0.0-1.0
minHeight: 8, // 高度
backgroundColor: Colors.grey.shade200, // 背景色
valueColor: AlwaysStoppedAnimation(Colors.red), // 进度色
borderRadius: BorderRadius.circular(4), // 圆角
)
7.4 ActionChip预设按钮
ActionChip用于实现可点击的标签按钮:
dart
ActionChip(
label: Text('5分钟'),
backgroundColor: Colors.blue.shade50,
side: BorderSide(color: Colors.blue.shade200),
onPressed: () => _usePreset(300),
)
7.5 渐变背景容器
使用LinearGradient实现渐变效果:
dart
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.red.shade400, Colors.red.shade600],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.red.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
)
7.6 OpenHarmony平台适配要点
在OpenHarmony设备上运行Flutter应用,需要注意:
- 签名配置:需要在DevEco Studio中配置应用签名
- 定时器精度:Timer在鸿蒙平台运行正常,精度满足需求
- 输入法适配:TextField在鸿蒙平台正常工作
八、总结与展望
本文详细介绍了使用Flutter for OpenHarmony开发倒计时功能的完整过程。通过Timer定时器、时间格式化、LinearProgressIndicator进度展示、ActionChip预设按钮等技术的综合运用,实现了一个功能完善、交互友好的倒计时应用。
技术要点回顾:
- 使用Timer.periodic实现倒计时
- 实现暂停、继续、重置功能
- 使用LinearProgressIndicator展示进度
- 使用ActionChip实现快捷预设
- 使用LinearGradient实现渐变背景
扩展方向:
- 数据持久化:保存常用预设到本地存储
- 通知提醒:集成本地通知,倒计时结束提醒
- 声音提醒:播放提示音
- 多任务支持:同时运行多个倒计时
Flutter for OpenHarmony为开发者提供了便捷的跨平台开发能力,使得倒计时等实用功能能够高效地在鸿蒙设备上实现。随着鸿蒙生态的不断发展,Flutter跨平台技术将在更多应用场景中发挥重要作用。