SunHongxun'S Site
计算生物学 · 药物发现
← 从侧边栏选择分类开始阅读
🗄️
抽屉
孙红勋和王乐怡的专属版本
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
));
runApp(const MoleculeTimerApp());
}
class MoleculeTimerApp extends StatelessWidget {
const MoleculeTimerApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Molecule Timer',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF4A90E2),
surface: const Color(0xFFF5F7FA),
surfaceContainer: Colors.white,
),
scaffoldBackgroundColor: const Color(0xFFF5F7FA),
appBarTheme: const AppBarTheme(
backgroundColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
elevation: 0,
centerTitle: true,
titleTextStyle: TextStyle(
color: Color(0xFF2D3436),
fontSize: 20,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
fontFamily: 'Roboto',
),
),
),
home: const TimerHomePage(),
);
}
}
// ---------------- 数据模型 ----------------
class TimerModel {
String id;
int initialSeconds;
int remainingSeconds;
bool isPaused;
String label;
TimerModel({
required this.initialSeconds,
required this.remainingSeconds,
this.isPaused = false,
this.label = "实验步骤",
}) : id = DateTime.now().millisecondsSinceEpoch.toString() +
DateTime.now().microsecond.toString();
}
// ---------------- 主页面 ----------------
class TimerHomePage extends StatefulWidget {
const TimerHomePage({super.key});
@override
State<TimerHomePage> createState() => _TimerHomePageState();
}
class _TimerHomePageState extends State<TimerHomePage> with TickerProviderStateMixin {
final List<TimerModel> _timers = [];
final List<int> _presets = [10, 20, 30, 60, 120, 150];
Timer? _globalTicker;
// 状态控制
bool _isLoveExpanded = false; // 爱意卡片默认收缩
bool _isPanelVisible = true; // 底部面板默认可见
// 恋爱倒计时
late DateTime _loveStartDate;
late DateTime _loveEndDate;
late Duration _loveRemaining;
// 动画控制器
late AnimationController _heartController;
late Animation<double> _heartScaleAnimation;
@override
void initState() {
super.initState();
_presets.sort();
// 恋爱时间逻辑
_loveStartDate = DateTime(2022, 9, 17, 21, 00);
_loveEndDate = DateTime(_loveStartDate.year + 100, _loveStartDate.month, _loveStartDate.day, _loveStartDate.hour, _loveStartDate.minute);
_calculateLoveTime();
// 心跳动画
_heartController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1000)
)..repeat(reverse: true);
_heartScaleAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(
CurvedAnimation(parent: _heartController, curve: Curves.easeInOut)
);
// 全局计时器
_globalTicker = Timer.periodic(const Duration(seconds: 1), (timer) {
_calculateLoveTime();
bool needsSetState = false;
if (_isLoveExpanded) needsSetState = true;
if (_timers.isNotEmpty) {
for (var t in _timers) {
if (!t.isPaused && t.remainingSeconds > 0) {
t.remainingSeconds--;
needsSetState = true;
}
}
}
if (needsSetState) setState(() {});
});
}
void _calculateLoveTime() {
final now = DateTime.now();
if (now.isBefore(_loveEndDate)) {
_loveRemaining = _loveEndDate.difference(now);
} else {
_loveRemaining = Duration.zero;
}
}
@override
void dispose() {
_globalTicker?.cancel();
_heartController.dispose();
super.dispose();
}
// ---------------- 逻辑方法 ----------------
void _addTimer(int minutes) {
setState(() {
int seconds = minutes * 60;
_timers.add(TimerModel(
initialSeconds: seconds,
remainingSeconds: seconds,
label: _formatLabel(minutes),
));
});
}
String _formatLabel(int minutes) {
if (minutes >= 60) {
double hours = minutes / 60;
return "${hours % 1 == 0 ? hours.toInt() : hours.toStringAsFixed(1)} 小时";
}
return "$minutes 分钟";
}
void _addNewPreset(int minutes) {
if (!_presets.contains(minutes)) {
setState(() {
_presets.add(minutes);
_presets.sort();
});
}
}
void _removePreset(int minutes) {
setState(() {
_presets.remove(minutes);
});
}
Future<void> _showAddPresetDialog() async {
final TextEditingController controller = TextEditingController();
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text("添加新时长"),
content: TextField(
controller: controller,
keyboardType: TextInputType.number,
autofocus: true,
decoration: const InputDecoration(hintText: "输入分钟数", suffixText: "分钟"),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("取消")),
TextButton(
onPressed: () {
final int? mins = int.tryParse(controller.text);
if (mins != null && mins > 0) {
_addNewPreset(mins);
Navigator.pop(context);
}
},
child: const Text("添加"),
),
],
);
},
);
}
void _togglePause(TimerModel timer) => setState(() => timer.isPaused = !timer.isPaused);
void _deleteTimer(TimerModel timer) => setState(() => _timers.remove(timer));
String _formatTime(int totalSeconds) {
int h = totalSeconds ~/ 3600;
int m = (totalSeconds % 3600) ~/ 60;
int s = totalSeconds % 60;
String minSec = "${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}";
return h > 0 ? "${h.toString().padLeft(2, '0')}:$minSec" : minSec;
}
// ---------------- UI 构建 ----------------
@override
Widget build(BuildContext context) {
// 获取屏幕底部高度,用于动画计算
final bottomPadding = MediaQuery.of(context).padding.bottom;
return Scaffold(
appBar: AppBar(
title: const Text('Molecule Timer'),
// 左上角心跳入口
leading: GestureDetector(
onTap: () => setState(() => _isLoveExpanded = !_isLoveExpanded),
child: ScaleTransition(
scale: _heartScaleAnimation,
child: Icon(
_isLoveExpanded ? Icons.favorite : Icons.favorite_border,
color: const Color(0xFFFF6B81),
size: 24,
),
),
),
actions: [
IconButton(
icon: const Icon(Icons.cleaning_services_outlined, color: Colors.grey),
onPressed: () => setState(() => _timers.clear()),
tooltip: "清空实验",
)
],
),
// 使用 Stack 实现层叠布局
body: Stack(
children: [
Column(
children: [
// 1. 顶部:爱意卡片 (AnimatedSize)
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: Container(
height: _isLoveExpanded ? null : 0,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ClipRect(child: _buildLoveCard()),
),
),
// 2. 中间:计时器列表 (占满剩余空间)
Expanded(
child: _timers.isEmpty
? _buildEmptyState()
: ListView.builder(
// 底部留出足够空间,防止被FAB遮挡
padding: const EdgeInsets.fromLTRB(16, 8, 16, 100),
itemCount: _timers.length,
itemBuilder: (context, index) {
return _buildTimerCard(_timers[index]);
},
),
),
],
),
// 3. 底部:完全可折叠的控制台 (AnimatedPositioned)
AnimatedPositioned(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOutCubic,
left: 0,
right: 0,
// 如果可见,距离底部0;如果不可见,移出屏幕外 (-300)
bottom: _isPanelVisible ? 0 : -300,
child: Container(
padding: EdgeInsets.fromLTRB(20, 20, 20, 30 + bottomPadding),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 30,
offset: const Offset(0, -5),
)
],
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题栏
Row(
children: [
const Icon(Icons.bolt_rounded, color: Colors.orangeAccent, size: 22),
const SizedBox(width: 8),
Text("快速预设",
style: TextStyle(
color: Colors.grey[800],
fontWeight: FontWeight.bold,
fontSize: 16
)
),
const Spacer(),
// 关闭按钮
IconButton(
onPressed: () => setState(() => _isPanelVisible = false),
icon: const Icon(Icons.keyboard_arrow_down_rounded, color: Colors.grey),
)
],
),
const SizedBox(height: 10),
// 预设按钮区域
Wrap(
spacing: 12,
runSpacing: 12,
children: [
..._presets.map((mins) => _buildPresetChip(mins)),
_buildAddButton(),
],
),
],
),
),
),
// 4. 右下角:悬浮唤醒按钮 (FAB)
// 只有当面板隐藏时才显示
AnimatedPositioned(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
right: 20,
bottom: _isPanelVisible ? -100 : 30 + bottomPadding, // 面板显示时FAB藏到底部,面板隐藏时FAB浮上来
child: FloatingActionButton(
onPressed: () => setState(() => _isPanelVisible = true),
backgroundColor: Colors.white,
elevation: 4,
shape: const CircleBorder(),
child: const Icon(Icons.bolt_rounded, color: Colors.orangeAccent, size: 28),
),
),
],
),
);
}
// Love Card
Widget _buildLoveCard() {
int years = (_loveRemaining.inDays / 365).floor();
int days = _loveRemaining.inDays % 365;
int hours = _loveRemaining.inHours % 24;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.white, const Color(0xFFFFF0F3)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(24),
border: Border.all(color: const Color(0xFFFFE3E3), width: 1),
boxShadow: [
BoxShadow(
color: const Color(0xFFFFB8B8).withOpacity(0.15),
blurRadius: 15,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("shx", style: TextStyle(fontFamily: 'Courier', fontWeight: FontWeight.bold, color: Color(0xFF555555), fontSize: 18)),
const SizedBox(width: 8),
const Icon(Icons.favorite, color: Color(0xFFFF6B81), size: 20),
const SizedBox(width: 8),
const Text("wly", style: TextStyle(fontFamily: 'Courier', fontWeight: FontWeight.bold, color: Color(0xFF555555), fontSize: 18)),
],
),
const SizedBox(height: 8),
const Text("100-Year Promise Countdown", style: TextStyle(fontSize: 10, color: Colors.grey, letterSpacing: 1)),
const SizedBox(height: 4),
Text(
"$years 年 $days 天 $hours 小时",
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w300,
color: Color(0xFF2D3436),
fontFamily: 'Courier',
),
),
],
),
);
}
// Timer Card
Widget _buildTimerCard(TimerModel timer) {
final isFinished = timer.remainingSeconds == 0;
final progress = timer.initialSeconds == 0 ? 0.0 : timer.remainingSeconds / timer.initialSeconds;
Color statusColor = isFinished ? const Color(0xFFFF6B6B) : (timer.isPaused ? const Color(0xFFFF9F43) : const Color(0xFF4A90E2));
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: const Color(0xFF4A90E2).withOpacity(0.08),
blurRadius: 15,
offset: const Offset(0, 4),
),
],
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
child: Row(
children: [
Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 56, height: 56,
child: CircularProgressIndicator(
value: isFinished ? 1 : progress,
strokeWidth: 5,
backgroundColor: Colors.grey[100],
color: statusColor,
strokeCap: StrokeCap.round,
),
),
if (timer.isPaused && !isFinished)
const Icon(Icons.pause, size: 24, color: Color(0xFFFF9F43))
else if (isFinished)
const Icon(Icons.check, size: 24, color: Color(0xFFFF6B6B)),
],
),
const SizedBox(width: 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_formatTime(timer.remainingSeconds),
style: TextStyle(
fontSize: 36, fontWeight: FontWeight.w700, fontFamily: 'Courier',
color: isFinished ? const Color(0xFFFF6B6B) : const Color(0xFF2D3436),
letterSpacing: -1,
),
),
Text(
isFinished ? "Time's up!" : "时长: ${timer.label}",
style: TextStyle(color: Colors.grey[500], fontSize: 13, fontWeight: FontWeight.w500),
),
],
),
),
if (!isFinished)
IconButton(
onPressed: () => _togglePause(timer),
icon: Icon(timer.isPaused ? Icons.play_arrow_rounded : Icons.pause_rounded, size: 32, color: timer.isPaused ? const Color(0xFF4A90E2) : Colors.grey[400]),
),
IconButton(
onPressed: () => _deleteTimer(timer),
icon: Icon(Icons.close_rounded, size: 24, color: Colors.grey[300]),
),
],
),
),
);
}
// Chips
Widget _buildPresetChip(int minutes) {
String label = minutes >= 60 ? "${(minutes / 60).toStringAsFixed(1).replaceAll('.0', '')}h" : "${minutes}m";
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _addTimer(minutes),
onLongPress: () {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text("删除预设"),
content: Text("确定要删除 $label 吗?"),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("取消")),
TextButton(
onPressed: () { _removePreset(minutes); Navigator.pop(ctx); },
child: const Text("删除", style: TextStyle(color: Colors.red))
),
],
)
);
},
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
decoration: BoxDecoration(color: const Color(0xFFF0F4F8), borderRadius: BorderRadius.circular(16)),
child: Text(label, style: const TextStyle(color: Color(0xFF4A90E2), fontWeight: FontWeight.w600, fontSize: 15)),
),
),
);
}
Widget _buildAddButton() {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: _showAddPresetDialog,
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFF4A90E2).withOpacity(0.3), width: 1.5),
borderRadius: BorderRadius.circular(16),
),
child: const Icon(Icons.add, size: 20, color: Color(0xFF4A90E2)),
),
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.science_outlined, size: 60, color: Colors.grey[200]),
const SizedBox(height: 16),
Text("Molecule Timer", style: TextStyle(color: Colors.grey[300], fontSize: 16, fontWeight: FontWeight.bold)),
],
),
);
}
}
💬 留言