logo

Flutter实战:从零实现微信式语音发送交互

作者:c4t2025.09.23 11:56浏览量:1

简介:本文深入解析Flutter中实现微信风格语音按钮和页面的完整方案,涵盖手势识别、音频录制、UI动画等核心功能,提供可复用的代码实现和优化建议。

一、需求分析与功能拆解

微信语音发送功能包含三个核心交互:

  1. 长按录音:手指按下触发录音,松开结束
  2. 滑动取消:向左滑动显示”松开手指取消发送”提示
  3. 视觉反馈:按钮按下动画、录音波纹效果、取消状态样式

技术实现需解决三个关键问题:

  • 精确的手势状态监听(按下/移动/松开)
  • 音频录制与停止的精准控制
  • 多状态UI的流畅过渡动画

二、核心组件实现

1. 自定义语音按钮组件

  1. class VoiceButton extends StatefulWidget {
  2. const VoiceButton({Key? key}) : super(key: key);
  3. @override
  4. State<VoiceButton> createState() => _VoiceButtonState();
  5. }
  6. class _VoiceButtonState extends State<VoiceButton> {
  7. bool _isRecording = false;
  8. bool _isCanceling = false;
  9. Offset? _startPosition;
  10. @override
  11. Widget build(BuildContext context) {
  12. return GestureDetector(
  13. onPanStart: (details) {
  14. _startPosition = details.localPosition;
  15. setState(() => _isRecording = true);
  16. _startRecording();
  17. },
  18. onPanUpdate: (details) {
  19. final dx = details.localPosition.dx - (_startPosition?.dx ?? 0);
  20. setState(() => _isCanceling = dx < -50); // 向左滑动50px触发取消
  21. },
  22. onPanEnd: (details) {
  23. if (_isRecording) {
  24. _stopRecording(!_isCanceling);
  25. setState(() {
  26. _isRecording = false;
  27. _isCanceling = false;
  28. });
  29. }
  30. },
  31. child: Container(
  32. width: 80,
  33. height: 80,
  34. decoration: BoxDecoration(
  35. shape: BoxShape.circle,
  36. color: _isRecording
  37. ? (_isCanceling ? Colors.red : Colors.green)
  38. : Colors.blue,
  39. boxShadow: [
  40. BoxShadow(
  41. color: Colors.black26,
  42. blurRadius: 5,
  43. offset: Offset(0, 2),
  44. )
  45. ],
  46. ),
  47. child: Center(
  48. child: Icon(
  49. _isRecording ? Icons.mic : Icons.mic_none,
  50. color: Colors.white,
  51. size: 32,
  52. ),
  53. ),
  54. ),
  55. );
  56. }
  57. void _startRecording() {
  58. // 实际项目中调用录音插件
  59. debugPrint('开始录音...');
  60. }
  61. void _stopRecording(bool shouldSend) {
  62. // 实际项目中停止录音并处理结果
  63. debugPrint(shouldSend ? '发送语音' : '取消发送');
  64. }
  65. }

2. 录音状态管理

推荐使用providerriverpod进行状态管理:

  1. class VoiceRecorderProvider with ChangeNotifier {
  2. RecordingStatus _status = RecordingStatus.idle;
  3. double _volume = 0.0;
  4. RecordingStatus get status => _status;
  5. double get volume => _volume;
  6. Future<void> startRecording() async {
  7. _status = RecordingStatus.recording;
  8. notifyListeners();
  9. // 模拟音量变化
  10. for (int i = 0; i < 10; i++) {
  11. await Future.delayed(Duration(milliseconds: 300));
  12. _volume = Random().nextDouble() * 0.8 + 0.2;
  13. notifyListeners();
  14. }
  15. }
  16. void stopRecording(bool shouldSend) {
  17. _status = shouldSend
  18. ? RecordingStatus.finished
  19. : RecordingStatus.cancelled;
  20. notifyListeners();
  21. }
  22. }
  23. enum RecordingStatus { idle, recording, finished, cancelled }

3. 录音波纹动画实现

使用CustomPaint绘制动态波纹:

  1. class VoiceWavePainter extends CustomPainter {
  2. final double volume;
  3. final double radius;
  4. VoiceWavePainter({required this.volume, required this.radius});
  5. @override
  6. void paint(Canvas canvas, Size size) {
  7. final paint = Paint()
  8. ..color = Colors.blue.withOpacity(0.6)
  9. ..style = PaintingStyle.stroke
  10. ..strokeWidth = 2;
  11. final center = Offset(size.width / 2, size.height / 2);
  12. // 绘制5个同心圆,半径随音量变化
  13. for (int i = 0; i < 5; i++) {
  14. final currentRadius = radius * (0.3 + i * 0.15) * volume;
  15. canvas.drawCircle(center, currentRadius, paint);
  16. }
  17. }
  18. @override
  19. bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
  20. }

