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.value = audioId;
await _audioPlayer.play(DeviceFileSource(filePath));
if(filePath.startsWith('https://')){
await _audioPlayer.play(UrlSource(filePath));
}else{
await _audioPlayer.play(DeviceFileSource(filePath));
}
} catch (e) {
print('播放音频失败: $e');
_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:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
@ -28,25 +31,67 @@ class VoiceItem extends StatefulWidget {
State<VoiceItem> createState() => _VoiceItemState();
}
class _VoiceItemState extends State<VoiceItem> {
class _VoiceItemState extends State<VoiceItem> with TickerProviderStateMixin {
final VoicePlayerManager _playerManager = VoicePlayerManager.instance;
late AnimationController _waveformAnimationController;
int _animationFrame = 0;
@override
void 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) {
if (mounted) {
setState(() {});
//
if (_playerManager.isPlaying(widget.messageId)) {
if (!_waveformAnimationController.isAnimating) {
_waveformAnimationController.repeat();
}
} else {
_waveformAnimationController.stop();
_waveformAnimationController.reset();
_animationFrame = 0;
}
}
});
ever(_playerManager.playerState, (state) {
if (mounted) {
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 {
try {
@ -55,19 +100,37 @@ class _VoiceItemState extends State<VoiceItem> {
final localPath = widget.voiceBody.localPath;
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) {
await _playerManager.play(widget.messageId, filePath);
} else {
SmartDialog.showToast('无法获取音频文件');
print('音频文件路径为空');
}
} catch (e) {
SmartDialog.showToast('播放失败: $e');
print('播放音频失败: $e');
}
}
@ -120,9 +183,7 @@ class _VoiceItemState extends State<VoiceItem> {
height: 20.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.isSentByMe
? Colors.white
: Colors.black,
color: widget.isSentByMe ? Colors.white : Colors.black,
),
child: Icon(
isPlaying ? Icons.pause : Icons.play_arrow,
@ -193,6 +254,7 @@ class _VoiceItemState extends State<VoiceItem> {
Widget _buildWaveform() {
// 20
final barCount = (widget.voiceBody.duration / 2).ceil().clamp(5, 20);
final isPlaying = _playerManager.isPlaying(widget.messageId);
return SizedBox(
height: 16.h,
@ -200,19 +262,44 @@ class _VoiceItemState extends State<VoiceItem> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
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,
height: height,
margin: EdgeInsets.symmetric(horizontal: 1.w),
decoration: BoxDecoration(
color: widget.isSentByMe
? Colors.white.withOpacity(0.8)
: Colors.grey.withOpacity(0.6),
color: color,
borderRadius: BorderRadius.circular(1.w),
),
);

Loading…
Cancel
Save