Flutter实战:从零实现微信风格语音发送按钮与交互页面

一、项目需求分析与交互设计

微信语音消息发送的核心交互包含三个阶段:长按录音、滑动取消、松开发送。开发者需要重点解决以下技术难点:

  1. 按钮状态机管理(正常/录音中/取消)
  2. 录音时长控制(1秒触发/60秒限制)
  3. 滑动取消的视觉反馈
  4. 录音波形动态渲染

通过Flutter的GestureDetector和AnimationController组合,可以精准控制各状态转换。建议采用状态模式设计按钮行为,将不同手势操作映射到具体状态。

二、语音按钮核心实现

2.1 基础按钮结构

  1. class VoiceButton extends StatefulWidget {
  2. @override
  3. _VoiceButtonState createState() => _VoiceButtonState();
  4. }
  5. class _VoiceButtonState extends State<VoiceButton> {
  6. VoiceButtonStatus _status = VoiceButtonStatus.normal;
  7. final _animationController = AnimationController(
  8. duration: const Duration(milliseconds: 300),
  9. vsync: this,
  10. );
  11. @override
  12. void dispose() {
  13. _animationController.dispose();
  14. super.dispose();
  15. }
  16. @override
  17. Widget build(BuildContext context) {
  18. return GestureDetector(
  19. onLongPressStart: _handleLongPressStart,
  20. onLongPressMoveUpdate: _handleMoveUpdate,
  21. onLongPressEnd: _handleLongPressEnd,
  22. child: AnimatedBuilder(
  23. animation: _animationController,
  24. builder: (context, child) {
  25. return Container(
  26. width: 70 + _animationController.value * 10,
  27. height: 70 + _animationController.value * 10,
  28. decoration: BoxDecoration(
  29. shape: BoxShape.circle,
  30. color: _status == VoiceButtonStatus.cancel
  31. ? Colors.red[400]
  32. : Colors.green[400],
  33. ),
  34. child: Center(
  35. child: Icon(
  36. _status == VoiceButtonStatus.recording
  37. ? Icons.mic
  38. : Icons.mic_off,
  39. size: 30 + _animationController.value * 5,
  40. ),
  41. ),
  42. );
  43. },
  44. ),
  45. );
  46. }
  47. }

2.2 状态管理实现

采用枚举类型管理三种核心状态:

  1. enum VoiceButtonStatus {
  2. normal, // 初始状态
  3. recording, // 录音中
  4. cancel // 滑动取消
  5. }

通过AnimationController控制按钮尺寸变化:

  • 长按时触发放大动画(0.8→1.2倍)
  • 取消时触发红色闪烁效果
  • 发送后恢复原始尺寸

三、录音功能集成

3.1 录音插件选择

推荐使用flutter_sound插件,其优势包括:

  • 支持iOS/Android双平台
  • 提供实时音频流回调
  • 内置录音权限处理

配置步骤:

  1. 添加依赖:

    1. dependencies:
    2. flutter_sound: ^9.2.13
  2. 初始化录音器:
    ```dart
    final _recorder = FlutterSoundRecorder();

Future _initRecorder() async {
const codec = Codec.aacADTS;
await _recorder.openRecorder();
await _recorder.setSubscriptionDuration(
const Duration(milliseconds: 100),
);
}

  1. ## 3.2 录音控制逻辑
  2. ```dart
  3. void _startRecording() async {
  4. if (_status != VoiceButtonStatus.recording) {
  5. setState(() {
  6. _status = VoiceButtonStatus.recording;
  7. _animationController.forward();
  8. });
  9. await _recorder.startRecorder(
  10. toFile: 'audio_${DateTime.now().millisecondsSinceEpoch}.aac',
  11. codec: Codec.aacADTS,
  12. );
  13. // 启动录音时长监控
  14. _recordingTimer = Timer.periodic(
  15. const Duration(seconds: 1),
  16. (timer) {
  17. if (_recordingDuration >= 60) {
  18. _stopRecording(isCancelled: false);
  19. } else {
  20. _recordingDuration++;
  21. }
  22. },
  23. );
  24. }
  25. }
  26. void _stopRecording({required bool isCancelled}) async {
  27. _recordingTimer?.cancel();
  28. await _recorder.stopRecorder();
  29. if (mounted) {
  30. setState(() {
  31. _status = isCancelled
  32. ? VoiceButtonStatus.cancel
  33. : VoiceButtonStatus.normal;
  34. _animationController.reverse();
  35. });
  36. if (!isCancelled && _recordingDuration >= 1) {
  37. // 处理音频文件
  38. final audioFile = File('audio_${DateTime.now().millisecondsSinceEpoch}.aac');
  39. // 上传或播放逻辑...
  40. }
  41. }
  42. }

四、波形动画实现

