Browse Source

feat(message): 实现语音消息波形动画和播放优化

- 添加波形动画控制器,实现播放时的动态波浪效果-优化音频文件路径处理逻辑,支持网络URL播放
- 改进播放状态监听,确保动画与播放状态同步- 添加文件存在性检查,提升播放稳定性
- 使用AnimatedContainer优化波形条动画过渡效果
- 完善错误提示,增强用户体验
ios
Jolie 4 months ago
parent
commit
388f2aebee
2 changed files with 110 additions and 19 deletions
  1. 6
      lib/controller/message/voice_player_manager.dart
  2. 123
      lib/widget/message/voice_item.dart

6
lib/controller/message/voice_player_manager.dart

@ -49,7 +49,11 @@ class VoicePlayerManager extends GetxController {
// //
_currentPlayingId = audioId; _currentPlayingId = audioId;
currentPlayingId.value = audioId; currentPlayingId.value = audioId;
await _audioPlayer.play(DeviceFileSource(filePath));
if(filePath.startsWith('https://')){
await _audioPlayer.play(UrlSource(filePath));
}else{
await _audioPlayer.play(DeviceFileSource(filePath));
}
} catch (e) { } catch (e) {
print('播放音频失败: $e'); print('播放音频失败: $e');
_currentPlayingId = null; _currentPlayingId = null;

123
lib/widget/message/voice_item.dart

@ -1,3 +1,6 @@
import 'dart:io';
import 'dart:math' as math;
import 'package:audioplayers/audioplayers.dart';
import 'package:dating_touchme_app/extension/ex_widget.dart'; import 'package:dating_touchme_app/extension/ex_widget.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
@ -28,25 +31,67 @@ class VoiceItem extends StatefulWidget {
State<VoiceItem> createState() => _VoiceItemState(); State<VoiceItem> createState() => _VoiceItemState();
} }
class _VoiceItemState extends State<VoiceItem> {
class _VoiceItemState extends State<VoiceItem> with TickerProviderStateMixin {
final VoicePlayerManager _playerManager = VoicePlayerManager.instance; final VoicePlayerManager _playerManager = VoicePlayerManager.instance;
late AnimationController _waveformAnimationController;
int _animationFrame = 0;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
//
_waveformAnimationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000), // 2
);
//
_waveformAnimationController.addListener(() {
if (mounted && _playerManager.isPlaying(widget.messageId)) {
setState(() {
// 使
_animationFrame++;
});
}
});
// //
ever(_playerManager.currentPlayingId, (audioId) { ever(_playerManager.currentPlayingId, (audioId) {
if (mounted) { if (mounted) {
setState(() {}); setState(() {});
//
if (_playerManager.isPlaying(widget.messageId)) {
if (!_waveformAnimationController.isAnimating) {
_waveformAnimationController.repeat();
}
} else {
_waveformAnimationController.stop();
_waveformAnimationController.reset();
_animationFrame = 0;
}
} }
}); });
ever(_playerManager.playerState, (state) { ever(_playerManager.playerState, (state) {
if (mounted) { if (mounted) {
setState(() {}); setState(() {});
//
if (state == PlayerState.playing &&
_playerManager.currentPlayingId.value == widget.messageId) {
_waveformAnimationController.repeat();
} else {
_waveformAnimationController.stop();
_animationFrame = 0;
}
} }
}); });
} }
@override
void dispose() {
_waveformAnimationController.dispose();
super.dispose();
}
// / // /
Future<void> _handlePlayPause() async { Future<void> _handlePlayPause() async {
try { try {
@ -55,19 +100,37 @@ class _VoiceItemState extends State<VoiceItem> {
final localPath = widget.voiceBody.localPath; final localPath = widget.voiceBody.localPath;
final remotePath = widget.voiceBody.remotePath; final remotePath = widget.voiceBody.remotePath;
if (localPath.isNotEmpty) {
filePath = localPath;
} else if (remotePath != null && remotePath.isNotEmpty) {
//
filePath = remotePath;
// 使
if (remotePath != null && remotePath.isNotEmpty) {
// 使audioplayers URL
// URL
if (remotePath.startsWith('http://') ||
remotePath.startsWith('https://')) {
filePath = remotePath;
} else {
SmartDialog.showToast('音频文件不存在,请等待下载完成');
print('远程音频文件路径: $remotePath');
return;
}
} else if (localPath.isNotEmpty) {
final localFile = File(localPath);
if (await localFile.exists()) {
filePath = localPath;
} else {
SmartDialog.showToast('音频文件不存在');
print('本地音频文件不存在: $localPath');
return;
}
} }
SmartDialog.showToast('来了$remotePath');
if (filePath != null && filePath.isNotEmpty) { if (filePath != null && filePath.isNotEmpty) {
await _playerManager.play(widget.messageId, filePath); await _playerManager.play(widget.messageId, filePath);
} else { } else {
SmartDialog.showToast('无法获取音频文件');
print('音频文件路径为空'); print('音频文件路径为空');
} }
} catch (e) { } catch (e) {
SmartDialog.showToast('播放失败: $e');
print('播放音频失败: $e'); print('播放音频失败: $e');
} }
} }
@ -120,9 +183,7 @@ class _VoiceItemState extends State<VoiceItem> {
height: 20.w, height: 20.w,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: widget.isSentByMe
? Colors.white
: Colors.black,
color: widget.isSentByMe ? Colors.white : Colors.black,
), ),
child: Icon( child: Icon(
isPlaying ? Icons.pause : Icons.play_arrow, isPlaying ? Icons.pause : Icons.play_arrow,
@ -193,6 +254,7 @@ class _VoiceItemState extends State<VoiceItem> {
Widget _buildWaveform() { Widget _buildWaveform() {
// 20 // 20
final barCount = (widget.voiceBody.duration / 2).ceil().clamp(5, 20); final barCount = (widget.voiceBody.duration / 2).ceil().clamp(5, 20);
final isPlaying = _playerManager.isPlaying(widget.messageId);
return SizedBox( return SizedBox(
height: 16.h, height: 16.h,
@ -200,19 +262,44 @@ class _VoiceItemState extends State<VoiceItem> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: List.generate(barCount, (index) { children: List.generate(barCount, (index) {
//
final random = (index * 7) % 5;
final baseHeight = 6 + random * 2;
final height = (baseHeight.clamp(4, 16)).h;
double height;
Color color;
if (isPlaying) {
//
// 使
// wavePhase: (_animationFrame)(index)
//
final wavePhase = _animationFrame * 0.15 + index * 0.6;
// 使 4-16
final sinValue = math.sin(wavePhase);
final normalizedValue = (sinValue + 1) / 2; // 0-1
final baseHeight = 4 + normalizedValue * 12;
height = (baseHeight.clamp(4, 16)).h;
//
final opacity = 0.5 + normalizedValue * 0.5;
color = widget.isSentByMe
? Colors.white.withOpacity(opacity.clamp(0.5, 1.0))
: Colors.grey.withOpacity(opacity.clamp(0.5, 0.9));
} else {
//
final random = (index * 7) % 5;
final baseHeight = 6 + random * 2;
height = (baseHeight.clamp(4, 16)).h;
color = widget.isSentByMe
? Colors.white.withOpacity(0.8)
: Colors.grey.withOpacity(0.6);
}
return Container(
return AnimatedContainer(
duration: const Duration(milliseconds: 100),
curve: Curves.easeOut,
width: 2.w, width: 2.w,
height: height, height: height,
margin: EdgeInsets.symmetric(horizontal: 1.w), margin: EdgeInsets.symmetric(horizontal: 1.w),
decoration: BoxDecoration( decoration: BoxDecoration(
color: widget.isSentByMe
? Colors.white.withOpacity(0.8)
: Colors.grey.withOpacity(0.6),
color: color,
borderRadius: BorderRadius.circular(1.w), borderRadius: BorderRadius.circular(1.w),
), ),
); );

Loading…
Cancel
Save