Compare commits

..

4 Commits

Author SHA1 Message Date
Max aba8b44ab8 Update RecordingButton design with new visual style
Major design changes based on user requirements:

1. Background Style Changes:
   - Remove solid color background and shadows
   - Use semi-transparent background (alpha: 0.12) for subtle visual feedback
   - Maintain circular shape for both states

2. Icon Improvements:
   - Increase icon size to 55% of button size for better visibility
   - Use color-coded icons: blue for idle, red for recording
   - Remove white icon color, use theme-based colors instead

3. Loading Indicator Enhancement:
   - Show CircularProgressIndicator during both processing and listening states
   - Position indicator as overlay using Stack layout
   - Maintain white color for progress indicator for contrast

4. Color Logic Simplification:
   - Consolidate color logic into single iconColor variable
   - Remove complex state-based color transitions
   - Use consistent color scheme: blue (#2196F3) idle, red (#FF5252) recording
   - Darker disabled color (#212121) for better contrast

5. Layout Structure Update:
   - Replace Transform.scale animation wrapper with direct Container
   - Use Stack for layering icon and progress indicator
   - Positioned widgets for precise control over element placement

This creates a more modern, minimalist design with better visual hierarchy
and improved accessibility through higher contrast ratios.
2025-09-09 15:08:35 +08:00
Max ac234d99ec Fix RecordingButton shape: Both states should be circular
Correction based on user feedback:
- Both idle and recording states use circular shape (BoxShape.circle)
- Remove conditional rectangle shape logic
- Keep color differentiation: blue for idle, red for recording
- Maintain consistent circular InkWell ripple effect

Design now correctly matches the mockup:
🔵 Blue circle (idle/ready to record)
🔴 Red circle (actively recording)

Both buttons maintain the same circular shape with only color changes.
2025-09-09 14:23:47 +08:00
Max 3e8fe73e27 Update RecordingButton design to match UI mockup
Design improvements based on provided mockup:

1. Color Updates:
   - Idle state: Blue (#2196F3) - matches mockup's blue recording button
   - Recording state: Red (#FF5252) - matches mockup's red stop button
   - Disabled state: Light grey (#9E9E9E) for better visibility
   - Processing state: Semi-transparent versions of respective colors

2. Shape Changes:
   - Idle state: Circle (for recording button)
   - Recording state: Rounded rectangle (12px radius) for stop button
   - Smooth transitions between shapes using BoxShape

3. Icon Improvements:
   - Use rounded icons (mic_rounded, stop_rounded) for modern look
   - Adjust icon sizes: recording (40% of button), stop (35% of button)
   - Better proportions matching the design intent

4. Shadow Enhancements:
   - Increased blur radius for idle state (8→12px) for better depth
   - Dynamic shadow based on button state
   - Enhanced spread radius for better visual impact

5. Interactive Elements:
   - InkWell borderRadius adapts to button shape
   - Smooth transitions between circular and rectangular ripple effects

These changes create a modern, professional appearance that matches
common design patterns and provides clear visual feedback for different states.
2025-09-09 14:20:10 +08:00
Max 6146508bed 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
2025-09-09 14:06:43 +08:00
1 changed files with 76 additions and 35 deletions

View File

@ -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,13 +148,29 @@ 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 {
await _speechService.startListening(partialResults: widget.partialResults); await _speechService.startListening(
partialResults: widget.partialResults,
);
} }
} catch (e) { } catch (e) {
widget.onError?.call(SpeechRecognitionError( widget.onError?.call(SpeechRecognitionError(
@ -160,51 +178,74 @@ 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;
});
}
});
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); Color iconColor;
Color buttonColor;
if (!widget.enabled || !_isInitialized) { if (!widget.enabled || !_isInitialized) {
buttonColor = widget.disabledColor ?? Colors.grey; iconColor = widget.disabledColor ?? Colors.grey[850]!;
} else if (_isListening) {
buttonColor = widget.recordingColor ?? Colors.red;
} else { } else {
buttonColor = widget.idleColor ?? theme.primaryColor; iconColor = _isListening
? (widget.recordingColor ?? const Color(0xFFFF5252))
: (widget.idleColor ?? const Color(0xFF2196F3));
} }
Widget button = AnimatedBuilder( Widget button = AnimatedBuilder(
animation: _scaleAnimation, animation: _scaleAnimation,
builder: (context, child) { builder: (context, child) {
return Transform.scale( return Container(
scale: _scaleAnimation.value, width: widget.size,
child: Container( height: widget.size,
width: widget.size, decoration: BoxDecoration(
height: widget.size, color: iconColor.withValues(alpha: 0.12),
decoration: BoxDecoration( shape: BoxShape.circle,
color: buttonColor, ),
shape: BoxShape.circle, child: Material(
boxShadow: [ color: Colors.transparent,
BoxShadow( child: InkWell(
color: buttonColor.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(widget.size / 2),
blurRadius: _isListening ? 20 : 8, onTap: _toggleRecording,
spreadRadius: _isListening ? 5 : 2, child: Stack(
), children: [
], Positioned(
), left: 0,
child: Material( right: 0,
color: Colors.transparent, bottom: 0,
child: InkWell( top: 0,
borderRadius: BorderRadius.circular(widget.size / 2), child: Icon(
onTap: _toggleRecording, _isListening ? Icons.stop_rounded : Icons.mic_rounded,
child: Icon( size: widget.size * 0.55,
_isListening ? Icons.stop : Icons.mic, color: iconColor,
size: widget.size * 0.4, ),
color: Colors.white, ),
), if (_isProcessing || _isListening)
Positioned(
left: 0,
right: 0,
bottom: 0,
top: 0,
child: SizedBox(
width: widget.size,
height: widget.size,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(Colors.white),
),
),
)
],
), ),
), ),
), ),