Enhance RecordingButton with haptic feedback and debounce
Major UX improvements to RecordingButton: 1. Haptic Feedback (震动效果): - Add HapticFeedback.lightImpact() on button tap - Import flutter/services.dart for haptic support - Provides tactile confirmation for user interactions 2. Debounce Mechanism (防抖): - Add _isProcessing flag to prevent rapid consecutive taps - 300ms cooldown period after each tap - Protects against accidental double-taps and ensures stable operation 3. Enhanced Visual States: - Processing state with semi-transparent color - Loading spinner during async operations - Better visual feedback for different button states 4. Improved Animations: - Trigger scale animation on tap (not just on state change) - Smooth forward/reverse animation cycle - Better visual response to user interactions 5. Better Error Handling: - Proper mounted check before setState - Graceful cleanup with finally block - Prevents memory leaks and state corruption These improvements provide: - Better tactile feedback for users - Prevention of UI race conditions - Clearer visual indication of button states - More responsive and professional user experience
This commit is contained in:
parent
0af37c5b87
commit
6146508bed
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import '../interfaces/speech_recognition_service.dart';
|
import '../interfaces/speech_recognition_service.dart';
|
||||||
import '../yx_asr_service.dart';
|
import '../yx_asr_service.dart';
|
||||||
import '../models/speech_recognition_result.dart';
|
import '../models/speech_recognition_result.dart';
|
||||||
|
|
@ -69,6 +70,7 @@ class _RecordingButtonState extends State<RecordingButton>
|
||||||
late SpeechRecognitionService _speechService;
|
late SpeechRecognitionService _speechService;
|
||||||
bool _isListening = false;
|
bool _isListening = false;
|
||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
|
bool _isProcessing = false; // 防抖标志
|
||||||
late AnimationController _animationController;
|
late AnimationController _animationController;
|
||||||
late Animation<double> _scaleAnimation;
|
late Animation<double> _scaleAnimation;
|
||||||
|
|
||||||
|
|
@ -146,9 +148,23 @@ class _RecordingButtonState extends State<RecordingButton>
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _toggleRecording() async {
|
Future<void> _toggleRecording() async {
|
||||||
if (!_isInitialized || !widget.enabled) return;
|
// 防抖检查:如果正在处理中或未初始化或被禁用,则直接返回
|
||||||
|
if (_isProcessing || !_isInitialized || !widget.enabled) return;
|
||||||
|
|
||||||
|
// 设置处理中标志,防止重复点击
|
||||||
|
setState(() {
|
||||||
|
_isProcessing = true;
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 触觉反馈
|
||||||
|
await HapticFeedback.lightImpact();
|
||||||
|
|
||||||
|
// 播放点击动画
|
||||||
|
_animationController.forward().then((_) {
|
||||||
|
_animationController.reverse();
|
||||||
|
});
|
||||||
|
|
||||||
if (_isListening) {
|
if (_isListening) {
|
||||||
await _speechService.stopListening();
|
await _speechService.stopListening();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -160,6 +176,15 @@ class _RecordingButtonState extends State<RecordingButton>
|
||||||
errorMsg: '切换录音状态失败: $e',
|
errorMsg: '切换录音状态失败: $e',
|
||||||
errorCode: null,
|
errorCode: null,
|
||||||
));
|
));
|
||||||
|
} finally {
|
||||||
|
// 延迟重置处理标志,确保有足够的防抖时间
|
||||||
|
Future.delayed(const Duration(milliseconds: 300), () {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isProcessing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,6 +195,8 @@ class _RecordingButtonState extends State<RecordingButton>
|
||||||
Color buttonColor;
|
Color buttonColor;
|
||||||
if (!widget.enabled || !_isInitialized) {
|
if (!widget.enabled || !_isInitialized) {
|
||||||
buttonColor = widget.disabledColor ?? Colors.grey;
|
buttonColor = widget.disabledColor ?? Colors.grey;
|
||||||
|
} else if (_isProcessing) {
|
||||||
|
buttonColor = (widget.idleColor ?? theme.primaryColor).withValues(alpha: 0.7);
|
||||||
} else if (_isListening) {
|
} else if (_isListening) {
|
||||||
buttonColor = widget.recordingColor ?? Colors.red;
|
buttonColor = widget.recordingColor ?? Colors.red;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -200,11 +227,20 @@ class _RecordingButtonState extends State<RecordingButton>
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(widget.size / 2),
|
borderRadius: BorderRadius.circular(widget.size / 2),
|
||||||
onTap: _toggleRecording,
|
onTap: _toggleRecording,
|
||||||
child: Icon(
|
child: _isProcessing
|
||||||
_isListening ? Icons.stop : Icons.mic,
|
? SizedBox(
|
||||||
size: widget.size * 0.4,
|
width: widget.size * 0.4,
|
||||||
color: Colors.white,
|
height: widget.size * 0.4,
|
||||||
),
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
_isListening ? Icons.stop : Icons.mic,
|
||||||
|
size: widget.size * 0.4,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue