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:
Max 2025-09-09 14:06:43 +08:00
parent 0af37c5b87
commit 6146508bed
1 changed files with 42 additions and 6 deletions

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../interfaces/speech_recognition_service.dart';
import '../yx_asr_service.dart';
import '../models/speech_recognition_result.dart';
@ -69,6 +70,7 @@ class _RecordingButtonState extends State<RecordingButton>
late SpeechRecognitionService _speechService;
bool _isListening = false;
bool _isInitialized = false;
bool _isProcessing = false; //
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@ -146,9 +148,23 @@ class _RecordingButtonState extends State<RecordingButton>
}
Future<void> _toggleRecording() async {
if (!_isInitialized || !widget.enabled) return;
//
if (_isProcessing || !_isInitialized || !widget.enabled) return;
//
setState(() {
_isProcessing = true;
});
try {
//
await HapticFeedback.lightImpact();
//
_animationController.forward().then((_) {
_animationController.reverse();
});
if (_isListening) {
await _speechService.stopListening();
} else {
@ -160,6 +176,15 @@ class _RecordingButtonState extends State<RecordingButton>
errorMsg: '切换录音状态失败: $e',
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;
if (!widget.enabled || !_isInitialized) {
buttonColor = widget.disabledColor ?? Colors.grey;
} else if (_isProcessing) {
buttonColor = (widget.idleColor ?? theme.primaryColor).withValues(alpha: 0.7);
} else if (_isListening) {
buttonColor = widget.recordingColor ?? Colors.red;
} else {
@ -200,11 +227,20 @@ class _RecordingButtonState extends State<RecordingButton>
child: InkWell(
borderRadius: BorderRadius.circular(widget.size / 2),
onTap: _toggleRecording,
child: Icon(
_isListening ? Icons.stop : Icons.mic,
size: widget.size * 0.4,
color: Colors.white,
),
child: _isProcessing
? SizedBox(
width: widget.size * 0.4,
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,
),
),
),
),