一、功能需求分析与设计
微信语音按钮的核心交互包含三个阶段:按住说话(触发录音)、滑动取消(上滑取消发送)、松开发送(完成录音)。实现该功能需解决三大技术挑战:
- 触摸事件精准识别:区分短按(非录音)、长按(录音)、滑动(取消)
- 录音状态管理:实时显示录音时长、音量波动动画
- 权限控制:动态申请麦克风权限并处理拒绝场景
设计时采用分层架构:
- 表现层:CustomPaint绘制波形动画
- 业务层:录音状态机(Idle→Recording→Canceling→Completed)
- 数据层:使用Provider管理录音时长、文件路径等状态
二、核心组件实现
1. 语音按钮组件(RecordButton)
class RecordButton extends StatefulWidget {const RecordButton({super.key});@overrideState<RecordButton> createState() => _RecordButtonState();}class _RecordButtonState extends State<RecordButton> {final _audioRecorder = AudioRecorder();Recording? _recording;bool _isCanceling = false;Offset? _startPosition;@overrideWidget build(BuildContext context) {return GestureDetector(onLongPressStart: _handleLongPressStart,onLongPressMoveUpdate: _handleLongPressMove,onLongPressEnd: _handleLongPressEnd,child: Container(width: 80,height: 80,decoration: BoxDecoration(shape: BoxShape.circle,color: _isCanceling ? Colors.red : Colors.green,),child: Center(child: Text(_isCanceling ? '松开取消' : '按住说话',style: TextStyle(color: Colors.white),),),),);}void _handleLongPressStart(LongPressStartDetails details) {_startPosition = details.globalPosition;_startRecording();}void _handleLongPressMove(LongPressMoveUpdateDetails details) {final dy = details.globalPosition.dy - _startPosition!.dy;_isCanceling = dy < -50; // 上滑50像素触发取消setState(() {});}void _handleLongPressEnd(LongPressEndDetails details) {if (_isCanceling) {_cancelRecording();} else {_finishRecording();}_isCanceling = false;setState(() {});}Future<void> _startRecording() async {final hasPermission = await _checkPermission();if (!hasPermission) return;_recording = await _audioRecorder.start(path: '${DateTime.now().millisecondsSinceEpoch}.m4a',audioEncoder: AudioEncoder.aacLc,sampleRate: 16000,);}// 其他方法实现...}
2. 录音管理器(AudioRecorder)
class AudioRecorder {final _audioRecorder = FlutterAudioRecorder2();Recording? _currentRecording;Future<Recording?> start({required String path,AudioEncoder audioEncoder = AudioEncoder.aacLc,int sampleRate = 16000,}) async {try {final dir = await getApplicationDocumentsDirectory();final fullPath = '${dir.path}/$path';final config = RecordingQuality.high;await _audioRecorder.openAudioSession();_currentRecording = await _audioRecorder.start(toFile: fullPath,encoder: audioEncoder,sampleRate: sampleRate,);return _currentRecording;} catch (e) {debugPrint('录音启动失败: $e');return null;}}Future<bool> stop() async {try {final recording = await _audioRecorder.stop();_currentRecording = null;return recording?.isRecording == false;} catch (e) {debugPrint('录音停止失败: $e');return false;}}}
三、交互细节优化
1. 滑动取消阈值设计
采用动态阈值算法:
bool _shouldCancel(Offset start, Offset current) {final screenHeight = MediaQuery.of(context).size.height;final threshold = screenHeight * 0.15; // 屏幕高度15%作为阈值return current.dy - start.dy < -threshold;}
2. 录音动画实现
使用CustomPaint绘制动态波形:
class WaveformPainter extends CustomPainter {final List<double> amplitudes;@overridevoid paint(Canvas canvas, Size size) {final paint = Paint()..color = Colors.blue..style = PaintingStyle.stroke..strokeWidth = 2;final path = Path();final step = size.width / (amplitudes.length - 1);for (int i = 0; i < amplitudes.length; i++) {final x = i * step;final y = size.height / 2 - amplitudes[i] * 50;if (i == 0) {path.moveTo(x, y);} else {path.lineTo(x, y);}}canvas.drawPath(path, paint);}@overridebool shouldRepaint(covariant CustomPainter oldDelegate) => true;}
3. 权限管理方案
Future<bool> _checkPermission() async {final status = await Permission.microphone.request();if (status != PermissionStatus.granted) {_showPermissionDialog();return false;}return true;}void _showPermissionDialog() {showDialog(context: context,builder: (ctx) => AlertDialog(title: Text('需要麦克风权限'),content: Text('请在设置中开启麦克风权限以使用语音功能'),actions: [TextButton(onPressed: () => openAppSettings(),child: Text('去设置'),),],),);}
四、完整页面集成
class VoiceRecordPage extends StatelessWidget {const VoiceRecordPage({super.key});@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('语音消息')),body: Column(mainAxisAlignment: MainAxisAlignment.center,children: [RecordButton(),SizedBox(height: 40),RecordingIndicator(),],),);}}class RecordingIndicator extends StatelessWidget {@overrideWidget build(BuildContext context) {final recorder = context.watch<AudioRecorderProvider>();return Column(children: [if (recorder.isRecording)SizedBox(width: 200,height: 100,child: WaveformPainter(amplitudes: recorder.amplitudes),),Text('${recorder.duration.inSeconds}秒',style: TextStyle(fontSize: 18),),],);}}
五、性能优化建议
- 录音采样率优化:移动端推荐16kHz采样率,平衡音质与性能
- 波形数据降采样:每100ms更新一次波形数据,减少重绘开销
- 内存管理:录音完成后立即释放资源,避免内存泄漏
- 动画优化:使用RepaintBoundary隔离动画组件,减少不必要的重绘
六、常见问题解决方案
- 录音失败:检查是否添加了
<uses-permission android:name="android.permission.RECORD_AUDIO" /> - 权限弹窗不显示:确保在AndroidManifest.xml中声明了权限
- 波形动画卡顿:使用
AnimatedBuilder替代setState触发重绘 - iOS录音延迟:在Info.plist中添加
NSSpeechRecognitionUsageDescription描述
通过以上实现,开发者可以构建出与微信高度相似的语音交互体验。实际开发中建议将录音功能封装为独立插件,便于多个页面复用。完整项目可参考GitHub上的flutter_voice_message示例,其中包含了更完善的错误处理和跨平台适配方案。