4.1 自定义波形组件

  1. class WaveForm extends StatefulWidget {
  2. final Stream<List<double>> amplitudeStream;
  3. const WaveForm({required this.amplitudeStream, Key? key}) : super(key: key);
  4. @override
  5. _WaveFormState createState() => _WaveFormState();
  6. }
  7. class _WaveFormState extends State<WaveForm> {
  8. List<double> _amplitudes = [];
  9. @override
  10. void initState() {
  11. super.initState();
  12. widget.amplitudeStream.listen((amplitudes) {
  13. if (mounted) {
  14. setState(() {
  15. _amplitudes = amplitudes;
  16. });
  17. }
  18. });
  19. }
  20. @override
  21. Widget build(BuildContext context) {
  22. return AspectRatio(
  23. aspectRatio: 2,
  24. child: CustomPaint(
  25. painter: WaveFormPainter(_amplitudes),
  26. ),
  27. );
  28. }
  29. }
  30. class WaveFormPainter extends CustomPainter {
  31. final List<double> amplitudes;
  32. WaveFormPainter(this.amplitudes);
  33. @override
  34. void paint(Canvas canvas, Size size) {
  35. final paint = Paint()
  36. ..color = Colors.green[300]!
  37. ..strokeWidth = 2
  38. ..style = PaintingStyle.stroke;
  39. final path = Path();
  40. final pointCount = amplitudes.length;
  41. final stepX = size.width / (pointCount - 1);
  42. for (int i = 0; i < pointCount; i++) {
  43. final x = i * stepX;
  44. final y = size.height / 2 * (1 - amplitudes[i].clamp(0, 1));
  45. if (i == 0) {
  46. path.moveTo(x, y);
  47. } else {
  48. path.lineTo(x, y);
  49. }
  50. }
  51. canvas.drawPath(path, paint);
  52. }
  53. @override
  54. bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
  55. }

4.2 实时数据流处理

通过flutter_soundonRecorderStateChanged获取实时振幅数据:

  1. _recorder.setSubscriptionDuration(const Duration(milliseconds: 50));
  2. final amplitudeStream = _recorder.onRecorderStateChanged
  3. ?.map((event) => event.dbPeakLevel ?? 0)
  4. .map((db) => (1 + db / 60).clamp(0, 1).toDouble())
  5. .buffer(const Duration(milliseconds: 100))
  6. .map((samples) {
  7. // 生成平滑的波形数据
  8. final windowSize = 30;
  9. final padded = List<double>.filled(windowSize, 0) + samples;
  10. final result = <double>[];
  11. for (var i = 0; i < windowSize; i++) {
  12. final windowStart = i;
  13. final windowEnd = i + samples.length;
  14. final window = padded.sublist(windowStart, windowEnd);
  15. result.add(window.reduce((a, b) => a + b) / window.length);
  16. }
  17. return result;
  18. });

五、完整页面集成

5.1 页面布局设计

  1. class VoiceMessagePage extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return Scaffold(
  5. appBar: AppBar(title: const Text('语音消息')),
  6. body: Column(
  7. children: [
  8. Expanded(
  9. child: Center(
  10. child: Container(
  11. width: 200,
  12. height: 200,
  13. decoration: BoxDecoration(
  14. border: Border.all(color: Colors.grey[300]!),
  15. borderRadius: BorderRadius.circular(10),
  16. ),
  17. child: const WaveForm(amplitudeStream: Stream.empty()),
  18. ),
  19. ),
  20. ),
  21. const Padding(
  22. padding: EdgeInsets.all(20),
  23. child: VoiceButton(),
  24. ),
  25. const SizedBox(height: 20),
  26. Text('滑动可取消发送', style: TextStyle(color: Colors.grey)),
  27. ],
  28. ),
  29. );
  30. }
  31. }

5.2 状态同步处理

使用ValueNotifier实现按钮状态与页面UI的同步:

  1. class VoiceButtonController extends ValueNotifier<VoiceButtonStatus> {
  2. VoiceButtonController() : super(VoiceButtonStatus.normal);
  3. void startRecording() => value = VoiceButtonStatus.recording;
  4. void cancelRecording() => value = VoiceButtonStatus.cancel;
  5. void reset() => value = VoiceButtonStatus.normal;
  6. }
  7. // 在页面中使用
  8. final controller = VoiceButtonController();
  9. ValueListenableBuilder(
  10. valueListenable: controller,
  11. builder: (context, status, child) {
  12. return AnimatedContainer(
  13. duration: const Duration(milliseconds: 300),
  14. color: status == VoiceButtonStatus.cancel
  15. ? Colors.red
  16. : Colors.green,
  17. // 其他UI元素...
  18. );
  19. },
  20. )

六、性能优化建议

  1. 录音内存管理

    • 及时关闭不再使用的录音器
    • 使用isolate处理耗时的音频编码
  2. 动画性能优化

    • CustomPaint使用RepaintBoundary
    • 限制波形数据点数(建议30-60个点)
  3. 状态管理优化

    • 使用ProviderRiverpod管理全局状态
    • 避免在build方法中创建新对象
  4. 平台适配处理

    • iOS需要配置NSMicrophoneUsageDescription
    • Android需要动态请求RECORD_AUDIO权限

七、扩展功能建议

  1. 语音转文字:集成阿里云/腾讯云语音识别API
  2. 变声效果:使用soundpool实现实时音频处理
  3. 多语言支持:根据系统语言切换提示文本
  4. 无障碍适配:为按钮添加语音提示功能

通过以上实现方案,开发者可以构建出与微信体验高度一致的语音发送功能。实际开发中建议将录音逻辑封装为独立模块,便于在不同页面复用。对于商业项目,还需考虑添加噪音抑制、回声消除等高级音频处理功能。