Flutter for OpenHarmony音乐播放器App实战:定时关闭实现

睡前听音乐是很多人的习惯,但如果音乐一直播放到天亮,不仅费电还可能影响睡眠质量。定时关闭功能对于音乐播放器来说是一个非常实用的功能。本篇将详细介绍如何实现一个完整的睡眠定时器页面。

功能分析

睡眠定时器页面需要实现以下功能:圆形计时器显示、预设时间快捷选择(15分钟、30分钟、45分钟、60分钟、90分钟)、特殊选项"播完当前"、自定义时间设置(5到180分钟)、定时器启动和取消、实时倒计时显示。

核心技术点

本篇涉及的核心技术包括:StatefulWidget状态管理、BoxDecoration圆形装饰、Wrap流式布局、showModalBottomSheet底部弹窗、StatefulBuilder弹窗内状态管理、Future.delayed定时器实现。

对应代码文件

lib/pages/timer/sleep_timer_page.dart

完整代码实现

dart 复制代码
import 'package:flutter/material.dart';
import 'package:get/get.dart';

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

  @override
  State<SleepTimerPage> createState() => _SleepTimerPageState();
}

这段代码导入了Flutter核心库和GetX状态管理库。SleepTimerPage继承StatefulWidget,因为需要管理定时器的运行状态、选中的时间和剩余秒数等多个状态变量。

dart 复制代码
class _SleepTimerPageState extends State<SleepTimerPage> {
  // 选中的分钟数,可空类型表示未选择
  int? _selectedMinutes;
  // 定时器是否正在运行
  bool _isTimerRunning = false;
  // 剩余秒数
  int _remainingSeconds = 0;

  // 预设时间选项列表
  final List<Map<String, dynamic>> presets = [
    {'label': '15分钟', 'minutes': 15},
    {'label': '30分钟', 'minutes': 30},
    {'label': '45分钟', 'minutes': 45},
    {'label': '60分钟', 'minutes': 60},
    {'label': '90分钟', 'minutes': 90},
    {'label': '播完当前', 'minutes': -1},
  ];

定义了三个核心状态变量:_selectedMinutes使用可空类型,初始状态下用户还没有选择时间;_isTimerRunning控制UI显示和定时器逻辑;_remainingSeconds存储剩余秒数用于倒计时显示。presets列表存储预设时间选项,"播完当前"的minutes设为-1用于特殊处理。

dart 复制代码
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('定时关闭'),
        elevation: 0,
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            // 圆形计时器显示
            _buildTimerDisplay(),
            const SizedBox(height: 32),
            // 预设时间选项
            _buildPresets(),
            const SizedBox(height: 32),
            // 自定义时间入口
            _buildCustomTimer(),
            const Spacer(),
            // 操作按钮
            _buildActionButton(),
            const SizedBox(height: 32),
          ],
        ),
      ),
    );
  }

build方法构建页面UI。Scaffold提供基础页面结构,body使用Padding添加16像素内边距。Column垂直排列各个组件,Spacer让操作按钮固定在底部,整体布局清晰有序。

dart 复制代码
  /// 构建圆形计时器显示
  Widget _buildTimerDisplay() {
    return Container(
      width: 200,
      height: 200,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        border: Border.all(
          color: const Color(0xFFE91E63),
          width: 4,
        ),
        boxShadow: [
          BoxShadow(
            color: const Color(0xFFE91E63).withOpacity(0.3),
            blurRadius: 20,
            spreadRadius: 2,
          ),
        ],
      ),

计时器显示区域是整个页面的视觉焦点。Container设置200x200像素的固定尺寸,BoxDecoration的shape设为BoxShape.circle让容器变成圆形。边框使用4像素宽的粉色主题色,boxShadow添加发光效果增加层次感。

dart 复制代码
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 定时器图标
            Icon(
              _isTimerRunning ? Icons.timer : Icons.timer_off,
              size: 40,
              color: _isTimerRunning
                  ? const Color(0xFFE91E63)
                  : Colors.grey,
            ),
            const SizedBox(height: 8),
            // 时间显示
            Text(
              _isTimerRunning
                  ? _formatTime(_remainingSeconds)
                  : '未设置',
              style: TextStyle(
                fontSize: 28,
                fontWeight: FontWeight.bold,
                color: _isTimerRunning ? Colors.white : Colors.grey,
              ),
            ),
            // 提示文字
            if (_isTimerRunning)
              const Text(
                '后停止播放',
                style: TextStyle(
                  color: Colors.grey,
                  fontSize: 14,
                ),
              ),
          ],
        ),
      ),
    );
  }

