|
|
|
@ -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), |
|
|
|
), |
|
|
|
); |
|
|
|
|