三、完整页面实现

  1. class VoiceMessagePage extends StatelessWidget {
  2. const VoiceMessagePage({Key? key}) : super(key: key);
  3. @override
  4. Widget build(BuildContext context) {
  5. return Scaffold(
  6. appBar: AppBar(title: Text('语音消息')),
  7. body: Column(
  8. mainAxisAlignment: MainAxisAlignment.end,
  9. children: [
  10. // 录音提示区域
  11. Container(
  12. padding: EdgeInsets.symmetric(horizontal: 20),
  13. child: Consumer<VoiceRecorderProvider>(
  14. builder: (context, provider, child) {
  15. if (provider.status == RecordingStatus.recording) {
  16. return Column(
  17. children: [
  18. SizedBox(height: 20),
  19. Text(
  20. '手指上滑,取消发送',
  21. style: TextStyle(color: Colors.grey),
  22. ),
  23. SizedBox(height: 30),
  24. CustomPaint(
  25. size: Size(200, 200),
  26. painter: VoiceWavePainter(
  27. volume: provider.volume,
  28. radius: 80,
  29. ),
  30. ),
  31. SizedBox(height: 10),
  32. Text('${(provider.volume * 5).toInt()}秒'),
  33. ],
  34. );
  35. }
  36. return SizedBox.shrink();
  37. },
  38. ),
  39. ),
  40. // 语音按钮
  41. Padding(
  42. padding: EdgeInsets.only(bottom: 50),
  43. child: VoiceButton(),
  44. ),
  45. ],
  46. ),
  47. );
  48. }
  49. }

四、关键技术点解析

1. 音频录制实现

推荐使用flutter_sound插件:

  1. // 初始化录音器
  2. final _audioRecorder = FlutterSoundRecorder();
  3. await _audioRecorder.openAudioSession();
  4. // 开始录音
  5. await _audioRecorder.startRecorder(
  6. toFile: 'audio_path.aac',
  7. codec: Codec.aacADTS,
  8. );
  9. // 停止录音
  10. final path = await _audioRecorder.stopRecorder();

2. 手势处理优化

  • 使用GestureDetectoronPan系列方法替代onLongPress,获得更精确的控制
  • 计算滑动距离时考虑按钮中心点而非触摸起点
  • 添加防抖处理避免误触发

3. 性能优化策略

  1. 动画优化:使用const构造器减少Widget重建
  2. 录音控制:及时释放音频资源避免内存泄漏
  3. 状态管理:避免不必要的notifyListeners调用

五、完整项目集成建议

  1. 插件依赖

    1. dependencies:
    2. flutter_sound: ^9.2.0
    3. provider: ^6.0.0
  2. 权限配置
    ```xml

// iOS

NSMicrophoneUsageDescription

需要麦克风权限来录制语音消息

  1. 3. **测试建议**:
  2. - 使用模拟器测试不同设备尺寸
  3. - 测试各种手势操作边界情况
  4. - 监控内存使用情况
  5. # 六、扩展功能实现
  6. ## 1. 语音时长显示
  7. ```dart
  8. class VoiceDurationIndicator extends StatelessWidget {
  9. final int duration; // 秒数
  10. @override
  11. Widget build(BuildContext context) {
  12. return Text(
  13. '${Duration(seconds: duration).format()}',
  14. style: TextStyle(
  15. color: Colors.white,
  16. fontSize: 16,
  17. fontWeight: FontWeight.bold,
  18. ),
  19. );
  20. }
  21. }
  22. extension on Duration {
  23. String format() {
  24. final minutes = inMinutes.remainder(60);
  25. final seconds = inSeconds.remainder(60);
  26. return '$minutes:${seconds.toString().padLeft(2, '0')}';
  27. }
  28. }

2. 发送状态反馈

  1. enum SendStatus {
  2. sending,
  3. success,
  4. failed
  5. }
  6. class SendStatusIndicator extends StatelessWidget {
  7. final SendStatus status;
  8. @override
  9. Widget build(BuildContext context) {
  10. return AnimatedSwitcher(
  11. duration: Duration(milliseconds: 300),
  12. child: status == SendStatus.sending
  13. ? CircularProgressIndicator()
  14. : status == SendStatus.success
  15. ? Icon(Icons.check_circle, color: Colors.green)
  16. : Icon(Icons.error, color: Colors.red),
  17. );
  18. }
  19. }

七、常见问题解决方案

  1. 录音权限问题

    • 确保在Info.plistAndroidManifest.xml中正确配置
    • 运行时检查权限:Permission.microphone.request()
  2. 录音中断处理

    1. _audioRecorder.setSubscriptionDurationCallback(
    2. const Duration(milliseconds: 100),
    3. (recordingData) {
    4. if (recordingData.duration.inSeconds > 60) {
    5. _stopRecording(true); // 超过60秒自动停止
    6. }
    7. },
    8. );
  3. UI卡顿问题

    • 将录音波纹动画放在CustomPaint中而非多个Container
    • 使用RepaintBoundary隔离动画区域

八、总结与展望

本实现完整复现了微信语音发送的核心交互,通过组合GestureDetectorCustomPaint和状态管理实现了流畅的用户体验。实际项目中可进一步优化:

  1. 添加语音波形可视化
  2. 实现语音试听功能
  3. 增加发送进度显示
  4. 适配多语言环境

完整代码示例已包含核心功能实现,开发者可根据实际需求进行调整扩展。建议在实际项目中使用riverpod进行状态管理,并添加更完善的错误处理机制。

相关文章推荐

发表评论

活动