Flutter for OpenHarmony 跨平台开发:倒计时功能实战指南

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. 时间输入:支持小时、分钟、秒的自定义输入
  2. 快捷预设:提供1分钟、5分钟、10分钟、15分钟、30分钟、1小时等常用预设
  3. 控制操作:支持开始、暂停、继续、重置等操作
  4. 进度展示:以进度条形式展示剩余时间比例
  5. 完成提醒:倒计时结束时弹出提示对话框
  6. 状态显示:清晰展示当前状态(设置中、进行中、已暂停)

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应用,需要注意:

  1. 签名配置:需要在DevEco Studio中配置应用签名
  2. 定时器精度:Timer在鸿蒙平台运行正常,精度满足需求
  3. 输入法适配:TextField在鸿蒙平台正常工作

八、总结与展望

本文详细介绍了使用Flutter for OpenHarmony开发倒计时功能的完整过程。通过Timer定时器、时间格式化、LinearProgressIndicator进度展示、ActionChip预设按钮等技术的综合运用,实现了一个功能完善、交互友好的倒计时应用。

技术要点回顾

  • 使用Timer.periodic实现倒计时
  • 实现暂停、继续、重置功能
  • 使用LinearProgressIndicator展示进度
  • 使用ActionChip实现快捷预设
  • 使用LinearGradient实现渐变背景

扩展方向

  • 数据持久化:保存常用预设到本地存储
  • 通知提醒:集成本地通知,倒计时结束提醒
  • 声音提醒:播放提示音
  • 多任务支持:同时运行多个倒计时

Flutter for OpenHarmony为开发者提供了便捷的跨平台开发能力,使得倒计时等实用功能能够高效地在鸿蒙设备上实现。随着鸿蒙生态的不断发展,Flutter跨平台技术将在更多应用场景中发挥重要作用。

相关推荐
liulian09163 小时前
Flutter for OpenHarmony 实用功能实战合集:日历打卡 + 高清图片浏览一站式指南
flutter
liulian09164 小时前
Flutter for OpenHarmony 跨平台开发:日历打卡功能实战指南
flutter
liulian09165 小时前
Flutter for OpenHarmony 用户登录与身份认证 + 地图功能适配综合实现指南
flutter
liulian09165 小时前
Flutter for OpenHarmony 跨平台开发:记事本功能实战指南
flutter
maaath5 小时前
【maaath】Flutter for OpenHarmony 学习答题应用实战开发
学习·flutter·华为·harmonyos
jiejiejiejie_6 小时前
Flutter for OpenHarmony 喝水提醒功能的实现
flutter
maaath6 小时前
【maaath】Flutter for OpenHarmony 实战:记账理财应用开发指南
flutter·华为·harmonyos
jiejiejiejie_6 小时前
Flutter for OpenHarmony 账单记录功能实战指南
flutter
千码君20166 小时前
flutter: 分享一下基于trae cn 构建的过程
java·vscode·flutter·kotlin·trae