晃动待优化 键盘小数点逻辑待优化 时间显示ok

This commit is contained in:
daodaoshi 2024-12-19 00:44:34 +08:00
parent 211fa195cb
commit 00e5316b52
7 changed files with 440 additions and 112 deletions

View File

@ -46,7 +46,7 @@ android {
// You can update the following values to match your application needs. // 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. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion targetSdkVersion 33
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }

View File

@ -1,9 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.VIBRATE"/> <uses-permission android:name="android.permission.VIBRATE"/>
<application <application
android:label="swing_account" android:label="摇晃记账"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher"
android:enableOnBackInvokedCallback="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"

View File

@ -18,11 +18,41 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> { class _HomePageState extends State<HomePage> {
StreamSubscription? _accelerometerSubscription; StreamSubscription? _accelerometerSubscription;
DateTime? _lastShakeTime; DateTime? _lastShakeTime;
int _shakeCount = 0; List<double> _recentXValues = []; // x轴加速度值
bool _isNavigating = false; bool _isNavigating = false;
final _dbService = DatabaseService(); final _dbService = DatabaseService();
List<Record> records = []; List<Record> 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 @override
void initState() { void initState() {
super.initState(); super.initState();
@ -30,6 +60,17 @@ class _HomePageState extends State<HomePage> {
_loadRecords(); _loadRecords();
} }
///
void _resetShakeDetection() {
_lastShakeTime = null;
_recentXValues.clear();
_isNavigating = false;
//
_accelerometerSubscription?.cancel();
_initShakeDetection();
}
/// ///
Future<void> _loadRecords() async { Future<void> _loadRecords() async {
final loadedRecords = await _dbService.getAllRecords(); final loadedRecords = await _dbService.getAllRecords();
@ -41,30 +82,48 @@ class _HomePageState extends State<HomePage> {
/// ///
void _initShakeDetection() { void _initShakeDetection() {
_accelerometerSubscription = accelerometerEvents.listen((event) { _accelerometerSubscription = accelerometerEvents.listen(
// x轴 (event) {
if (event.x.abs() > 25) { if (_isNavigating) return; //
final now = DateTime.now(); final now = DateTime.now();
if (_lastShakeTime == null) {
_lastShakeTime = now; // x轴值
_shakeCount = 1; _recentXValues.add(event.x);
} else { if (_recentXValues.length > _sampleSize) {
// _recentXValues.removeAt(0);
if (now.difference(_lastShakeTime!) < const Duration(milliseconds: 500)) {
_shakeCount++;
if (_shakeCount >= 2) {
_navigateToRecordPageWithVibration();
_shakeCount = 0;
_lastShakeTime = null;
}
} else {
//
_shakeCount = 1;
}
_lastShakeTime = now;
} }
}
}); //
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<HomePage> {
/// ///
void _navigateToRecordPage([Record? record]) { void _navigateToRecordPage([Record? record]) {
_isNavigating = true; //
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@ -99,7 +159,8 @@ class _HomePageState extends State<HomePage> {
maintainState: false, maintainState: false,
), ),
).then((_) { ).then((_) {
_isNavigating = false; //
_resetShakeDetection();
}); });
} }

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/record.dart'; import '../models/record.dart';
import '../data/categories.dart'; import '../data/categories.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -24,30 +25,43 @@ class _RecordPageState extends State<RecordPage> with SingleTickerProviderStateM
double _amount = 0.0; double _amount = 0.0;
DateTime _selectedDate = DateTime.now(); DateTime _selectedDate = DateTime.now();
final _uuid = const Uuid(); final _uuid = const Uuid();
String _inputBuffer = '0.0'; //
String? _pendingOperation; //
double? _pendingValue; //
bool _isNewInput = true; //
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// // TabController
if (widget.record != null) {
_selectedCategoryId = widget.record!.categoryId;
_note = widget.record!.note ?? '';
_amount = widget.record!.amount;
_selectedDate = widget.record!.createTime;
}
_tabController = TabController( _tabController = TabController(
length: 2, length: 2,
vsync: this, vsync: this,
initialIndex: widget.record?.type == RecordType.income ? 1 : 0, initialIndex: widget.record?.type == RecordType.income ? 1 : 0,
); );
_tabController.addListener(_onTabChanged); _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() { void _onTabChanged() {
setState(() { setState(() {
_selectedCategoryId = null; // //
_selectedCategoryId = _currentCategories.first.id;
}); });
} }
@ -87,6 +101,234 @@ class _RecordPageState extends State<RecordPage> with SingleTickerProviderStateM
Navigator.of(context).pop(); 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('.')) {
// 00
_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)00.
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -175,26 +417,7 @@ class _RecordPageState extends State<RecordPage> with SingleTickerProviderStateM
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// //
Row( _buildNoteAndAmount(),
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,
),
),
],
),
// //
Row( Row(
children: [ children: [
@ -218,9 +441,7 @@ class _RecordPageState extends State<RecordPage> with SingleTickerProviderStateM
} }
}, },
icon: const Icon(Icons.calendar_today), icon: const Icon(Icons.calendar_today),
label: Text(_selectedDate == DateTime.now() label: Text(_formatDate(_selectedDate)),
? '今天'
: '${_selectedDate.month}${_selectedDate.day}'),
), ),
TextButton.icon( TextButton.icon(
onPressed: () { onPressed: () {
@ -279,39 +500,29 @@ class _RecordPageState extends State<RecordPage> with SingleTickerProviderStateM
); );
} }
/// ///
void _onKeyboardButtonPressed(String value) { Widget _buildNoteAndAmount() {
switch (value) { return Row(
case '删除': children: [
setState(() { Expanded(
final amountStr = _amount.toStringAsFixed(2); child: TextField(
if (amountStr.length > 1) { controller: TextEditingController(text: _note), // 使
_amount = double.parse(amountStr.substring(0, amountStr.length - 1)); decoration: const InputDecoration(
} else { hintText: '添加备注',
_amount = 0; border: InputBorder.none,
} ),
}); onChanged: (value) => setState(() => _note = value),
break; ),
case '保存': ),
_saveRecord(); Text(
break; _displayAmount,
case 'again': style: const TextStyle(
// TODO: again功能 fontSize: 24,
break; fontWeight: FontWeight.bold,
case '+': ),
case '-': ),
// TODO: ],
break; );
case '.':
// TODO:
break;
default:
if (_amount == 0) {
setState(() => _amount = double.parse(value));
} else {
setState(() => _amount = double.parse('$_amount$value'));
}
}
} }
@override @override

View File

@ -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];
}
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import '../models/record.dart'; import '../models/record.dart';
import '../data/categories.dart'; import '../data/categories.dart';
import './record_detail_dialog.dart'; import './record_detail_dialog.dart';
import '../utils/date_formatter.dart';
class RecordList extends StatelessWidget { class RecordList extends StatelessWidget {
final List<Record> records; final List<Record> records;
@ -22,23 +23,22 @@ class RecordList extends StatelessWidget {
} }
/// ///
Map<String, List<Record>> _groupByDate() { Map<DateTime, List<Record>> _groupByDate() {
final groups = <String, List<Record>>{}; final groups = <DateTime, List<Record>>{};
for (final record in records) { for (final record in records) {
final date = _formatDate(record.createTime); final dateKey = DateTime(
if (!groups.containsKey(date)) { record.createTime.year,
groups[date] = []; record.createTime.month,
record.createTime.day,
);
if (!groups.containsKey(dateKey)) {
groups[dateKey] = [];
} }
groups[date]!.add(record); groups[dateKey]!.add(record);
} }
return groups; return groups;
} }
///
String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
/// ///
String _getDailySummary(List<Record> dayRecords) { String _getDailySummary(List<Record> dayRecords) {
double expense = 0; double expense = 0;
@ -60,13 +60,15 @@ class RecordList extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final groups = _groupByDate(); 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( return ListView.builder(
itemCount: dates.length, itemCount: dates.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final date = dates[index]; final dateKey = dates[index];
final dayRecords = groups[date]!; final dayRecords = groups[dateKey]!;
final formattedDate = DateFormatter.formatDate(dateKey);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -78,13 +80,17 @@ class RecordList extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
date, formattedDate,
style: const TextStyle( style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.w500,
fontSize: 16, ),
),
Text(
_getDailySummary(dayRecords),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600],
), ),
), ),
Text(_getDailySummary(dayRecords)),
], ],
), ),
), ),

View File

@ -359,4 +359,4 @@ packages:
version: "1.1.2" version: "1.1.2"
sdks: sdks:
dart: ">=3.2.5 <4.0.0" dart: ">=3.2.5 <4.0.0"
flutter: ">=3.3.0" flutter: ">=3.7.0"