diff --git a/lib/controller/message/voice_player_manager.dart b/lib/controller/message/voice_player_manager.dart index 0f498c6..af94c32 100644 --- a/lib/controller/message/voice_player_manager.dart +++ b/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; diff --git a/lib/widget/message/voice_item.dart b/lib/widget/message/voice_item.dart index 3e91478..6fc1656 100644 --- a/lib/widget/message/voice_item.dart +++ b/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 createState() => _VoiceItemState(); } -class _VoiceItemState extends State { +class _VoiceItemState extends State 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 _handlePlayPause() async { try { @@ -55,19 +100,37 @@ class _VoiceItemState extends State { 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 { 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 { 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 { 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), ), );