在这里插入图片描述

刷牙计时是口腔护理App的核心功能之一,帮助用户养成科学的刷牙习惯。牙医建议每次刷牙至少2-3分钟,但很多人实际刷牙时间远远不够。通过计时功能,用户可以直观地看到自己的刷牙时长,逐步养成正确的刷牙习惯。

计时功能的设计目标

计时页面需要实现几个核心功能:显示当前刷牙时长、提供开始/暂停/重置控制、达到目标时间后提醒用户、保存刷牙记录并计算评分。界面设计要简洁直观,用户在刷牙时能够一眼看清当前进度,操作按钮要足够大方便单手操作。

依赖导入

import 'dart:async';

dart:async库提供了Timer类,用于实现定时器功能。

这是实现计时功能的核心依赖。

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

Flutter核心库和Provider状态管理。

Provider用于保存刷牙记录到全局状态。

import 'package:percent_indicator/circular_percent_indicator.dart';

环形进度指示器组件。

用于显示刷牙进度,比纯文字更直观。

import '../../providers/app_provider.dart';
import '../../models/oral_models.dart';

AppProvider用于保存记录,BrushRecord是刷牙记录的数据模型。

状态变量定义

class BrushTimerPage extends StatefulWidget {
  const BrushTimerPage({super.key});

  
  State<BrushTimerPage> createState() => _BrushTimerPageState();
}

计时页面需要管理多个状态,所以使用StatefulWidget。

包括计时秒数、运行状态、选中的时间段等。

class _BrushTimerPageState extends State<BrushTimerPage> {
  int _seconds = 0;
  int _targetSeconds = 180;

_seconds记录当前已刷牙的秒数。

_targetSeconds是目标时长,默认180秒即3分钟。

  Timer? _timer;
  bool _isRunning = false;

_timer是定时器对象,用于每秒更新计时。

_isRunning标记计时器是否正在运行。

  String _selectedType = 'morning';

  final Map<String, String> _typeLabels = {
    'morning': '早晨',
    'noon': '中午',
    'evening': '晚上',
  };

_selectedType记录用户选择的刷牙时间段。

_typeLabels定义时间段的显示文字。

生命周期管理

  
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }

dispose方法在页面销毁时调用。

必须取消定时器,否则会造成内存泄漏和后台持续运行。

计时器控制方法

  void _startTimer() {
    setState(() => _isRunning = true);
    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      setState(() {
        _seconds++;
        if (_seconds >= _targetSeconds) {
          _stopTimer();
          _showCompleteDialog();
        }
      });
    });
  }

_startTimer启动计时器。

Timer.periodic每秒执行一次回调,更新_seconds并检查是否达到目标。

  void _stopTimer() {
    _timer?.cancel();
    setState(() => _isRunning = false);
  }

_stopTimer暂停计时器。

调用cancel()停止定时器,更新运行状态。

  void _resetTimer() {
    _timer?.cancel();
    setState(() {
      _seconds = 0;
      _isRunning = false;
    });
  }

_resetTimer重置计时器。

停止定时器并将秒数归零。

保存记录功能

  void _saveRecord() {
    if (_seconds < 30) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('刷牙时间太短,至少需要30秒')),
      );
      return;
    }

保存前检查刷牙时长。

少于30秒不允许保存,用SnackBar提示用户。

    final score = _calculateScore();
    final record = BrushRecord(
      dateTime: DateTime.now(),
      durationSeconds: _seconds,
      type: _selectedType,
      score: score,
    );

计算评分并创建BrushRecord对象。

记录包含时间、时长、类型和评分四个字段。

    context.read<AppProvider>().addBrushRecord(record);
    
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('记录已保存!评分:$score')),
    );
    
    _resetTimer();
  }

调用provider的方法保存记录。

显示保存成功提示,然后重置计时器。

评分计算逻辑

  int _calculateScore() {
    if (_seconds >= 180) return 100;
    if (_seconds >= 150) return 95;
    if (_seconds >= 120) return 90;
    if (_seconds >= 90) return 85;
    if (_seconds >= 60) return 80;
    return 70;
  }

根据刷牙时长计算评分。

3分钟以上满分,时间越短分数越低。

这种阶梯式评分鼓励用户延长刷牙时间。

完成提醒对话框

  void _showCompleteDialog() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('🎉 刷牙完成!'),
        content: const Text('太棒了!你已经完成了3分钟的刷牙,保持良好习惯!'),

达到目标时间后弹出对话框。

标题带emoji增加趣味性,内容给予正向鼓励。

        actions: [
          TextButton(
            onPressed: () {
              Navigator.pop(context);
              _saveRecord();
            },
            child: const Text('保存记录'),
          ),
        ],
      ),
    );
  }

对话框只有一个"保存记录"按钮。

点击后关闭对话框并保存记录。