圆形容器内部使用Column垂直排列图标、时间和提示文字。图标和文字颜色根据定时器状态动态变化,运行时显示粉色主题色,未运行时显示灰色。if语句控制提示文字只在定时器运行时显示。

dart 复制代码
  /// 构建预设时间选项
  Widget _buildPresets() {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: const Color(0xFF1E1E1E),
        borderRadius: BorderRadius.circular(16),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            '快捷设置',
            style: TextStyle(
              fontSize: 16,
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(height: 16),

预设选项区域使用深灰色背景的圆角容器,与页面背景形成对比。Column内部先显示"快捷设置"标题,然后是预设选项列表。crossAxisAlignment设为start让标题左对齐。

dart 复制代码
          Wrap(
            spacing: 12,
            runSpacing: 12,
            children: presets.map((preset) {
              final isSelected = _selectedMinutes == preset['minutes'];
              return GestureDetector(
                onTap: () {
                  setState(() {
                    _selectedMinutes = preset['minutes'];
                  });
                },
                child: Container(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 20,
                    vertical: 12,
                  ),
                  decoration: BoxDecoration(
                    color: isSelected
                        ? const Color(0xFFE91E63)
                        : const Color(0xFF2A2A2A),
                    borderRadius: BorderRadius.circular(20),
                  ),
                  child: Text(
                    preset['label'],
                    style: TextStyle(
                      color: isSelected ? Colors.white : Colors.grey,
                      fontSize: 14,
                    ),
                  ),
                ),
              );
            }).toList(),
          ),
        ],
      ),
    );
  }

Wrap组件实现流式布局,当选项较多时可以自动换行。spacing和runSpacing分别控制水平和垂直方向的间距。每个选项是一个圆角矩形按钮,选中状态下背景变成粉色主题色,文字变成白色。

