
睡前听音乐是很多人的习惯,但如果音乐一直播放到天亮,不仅费电还可能影响睡眠质量。定时关闭功能对于音乐播放器来说是一个非常实用的功能。本篇将详细介绍如何实现一个完整的睡眠定时器页面。
功能分析
睡眠定时器页面需要实现以下功能:圆形计时器显示、预设时间快捷选择(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