From 00e5316b525f189c10f6a2eab722cbe615392290 Mon Sep 17 00:00:00 2001 From: daodaoshi Date: Thu, 19 Dec 2024 00:44:34 +0800 Subject: [PATCH] =?UTF-8?q?=E6=99=83=E5=8A=A8=E5=BE=85=E4=BC=98=E5=8C=96?= =?UTF-8?q?=20=E9=94=AE=E7=9B=98=E5=B0=8F=E6=95=B0=E7=82=B9=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E5=BE=85=E4=BC=98=E5=8C=96=20=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E6=98=BE=E7=A4=BAok?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/build.gradle | 2 +- android/app/src/main/AndroidManifest.xml | 5 +- lib/pages/home_page.dart | 109 ++++++-- lib/pages/record_page.dart | 341 ++++++++++++++++++----- lib/utils/date_formatter.dart | 49 ++++ lib/widgets/record_list.dart | 44 +-- pubspec.lock | 2 +- 7 files changed, 440 insertions(+), 112 deletions(-) create mode 100644 lib/utils/date_formatter.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 7310c1d..747d859 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -46,7 +46,7 @@ android { // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion flutter.minSdkVersion - targetSdkVersion flutter.targetSdkVersion + targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 22a2868..7b046cc 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,9 +1,10 @@ + android:icon="@mipmap/ic_launcher" + android:enableOnBackInvokedCallback="true"> { StreamSubscription? _accelerometerSubscription; DateTime? _lastShakeTime; - int _shakeCount = 0; + List _recentXValues = []; // 存储原始x轴加速度值(不取绝对值) bool _isNavigating = false; final _dbService = DatabaseService(); List records = []; + // 定义摇晃检测的常量 + static const double _shakeThreshold = 8.0; // 单次摇晃的加速度阈值 + static const double _directionThreshold = 2.0; // 方向改变的最小加速度 + static const int _sampleSize = 3; // 采样数量 + static const Duration _shakeWindow = Duration(milliseconds: 800); + static const Duration _cooldown = Duration(milliseconds: 50); + + /// 检测有效的摇晃 + bool _isValidShake() { + if (_recentXValues.length < _sampleSize) return false; + + // 检查是否有足够大的加速度 + bool hasStrongShake = _recentXValues.any((x) => x.abs() > _shakeThreshold); + if (!hasStrongShake) return false; + + // 检查方向变化 + bool hasDirectionChange = false; + for (int i = 1; i < _recentXValues.length; i++) { + // 如果相邻两个值的符号相反,且都超过方向阈值,说明发生了方向改变 + if (_recentXValues[i].abs() > _directionThreshold && + _recentXValues[i - 1].abs() > _directionThreshold && + _recentXValues[i].sign != _recentXValues[i - 1].sign) { + hasDirectionChange = true; + break; + } + } + + return hasDirectionChange; + } + @override void initState() { super.initState(); @@ -30,6 +60,17 @@ class _HomePageState extends State { _loadRecords(); } + /// 重置摇晃检测状态 + void _resetShakeDetection() { + _lastShakeTime = null; + _recentXValues.clear(); + _isNavigating = false; + + // 重新初始化传感器监听 + _accelerometerSubscription?.cancel(); + _initShakeDetection(); + } + /// 加载记录 Future _loadRecords() async { final loadedRecords = await _dbService.getAllRecords(); @@ -41,30 +82,48 @@ class _HomePageState extends State { /// 初始化摇晃检测 void _initShakeDetection() { - _accelerometerSubscription = accelerometerEvents.listen((event) { - // 主要检测左右摇晃(x轴) - if (event.x.abs() > 25) { + _accelerometerSubscription = accelerometerEvents.listen( + (event) { + if (_isNavigating) return; // 如果正在导航,忽略摇晃检测 + final now = DateTime.now(); - if (_lastShakeTime == null) { - _lastShakeTime = now; - _shakeCount = 1; - } else { - // 缩短有效时间窗口,要求更快的摇晃 - if (now.difference(_lastShakeTime!) < const Duration(milliseconds: 500)) { - _shakeCount++; - if (_shakeCount >= 2) { - _navigateToRecordPageWithVibration(); - _shakeCount = 0; - _lastShakeTime = null; - } - } else { - // 重置计数 - _shakeCount = 1; - } - _lastShakeTime = now; + + // 添加新的x轴值(保留正负号) + _recentXValues.add(event.x); + if (_recentXValues.length > _sampleSize) { + _recentXValues.removeAt(0); } - } - }); + + // 检查是否处于冷却期 + if (_lastShakeTime != null && + now.difference(_lastShakeTime!) < _cooldown) { + return; + } + + // 检测有效的摇晃 + if (_isValidShake()) { + if (_lastShakeTime == null) { + _lastShakeTime = now; + _recentXValues.clear(); + } else { + final timeDiff = now.difference(_lastShakeTime!); + if (timeDiff < _shakeWindow) { + _navigateToRecordPageWithVibration(); + _lastShakeTime = null; + _recentXValues.clear(); + } else { + _lastShakeTime = now; + _recentXValues.clear(); + } + } + } + }, + onError: (error) { + debugPrint('Accelerometer error: $error'); + _resetShakeDetection(); // 发生错误时重置检测 + }, + cancelOnError: false, // 错误时不取消订阅,而是重置 + ); } /// 带震动的记账页面导航 @@ -82,6 +141,7 @@ class _HomePageState extends State { /// 导航到记账页面 void _navigateToRecordPage([Record? record]) { + _isNavigating = true; // 设置导航标志 Navigator.push( context, MaterialPageRoute( @@ -99,7 +159,8 @@ class _HomePageState extends State { maintainState: false, ), ).then((_) { - _isNavigating = false; + // 返回时重置检测状态 + _resetShakeDetection(); }); } diff --git a/lib/pages/record_page.dart b/lib/pages/record_page.dart index 53b06ab..73580c4 100644 --- a/lib/pages/record_page.dart +++ b/lib/pages/record_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import '../models/record.dart'; import '../data/categories.dart'; import 'package:uuid/uuid.dart'; @@ -24,30 +25,43 @@ class _RecordPageState extends State with SingleTickerProviderStateM double _amount = 0.0; DateTime _selectedDate = DateTime.now(); final _uuid = const Uuid(); + String _inputBuffer = '0.0'; // 用于显示的字符串 + String? _pendingOperation; // 等待执行的运算符 + double? _pendingValue; // 等待计算的第一个值 + bool _isNewInput = true; // 是否开始新的输入 @override void initState() { super.initState(); - // 如果是编辑模式,初始化现有记录的数据 - if (widget.record != null) { - _selectedCategoryId = widget.record!.categoryId; - _note = widget.record!.note ?? ''; - _amount = widget.record!.amount; - _selectedDate = widget.record!.createTime; - } - + // 先初始化 TabController _tabController = TabController( length: 2, vsync: this, initialIndex: widget.record?.type == RecordType.income ? 1 : 0, ); _tabController.addListener(_onTabChanged); + + // 初始化数据 + if (widget.record != null) { + // 编辑模式:初始化现有记录的数据 + _selectedCategoryId = widget.record!.categoryId; + _note = widget.record!.note ?? ''; + _amount = widget.record!.amount; + _selectedDate = widget.record!.createTime; + // 设置输入缓冲区为当前金额的格式化字符串 + _inputBuffer = _formatNumberForDisplay(widget.record!.amount); + _isNewInput = false; + } else { + // 新建模式:默认选中第一个分类 + _selectedCategoryId = _currentCategories.first.id; + } } /// 标签切换监听 void _onTabChanged() { setState(() { - _selectedCategoryId = null; // 切换类型时重置选中的分类 + // 切换类型时选中新类型的第一个分类 + _selectedCategoryId = _currentCategories.first.id; }); } @@ -87,6 +101,234 @@ class _RecordPageState extends State with SingleTickerProviderStateM Navigator.of(context).pop(); } + /// 格式化显示金额 + String get _displayAmount { + if (_pendingOperation != null && _pendingValue != null) { + return '$_pendingValue $_pendingOperation ${_isNewInput ? "" : _inputBuffer}'; + } + return _inputBuffer; + } + + /// 处理数字输入 + void _handleNumber(String digit) { + setState(() { + if (_isNewInput) { + // 新输入时直接替换 + _inputBuffer = digit; + _isNewInput = false; + } else if (_inputBuffer == '0' && !_inputBuffer.contains('.')) { + // 当前是0且没有小数点时,直接替换0 + _inputBuffer = digit; + } else if (_inputBuffer == '0.0') { + // 如果是默认状态,直接替换为新数字 + _inputBuffer = digit; + _isNewInput = false; + } else { + // 其他情况追加数字 + _inputBuffer = _inputBuffer + digit; + } + _amount = double.parse(_inputBuffer); + }); + } + + /// 处理小数点 + void _handleDecimal() { + setState(() { + if (!_inputBuffer.contains('.')) { + // 如果是默认状态(0.0)或只有0,直接显示0. + if (_inputBuffer == '0.0' || _inputBuffer == '0') { + _inputBuffer = '0.'; + _isNewInput = false; + } else { + // 其他数字直接加小数点 + _inputBuffer = '$_inputBuffer.'; + } + } else { + // 已有小数点时震动反馈 + HapticFeedback.heavyImpact(); + } + }); + } + + /// 处理删除 + void _handleDelete() { + setState(() { + if (_pendingOperation != null && !_isNewInput) { + // 删除第二个数字 + if (_inputBuffer.length > 1) { + _inputBuffer = _inputBuffer.substring(0, _inputBuffer.length - 1); + if (_inputBuffer.isEmpty || _inputBuffer == '0') { + _inputBuffer = '0.0'; + _isNewInput = true; + } + } else { + _inputBuffer = '0.0'; + _isNewInput = true; + } + } else if (_pendingOperation != null) { + // 删除运算符 + _inputBuffer = _pendingValue.toString(); + _pendingOperation = null; + _pendingValue = null; + _isNewInput = false; + } else { + // 删除普通数字 + if (_inputBuffer.length > 1) { + _inputBuffer = _inputBuffer.substring(0, _inputBuffer.length - 1); + if (_inputBuffer.isEmpty || _inputBuffer == '0') { + _inputBuffer = '0.0'; + } + } else { + _inputBuffer = '0.0'; + } + } + _amount = double.parse(_inputBuffer); + }); + } + + /// 处理运算符 + void _handleOperator(String operator) { + // 如果有待处理的运算,先计算结果 + if (_pendingOperation != null && !_isNewInput) { + // 先保存当前运算符,因为计算可能会重置状态 + final newOperator = operator; + _calculateResult(); + + // 如果计算导致重置(结果为0),不添加新的运算符 + if (_inputBuffer == '0.0') { + return; + } + + // 计算完成后,设置新的运算符 + setState(() { + _pendingOperation = newOperator; + _pendingValue = double.parse(_inputBuffer); + _isNewInput = true; + }); + } else { + // 正常设置运算符 + setState(() { + _pendingOperation = operator; + _pendingValue = double.parse(_inputBuffer); + _isNewInput = true; + }); + } + } + + /// 计算结果 + void _calculateResult() { + if (_pendingOperation != null && _pendingValue != null && !_isNewInput) { + final currentValue = double.parse(_inputBuffer); + double result; + + switch (_pendingOperation) { + case '+': + result = _pendingValue! + currentValue; + if (result == 0) { + _resetToDefault(); + return; + } + break; + case '-': + result = _pendingValue! - currentValue; + if (result <= 0) { + _resetToDefault(); + return; + } + break; + default: + return; + } + + setState(() { + // 格式化结果,去掉不必要的小数位 + _inputBuffer = _formatNumberForDisplay(result); + _amount = result; + _pendingOperation = null; + _pendingValue = null; + _isNewInput = true; + }); + } + } + + /// 重置到默认状态 + void _resetToDefault() { + setState(() { + _inputBuffer = '0.0'; + _amount = 0; + _pendingOperation = null; + _pendingValue = null; + _isNewInput = true; + }); + } + + /// 格式化数字用于显示 + String _formatNumberForDisplay(double number) { + // 如果是整数,直接返回整数部分 + if (number % 1 == 0) { + return number.toInt().toString(); + } + // 如果是小数,去掉末尾的0 + String numStr = number.toString(); + while (numStr.endsWith('0')) { + numStr = numStr.substring(0, numStr.length - 1); + } + if (numStr.endsWith('.')) { + numStr = numStr.substring(0, numStr.length - 1); + } + return numStr; + } + + /// 修改键盘按钮处理逻辑 + void _onKeyboardButtonPressed(String value) { + switch (value) { + case '删除': + _handleDelete(); + break; + case '保存': + _calculateResult(); + _saveRecord(); + break; + case 'again': + // TODO: 实现again功能 + break; + case '+': + case '-': + _handleOperator(value); + break; + case '.': + _handleDecimal(); + break; + default: + _handleNumber(value); + } + } + + /// 格式化日期显示 + String _formatDate(DateTime date) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final dateToCheck = DateTime(date.year, date.month, date.day); + final difference = today.difference(dateToCheck).inDays; + + // 检查是否是今天、昨天、前天 + switch (difference) { + case 0: + return '今天'; + case 1: + return '昨天'; + case 2: + return '前天'; + default: + // 如果是当年 + if (date.year == now.year) { + return '${date.month}/${date.day}'; + } + // 不是当年 + return '${date.year}/${date.month}/${date.day}'; + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -175,26 +417,7 @@ class _RecordPageState extends State with SingleTickerProviderStateM mainAxisSize: MainAxisSize.min, children: [ // 备注和金额 - Row( - children: [ - Expanded( - child: TextField( - decoration: const InputDecoration( - hintText: '添加备注', - border: InputBorder.none, - ), - onChanged: (value) => setState(() => _note = value), - ), - ), - Text( - '¥${_amount.toStringAsFixed(2)}', - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - ], - ), + _buildNoteAndAmount(), // 配置按钮 Row( children: [ @@ -218,9 +441,7 @@ class _RecordPageState extends State with SingleTickerProviderStateM } }, icon: const Icon(Icons.calendar_today), - label: Text(_selectedDate == DateTime.now() - ? '今天' - : '${_selectedDate.month}月${_selectedDate.day}日'), + label: Text(_formatDate(_selectedDate)), ), TextButton.icon( onPressed: () { @@ -279,39 +500,29 @@ class _RecordPageState extends State with SingleTickerProviderStateM ); } - /// 处理键盘按钮点击 - void _onKeyboardButtonPressed(String value) { - switch (value) { - case '删除': - setState(() { - final amountStr = _amount.toStringAsFixed(2); - if (amountStr.length > 1) { - _amount = double.parse(amountStr.substring(0, amountStr.length - 1)); - } else { - _amount = 0; - } - }); - break; - case '保存': - _saveRecord(); - break; - case 'again': - // TODO: 实现again功能 - break; - case '+': - case '-': - // TODO: 实现加减功能 - break; - case '.': - // TODO: 实现小数点功能 - break; - default: - if (_amount == 0) { - setState(() => _amount = double.parse(value)); - } else { - setState(() => _amount = double.parse('$_amount$value')); - } - } + /// 构建底部编辑区域中的备注和金额行 + Widget _buildNoteAndAmount() { + return Row( + children: [ + Expanded( + child: TextField( + controller: TextEditingController(text: _note), // 使用控制器设置初始值 + decoration: const InputDecoration( + hintText: '添加备注', + border: InputBorder.none, + ), + onChanged: (value) => setState(() => _note = value), + ), + ), + Text( + _displayAmount, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ], + ); } @override diff --git a/lib/utils/date_formatter.dart b/lib/utils/date_formatter.dart new file mode 100644 index 0000000..a9573f3 --- /dev/null +++ b/lib/utils/date_formatter.dart @@ -0,0 +1,49 @@ +class DateFormatter { + /// 格式化日期显示 + static String formatDate(DateTime date) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final dateToCheck = DateTime(date.year, date.month, date.day); + final difference = today.difference(dateToCheck).inDays; + + // 格式化日期部分 + String dateStr; + if (date.year == now.year) { + // 当年日期显示 月-日 + dateStr = '${_padZero(date.month)}-${_padZero(date.day)}'; + } else { + // 非当年日期显示 年-月-日 + dateStr = '${date.year}-${_padZero(date.month)}-${_padZero(date.day)}'; + } + + // 获取提示文案 + String hint; + switch (difference) { + case 0: + hint = '今天'; + break; + case 1: + hint = '昨天'; + break; + case 2: + hint = '前天'; + break; + default: + // 获取星期几 + hint = _getWeekDay(date); + } + + return '$dateStr $hint'; + } + + /// 数字补零 + static String _padZero(int number) { + return number.toString().padLeft(2, '0'); + } + + /// 获取星期几 + static String _getWeekDay(DateTime date) { + final weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']; + return weekDays[date.weekday % 7]; + } +} \ No newline at end of file diff --git a/lib/widgets/record_list.dart b/lib/widgets/record_list.dart index 97f3984..485ce02 100644 --- a/lib/widgets/record_list.dart +++ b/lib/widgets/record_list.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../models/record.dart'; import '../data/categories.dart'; import './record_detail_dialog.dart'; +import '../utils/date_formatter.dart'; class RecordList extends StatelessWidget { final List records; @@ -22,23 +23,22 @@ class RecordList extends StatelessWidget { } /// 按日期分组记录 - Map> _groupByDate() { - final groups = >{}; + Map> _groupByDate() { + final groups = >{}; for (final record in records) { - final date = _formatDate(record.createTime); - if (!groups.containsKey(date)) { - groups[date] = []; + final dateKey = DateTime( + record.createTime.year, + record.createTime.month, + record.createTime.day, + ); + if (!groups.containsKey(dateKey)) { + groups[dateKey] = []; } - groups[date]!.add(record); + groups[dateKey]!.add(record); } return groups; } - /// 格式化日期 - String _formatDate(DateTime date) { - return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; - } - /// 计算日汇总 String _getDailySummary(List dayRecords) { double expense = 0; @@ -60,13 +60,15 @@ class RecordList extends StatelessWidget { @override Widget build(BuildContext context) { final groups = _groupByDate(); - final dates = groups.keys.toList()..sort((a, b) => b.compareTo(a)); + final dates = groups.keys.toList() + ..sort((a, b) => b.compareTo(a)); return ListView.builder( itemCount: dates.length, itemBuilder: (context, index) { - final date = dates[index]; - final dayRecords = groups[date]!; + final dateKey = dates[index]; + final dayRecords = groups[dateKey]!; + final formattedDate = DateFormatter.formatDate(dateKey); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -78,13 +80,17 @@ class RecordList extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - date, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, + formattedDate, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + Text( + _getDailySummary(dayRecords), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey[600], ), ), - Text(_getDailySummary(dayRecords)), ], ), ), diff --git a/pubspec.lock b/pubspec.lock index c0dd14b..277f82c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -359,4 +359,4 @@ packages: version: "1.1.2" sdks: dart: ">=3.2.5 <4.0.0" - flutter: ">=3.3.0" + flutter: ">=3.7.0"