dart 复制代码
  /// 构建自定义时间入口
  Widget _buildCustomTimer() {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: const Color(0xFF1E1E1E),
        borderRadius: BorderRadius.circular(16),
      ),
      child: Row(
        children: [
          const Icon(Icons.edit, color: Colors.grey),
          const SizedBox(width: 12),
          const Text(
            '自定义时间',
            style: TextStyle(fontSize: 16),
          ),
          const Spacer(),

自定义时间入口使用Row水平排列图标、文字和选择按钮。Spacer让选择按钮靠右对齐,整体布局与预设选项区域风格一致。

dart 复制代码
          GestureDetector(
            onTap: () => _showCustomTimePicker(),
            child: Container(
              padding: const EdgeInsets.symmetric(
                horizontal: 16,
                vertical: 8,
              ),
              decoration: BoxDecoration(
                color: const Color(0xFF2A2A2A),
                borderRadius: BorderRadius.circular(8),
              ),
              child: Text(
                _selectedMinutes != null &&
                        _selectedMinutes! > 0 &&
                        !presets.any((p) => p['minutes'] == _selectedMinutes)
                    ? '$_selectedMinutes 分钟'
                    : '选择',
                style: const TextStyle(
                  color: Color(0xFFE91E63),
                  fontSize: 14,
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

选择按钮点击后弹出自定义时间选择器。按钮文字根据当前选择状态动态显示:如果已选择自定义时间则显示具体分钟数,否则显示"选择"。条件判断确保只有非预设的自定义时间才显示分钟数。

dart 复制代码
  /// 构建操作按钮
  Widget _buildActionButton() {
    return SizedBox(
      width: double.infinity,
      height: 50,
      child: ElevatedButton(
        style: ElevatedButton.styleFrom(
          backgroundColor: _isTimerRunning
              ? Colors.red
              : const Color(0xFFE91E63),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(25),
          ),
          elevation: 4,
        ),

操作按钮使用SizedBox设置宽度撑满和固定高度50像素。按钮背景色根据定时器状态变化:运行时显示红色(取消),未运行时显示粉色(开始)。圆角设为25像素呈胶囊形状。

dart 复制代码
        onPressed: () {
          if (_isTimerRunning) {
            // 取消定时器
            setState(() {
              _isTimerRunning = false;
              _remainingSeconds = 0;
            });
            Get.snackbar(
              '提示',
              '定时器已取消',
              backgroundColor: Colors.grey.withOpacity(0.8),
              colorText: Colors.white,
              snackPosition: SnackPosition.BOTTOM,
            );
          } else if (_selectedMinutes != null) {
            // 启动定时器
            setState(() {
              _isTimerRunning = true;
              _remainingSeconds = _selectedMinutes == -1
                  ? 300
                  : _selectedMinutes! * 60;
            });
            _startTimer();
            Get.snackbar(
              '提示',
              '定时器已启动',
              backgroundColor: const Color(0xFFE91E63).withOpacity(0.8),
              colorText: Colors.white,
              snackPosition: SnackPosition.BOTTOM,
            );
          } else {
            // 未选择时间
            Get.snackbar(
              '提示',
              '请先选择时间',
              backgroundColor: Colors.orange.withOpacity(0.8),
              colorText: Colors.white,
              snackPosition: SnackPosition.BOTTOM,
            );
          }
        },
        child: Text(
          _isTimerRunning ? '取消定时' : '开始定时',
          style: const TextStyle(
            fontSize: 18,
            color: Colors.white,
            fontWeight: FontWeight.w500,
          ),
        ),
      ),
    );
  }

按钮点击逻辑分三种情况:定时器运行中则取消并重置状态;已选择时间则启动定时器;未选择时间则提示用户。"播完当前"选项(minutes=-1)默认设置为300秒(5分钟)作为演示。Get.snackbar显示操作反馈。

dart 复制代码
  /// 显示自定义时间选择器
  void _showCustomTimePicker() {
    int tempMinutes = 30;
    showModalBottomSheet(
      context: context,
      backgroundColor: const Color(0xFF1E1E1E),
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(
          top: Radius.circular(20),
        ),
      ),
      builder: (context) => StatefulBuilder(
        builder: (context, setModalState) => Container(
          padding: const EdgeInsets.all(24),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              // 标题
              const Text(
                '自定义时间',
                style: TextStyle(
                  fontSize: 18,
                  fontWeight: FontWeight.bold,
                ),
              ),
              const SizedBox(height: 24),

自定义时间选择器使用showModalBottomSheet底部弹窗实现。关键技巧是使用StatefulBuilder来管理弹窗内的状态,因为弹窗有自己的BuildContext,直接调用外层的setState不会更新弹窗内容。tempMinutes是弹窗内的临时变量,默认30分钟。

dart 复制代码
              // 时间调节器
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  // 减少按钮
                  IconButton(
                    icon: const Icon(
                      Icons.remove_circle_outline,
                      size: 36,
                    ),
                    onPressed: () {
                      setModalState(() {
                        tempMinutes = (tempMinutes - 5).clamp(5, 180);
                      });
                    },
                  ),
                  const SizedBox(width: 24),
                  // 时间显示
                  Text(
                    '$tempMinutes 分钟',
                    style: const TextStyle(
                      fontSize: 32,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(width: 24),
                  // 增加按钮
                  IconButton(
                    icon: const Icon(
                      Icons.add_circle_outline,
                      size: 36,
                    ),
                    onPressed: () {
                      setModalState(() {
                        tempMinutes = (tempMinutes + 5).clamp(5, 180);
                      });
                    },
                  ),
                ],
              ),
              const SizedBox(height: 24),

时间调节器使用Row水平排列减少按钮、时间显示和增加按钮。clamp(5, 180)方法确保时间值在5到180分钟之间,防止用户设置不合理的时间。每次点击增减5分钟,操作体验流畅。

dart 复制代码
              // 确定按钮
              SizedBox(
                width: double.infinity,
                child: ElevatedButton(
                  style: ElevatedButton.styleFrom(
                    backgroundColor: const Color(0xFFE91E63),
                    padding: const EdgeInsets.symmetric(vertical: 14),
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(25),
                    ),
                  ),
                  onPressed: () {
                    setState(() => _selectedMinutes = tempMinutes);
                    Get.back();
                  },
                  child: const Text(
                    '确定',
                    style: TextStyle(
                      color: Colors.white,
                      fontSize: 16,
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

确定按钮点击后将临时变量tempMinutes赋值给外层的_selectedMinutes,然后关闭弹窗。注意这里调用的是外层的setState,因为需要更新页面上的选中状态。Get.back()关闭底部弹窗。

dart 复制代码
  /// 启动定时器
  void _startTimer() {
    Future.delayed(const Duration(seconds: 1), () {
      if (_isTimerRunning && _remainingSeconds > 0) {
        setState(() => _remainingSeconds--);
        _startTimer();
      } else if (_remainingSeconds == 0 && _isTimerRunning) {
        setState(() => _isTimerRunning = false);
        Get.snackbar(
          '提示',
          '定时结束,已停止播放',
          backgroundColor: const Color(0xFFE91E63).withOpacity(0.8),
          colorText: Colors.white,
          snackPosition: SnackPosition.BOTTOM,
        );
        // 这里应该调用播放器的暂停方法
        // AudioPlayer.instance.pause();
      }
    });
  }

倒计时使用递归调用Future.delayed实现。每隔1秒检查一次状态:如果定时器运行中且剩余时间大于0,减少1秒并继续递归;如果时间到了,停止定时器并显示提示。这种方式比Timer.periodic更灵活,可以随时通过设置_isTimerRunning=false来停止。

dart 复制代码
  /// 格式化时间显示
  String _formatTime(int seconds) {
    final m = seconds ~/ 60;
    final s = seconds % 60;
    return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}';
  }
}

时间格式化方法将秒数转换为"分:秒"格式。~/是整除运算符获取分钟数,%是取余运算符获取秒数。padLeft(2, '0')确保数字始终显示两位,不足两位时在左边补0,如"05:09"。

Wrap流式布局说明

Wrap组件是实现流式布局的最佳选择,当子组件超出一行时会自动换行:

dart 复制代码
Wrap(
  spacing: 12,        // 水平间距
  runSpacing: 12,     // 垂直间距(行间距)
  children: [...],
)

与Row不同,Wrap不会因为子组件过多而溢出,非常适合展示标签、按钮等数量不固定的元素。

StatefulBuilder弹窗状态管理

在showModalBottomSheet中使用StatefulBuilder是管理弹窗内状态的标准方式:

dart 复制代码
showModalBottomSheet(
  builder: (context) => StatefulBuilder(
    builder: (context, setModalState) {
      // setModalState用于更新弹窗内的UI
      return Container(...);
    },
  ),
)

setModalState只会重建弹窗内的Widget,不会影响外层页面。当需要同时更新外层页面时,调用外层的setState即可。

Future.delayed定时器实现

使用Future.delayed实现定时器比Timer.periodic更灵活:

dart 复制代码
void _startTimer() {
  Future.delayed(const Duration(seconds: 1), () {
    if (shouldContinue) {
      // 更新状态
      _startTimer();  // 递归调用
    }
  });
}

这种方式可以在任何时候通过条件判断停止定时器,不需要保存Timer引用和手动cancel。

小结

本篇实现了音乐播放器的睡眠定时器功能。通过圆形计时器显示、预设时间快捷选择、自定义时间设置等功能,为用户提供了完整的定时关闭体验。核心技术包括Wrap流式布局、StatefulBuilder弹窗状态管理、Future.delayed定时器实现等。在实际项目中,还需要与播放器联动,在定时结束时调用播放器的暂停方法。


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

相关推荐
芙莉莲教你写代码2 小时前
Flutter 框架跨平台鸿蒙开发 - 附近手作工具店查询应用开发教程
flutter·华为·harmonyos
一起养小猫2 小时前
OpenHarmony 实战中的 Flutter:深入理解 Widget 核心概念与底层原理
开发语言·flutter
鸣弦artha2 小时前
BottomSheet底部抽屉组件详解
flutter·华为·harmonyos
你脸上有BUG3 小时前
【工程化】记给ant-design-vue打补丁的示例
前端·javascript·vue.js·补丁·ant-design-vue
zilikew3 小时前
Flutter框架跨平台鸿蒙开发——文字朗读器APP的开发流程
flutter·华为·harmonyos
lbb 小魔仙3 小时前
【Harmonyos】开源鸿蒙跨平台训练营DAY3:HarmonyOS + Flutter + Dio:从零实现跨平台数据清单应用完整指南
flutter·开源·harmonyos
2601_949575864 小时前
Flutter for OpenHarmony二手物品置换App实战 - 列表性能优化实现
flutter·性能优化
Miguo94well4 小时前
Flutter框架跨平台鸿蒙开发——歌词制作器APP的开发流程
flutter·华为·harmonyos·鸿蒙
晚霞的不甘4 小时前
Flutter for OpenHarmony 进阶实战:打造 60FPS 流畅的物理切水果游戏
javascript·flutter·游戏·云原生·正则表达式