页面UI构建

  
  Widget build(BuildContext context) {
    final progress = _seconds / _targetSeconds;
    final minutes = _seconds ~/ 60;
    final secs = _seconds % 60;

计算进度百分比和分秒显示值。

~/是整除运算符,%是取余运算符。

    return Scaffold(
      appBar: AppBar(title: const Text('刷牙计时')),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(24),

Scaffold提供页面结构,SingleChildScrollView让内容可滚动。

24像素内边距让内容不会太靠边。

时间段选择器

            Container(
              padding: const EdgeInsets.all(4),
              decoration: BoxDecoration(
                color: Colors.grey.shade200,
                borderRadius: BorderRadius.circular(12),
              ),

时间段选择器的外层容器。

灰色背景,圆角设计。

              child: Row(
                children: _typeLabels.entries.map((entry) => Expanded(
                  child: GestureDetector(
                    onTap: () => setState(() => _selectedType = entry.key),

三个选项横向排列,Expanded让它们等宽。

点击时更新选中状态。

                    child: Container(
                      padding: const EdgeInsets.symmetric(vertical: 12),
                      decoration: BoxDecoration(
                        color: _selectedType == entry.key ? const Color(0xFF26A69A) : Colors.transparent,
                        borderRadius: BorderRadius.circular(10),
                      ),

选中的选项显示绿色背景,未选中透明。

这种设计类似iOS的分段控制器。

                      child: Text(
                        entry.value,
                        textAlign: TextAlign.center,
                        style: TextStyle(
                          color: _selectedType == entry.key ? Colors.white : Colors.grey.shade600,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ),
                  ),
                )).toList(),
              ),
            ),

选中时文字白色,未选中时灰色。

加粗字体让文字更清晰。

环形进度指示器

            CircularPercentIndicator(
              radius: 140,
              lineWidth: 15,
              percent: progress.clamp(0, 1),

大尺寸的环形进度条,半径140像素。

线宽15像素,进度值限制在0-1之间。

              center: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(
                    '${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}',
                    style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
                  ),

环形中间显示时间,格式为"00:00"。

padLeft确保个位数时前面补0。

                  Text(
                    '目标 ${_targetSeconds ~/ 60} 分钟',
                    style: TextStyle(color: Colors.grey.shade600),
                  ),
                ],
              ),

下方显示目标时长提示。

灰色小字作为辅助信息。

              progressColor: const Color(0xFF26A69A),
              backgroundColor: Colors.grey.shade200,
              circularStrokeCap: CircularStrokeCap.round,
            ),

进度条绿色,背景灰色,两端圆形。

控制按钮区域

            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                if (!_isRunning && _seconds > 0)
                  ElevatedButton.icon(
                    onPressed: _resetTimer,
                    icon: const Icon(Icons.refresh),
                    label: const Text('重置'),

重置按钮只在暂停且有计时时显示。

使用条件渲染控制按钮的显示。

                    style: ElevatedButton.styleFrom(
                      backgroundColor: Colors.grey.shade300,
                      foregroundColor: Colors.black87,
                      padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
                    ),
                  ),

灰色背景,黑色文字,与主按钮区分。

                ElevatedButton.icon(
                  onPressed: _isRunning ? _stopTimer : _startTimer,
                  icon: Icon(_isRunning ? Icons.pause : Icons.play_arrow),
                  label: Text(_isRunning ? '暂停' : (_seconds > 0 ? '继续' : '开始')),

主按钮根据状态显示不同的图标和文字。

运行中显示暂停,暂停时显示继续或开始。

                  style: ElevatedButton.styleFrom(
                    backgroundColor: const Color(0xFF26A69A),
                    foregroundColor: Colors.white,
                    padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
                  ),
                ),
              ],
            ),

绿色背景,白色文字,是页面的主要操作按钮。

保存记录按钮

            if (_seconds > 0 && !_isRunning)
              ElevatedButton.icon(
                onPressed: _saveRecord,
                icon: const Icon(Icons.save),
                label: const Text('保存记录'),
                style: ElevatedButton.styleFrom(
                  backgroundColor: const Color(0xFF4DB6AC),
                  foregroundColor: Colors.white,
                  padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
                ),
              ),

保存按钮只在暂停且有计时时显示。

使用稍浅的绿色与主按钮区分。

刷牙提示区域

            Container(
              padding: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                color: const Color(0xFF26A69A).withOpacity(0.1),
                borderRadius: BorderRadius.circular(12),
              ),

提示区域使用浅绿色背景。

圆角设计与整体风格一致。

              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: const [
                  Text('刷牙小贴士', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
                  SizedBox(height: 8),
                  Text('• 使用巴氏刷牙法,牙刷与牙齿呈45度角'),
                  Text('• 每个区域刷10-15次'),
                  Text('• 不要忘记刷舌头'),
                  Text('• 建议每次刷牙2-3分钟'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

提供实用的刷牙建议。

用户在等待计时的同时可以学习正确的刷牙方法。

Timer的使用注意事项

Timer.periodic创建的定时器会持续运行,即使页面不可见。因此必须在dispose方法中取消定时器,否则会造成内存泄漏。另外,定时器的回调中调用setState时,如果Widget已经被销毁会报错,所以在复杂场景下需要先检查mounted属性。

小结

刷牙计时页面通过Timer实现了精确的秒级计时,环形进度条直观展示刷牙进度。时间段选择器让用户标记是早中晚哪次刷牙,评分系统根据时长给出分数激励用户。控制按钮根据状态动态显示,交互逻辑清晰。刷牙提示区域提供了实用的护理建议,让页面不仅是工具,也是知识传播的载体。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