Browse Source
Merge branch 'master' of http://git.qniao.cn/dating-agency/dating_touchme_app
Merge branch 'master' of http://git.qniao.cn/dating-agency/dating_touchme_app
* 'master' of http://git.qniao.cn/dating-agency/dating_touchme_app: feat(live): 实现实名认证匹配功能并优化直播间动效播放 添加用户信息页面跳转到聊天页面 no message feat(discover): 实现SVGA动画播放管理器和播放组件 feat(live): 实现礼物弹窗用户选择与礼物展示功能 增加红娘等级,增加昵称最大宽度 修改信息上传头像增加示例,增加跳转支付宝逻辑 修改信息上传头像增加示例,增加跳转支付宝逻辑 # Conflicts: # lib/controller/discover/room_controller.dartios
15 changed files with 602 additions and 197 deletions
Unified View
Diff Options
-
9ios/Podfile
-
103lib/controller/discover/room_controller.dart
-
86lib/controller/discover/svga_player_manager.dart
-
1lib/controller/mine/mine_controller.dart
-
1lib/controller/mine/rose_controller.dart
-
28lib/model/home/marriage_data.dart
-
2lib/network/api_urls.dart
-
4lib/network/rtc_api.dart
-
31lib/network/rtc_api.g.dart
-
4lib/pages/discover/live_room_page.dart
-
14lib/pages/home/user_information_page.dart
-
19lib/pages/mine/mine_page.dart
-
264lib/widget/live/live_gift_popup.dart
-
6lib/widget/live/live_room_anchor_showcase.dart
-
227lib/widget/live/svga_player_widget.dart
@ -0,0 +1,86 @@ |
|||||
|
import 'dart:collection'; |
||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_svga/flutter_svga.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
/// SVGA 动画项 |
||||
|
class SvgaAnimationItem { |
||||
|
final String svgaFile; |
||||
|
final String? targetUserId; // 接收礼物的用户ID |
||||
|
final String? senderUserId; // 发送礼物的用户ID |
||||
|
final String? giftProductId; // 礼物产品ID |
||||
|
|
||||
|
SvgaAnimationItem({ |
||||
|
required this.svgaFile, |
||||
|
this.targetUserId, |
||||
|
this.senderUserId, |
||||
|
this.giftProductId, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/// SVGA 动画播放队列管理器 |
||||
|
/// 注意:由于 SVGAAnimationController 需要 vsync,实际播放需要在 Widget 中完成 |
||||
|
/// 这个管理器只负责管理队列,实际的播放需要通过回调通知外部 Widget |
||||
|
class SvgaPlayerManager extends GetxController { |
||||
|
static SvgaPlayerManager? _instance; |
||||
|
static SvgaPlayerManager get instance { |
||||
|
_instance ??= Get.put(SvgaPlayerManager()); |
||||
|
return _instance!; |
||||
|
} |
||||
|
|
||||
|
final Queue<SvgaAnimationItem> _animationQueue = Queue<SvgaAnimationItem>(); |
||||
|
final Rx<SvgaAnimationItem?> currentItem = Rx<SvgaAnimationItem?>(null); |
||||
|
final RxBool isPlaying = false.obs; |
||||
|
|
||||
|
/// 添加动画到队列 |
||||
|
void addToQueue(SvgaAnimationItem item) { |
||||
|
_animationQueue.add(item); |
||||
|
_playNext(); |
||||
|
} |
||||
|
|
||||
|
/// 播放下一个动画 |
||||
|
void _playNext() { |
||||
|
if (isPlaying.value || _animationQueue.isEmpty) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
final item = _animationQueue.removeFirst(); |
||||
|
currentItem.value = item; |
||||
|
isPlaying.value = true; |
||||
|
print('✅ SVGA 动画已添加到播放队列: ${item.svgaFile}'); |
||||
|
} |
||||
|
|
||||
|
/// 标记当前动画播放完成 |
||||
|
void onAnimationFinished() { |
||||
|
print('✅ SVGA 动画播放完成'); |
||||
|
isPlaying.value = false; |
||||
|
currentItem.value = null; |
||||
|
// 播放下一个 |
||||
|
_playNext(); |
||||
|
} |
||||
|
|
||||
|
/// 标记当前动画播放失败 |
||||
|
void onAnimationError(String error) { |
||||
|
print('❌ SVGA 动画播放失败: $error'); |
||||
|
isPlaying.value = false; |
||||
|
currentItem.value = null; |
||||
|
// 继续播放下一个 |
||||
|
_playNext(); |
||||
|
} |
||||
|
|
||||
|
/// 停止当前播放 |
||||
|
void stop() { |
||||
|
isPlaying.value = false; |
||||
|
currentItem.value = null; |
||||
|
} |
||||
|
|
||||
|
/// 清空队列 |
||||
|
void clearQueue() { |
||||
|
stop(); |
||||
|
_animationQueue.clear(); |
||||
|
} |
||||
|
|
||||
|
/// 获取队列长度 |
||||
|
int get queueLength => _animationQueue.length; |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,227 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_svga/flutter_svga.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:dating_touchme_app/controller/discover/svga_player_manager.dart'; |
||||
|
|
||||
|
/// SVGA 动画播放 Widget |
||||
|
/// 监听 SvgaPlayerManager 的队列,自动播放 SVGA 动画 |
||||
|
class SvgaPlayerWidget extends StatefulWidget { |
||||
|
const SvgaPlayerWidget({super.key}); |
||||
|
|
||||
|
@override |
||||
|
State<SvgaPlayerWidget> createState() => _SvgaPlayerWidgetState(); |
||||
|
} |
||||
|
|
||||
|
class _SvgaPlayerWidgetState extends State<SvgaPlayerWidget> |
||||
|
with TickerProviderStateMixin { |
||||
|
final SvgaPlayerManager _manager = SvgaPlayerManager.instance; |
||||
|
SVGAAnimationController? _controller; |
||||
|
SvgaAnimationItem? _currentItem; |
||||
|
|
||||
|
@override |
||||
|
void initState() { |
||||
|
super.initState(); |
||||
|
// 监听队列变化 |
||||
|
ever(_manager.currentItem, (item) { |
||||
|
print( |
||||
|
'📢 currentItem 变化: ${item?.svgaFile ?? "null"}, 当前 _currentItem: ${_currentItem?.svgaFile ?? "null"}', |
||||
|
); |
||||
|
if (item != null) { |
||||
|
// 如果当前没有播放,或者新的 item 与当前不同,则播放 |
||||
|
if (_currentItem == null || item.svgaFile != _currentItem!.svgaFile) { |
||||
|
print('🎯 准备播放新动画: ${item.svgaFile}'); |
||||
|
_playAnimation(item); |
||||
|
} else { |
||||
|
print('⚠️ 相同的动画,跳过播放'); |
||||
|
} |
||||
|
} else { |
||||
|
// currentItem 变为 null,但不立即清理,等待播放完成回调 |
||||
|
print('📢 currentItem 变为 null,等待播放完成回调'); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
void dispose() { |
||||
|
_controller?.dispose(); |
||||
|
super.dispose(); |
||||
|
} |
||||
|
|
||||
|
/// 播放动画 |
||||
|
Future<void> _playAnimation(SvgaAnimationItem item) async { |
||||
|
print( |
||||
|
'🎬 开始播放动画: ${item.svgaFile}, 当前状态: _controller=${_controller != null}, _currentItem=${_currentItem?.svgaFile ?? "null"}', |
||||
|
); |
||||
|
|
||||
|
// 如果正在播放,先停止并清理 |
||||
|
if (_controller != null) { |
||||
|
print('🛑 停止当前播放'); |
||||
|
_controller!.stop(); |
||||
|
_controller!.dispose(); |
||||
|
_controller = null; |
||||
|
} |
||||
|
|
||||
|
// 设置当前项并创建新的 controller |
||||
|
_currentItem = item; |
||||
|
_controller = SVGAAnimationController(vsync: this); |
||||
|
print('✅ 创建新的 controller,准备加载动画'); |
||||
|
|
||||
|
try { |
||||
|
// 判断是网络 URL 还是本地资源 |
||||
|
if (item.svgaFile.startsWith('http://') || |
||||
|
item.svgaFile.startsWith('https://')) { |
||||
|
// 网络 URL |
||||
|
SVGAParser.shared |
||||
|
.decodeFromURL(item.svgaFile) |
||||
|
.then((video) { |
||||
|
if (!mounted) return; |
||||
|
|
||||
|
// 检查是否还是当前要播放的动画 |
||||
|
if (_currentItem != item || _controller == null) { |
||||
|
print('⚠️ 动画已变更,取消播放: ${item.svgaFile}'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
_controller!.videoItem = video; |
||||
|
// 播放动画(repeat 会循环播放,我们需要监听完成) |
||||
|
_controller!.repeat(); |
||||
|
|
||||
|
// 获取动画时长,如果为 null 则使用默认值 3 秒 |
||||
|
final duration = _controller!.duration; |
||||
|
final playDuration = duration != null && duration > Duration.zero |
||||
|
? duration |
||||
|
: const Duration(seconds: 3); |
||||
|
|
||||
|
print( |
||||
|
'✅ SVGA 动画加载成功(网络): ${item.svgaFile}, 播放时长: ${playDuration.inMilliseconds}ms', |
||||
|
); |
||||
|
|
||||
|
// 在动画时长后停止并通知完成 |
||||
|
Future.delayed(playDuration, () { |
||||
|
if (!mounted) { |
||||
|
print('⚠️ Widget 已卸载,取消完成回调'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 再次检查是否还是当前动画 |
||||
|
if (_currentItem == item && _controller != null) { |
||||
|
print('✅ SVGA 动画播放完成(网络): ${item.svgaFile}'); |
||||
|
|
||||
|
// 先停止动画 |
||||
|
_controller!.stop(); |
||||
|
|
||||
|
// 清理当前项和 controller |
||||
|
final wasCurrentItem = _currentItem; |
||||
|
_currentItem = null; |
||||
|
_controller?.dispose(); |
||||
|
_controller = null; |
||||
|
|
||||
|
// 通知管理器播放完成(这会触发下一个动画) |
||||
|
if (wasCurrentItem == item) { |
||||
|
_manager.onAnimationFinished(); |
||||
|
} |
||||
|
} else { |
||||
|
print( |
||||
|
'⚠️ 动画已变更,跳过完成回调: _currentItem=${_currentItem?.svgaFile ?? "null"}, item=${item.svgaFile}', |
||||
|
); |
||||
|
} |
||||
|
}); |
||||
|
}) |
||||
|
.catchError((error) { |
||||
|
print('❌ SVGA 动画加载失败(网络): $error'); |
||||
|
_currentItem = null; |
||||
|
_controller?.dispose(); |
||||
|
_controller = null; |
||||
|
_manager.onAnimationError(error.toString()); |
||||
|
}); |
||||
|
} else { |
||||
|
// 本地资源(assets) |
||||
|
SVGAParser.shared |
||||
|
.decodeFromAssets(item.svgaFile) |
||||
|
.then((video) { |
||||
|
if (!mounted) return; |
||||
|
|
||||
|
// 检查是否还是当前要播放的动画 |
||||
|
if (_currentItem != item || _controller == null) { |
||||
|
print('⚠️ 动画已变更,取消播放: ${item.svgaFile}'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
_controller!.videoItem = video; |
||||
|
// 播放动画(repeat 会循环播放,我们需要监听完成) |
||||
|
_controller!.repeat(); |
||||
|
|
||||
|
// 获取动画时长,如果为 null 则使用默认值 3 秒 |
||||
|
final duration = _controller!.duration; |
||||
|
final playDuration = duration != null && duration > Duration.zero |
||||
|
? duration |
||||
|
: const Duration(seconds: 3); |
||||
|
|
||||
|
print( |
||||
|
'✅ SVGA 动画加载成功(本地): ${item.svgaFile}, 播放时长: ${playDuration.inMilliseconds}ms', |
||||
|
); |
||||
|
|
||||
|
// 在动画时长后停止并通知完成 |
||||
|
Future.delayed(playDuration, () { |
||||
|
if (!mounted) { |
||||
|
print('⚠️ Widget 已卸载,取消完成回调'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 再次检查是否还是当前动画 |
||||
|
if (_currentItem == item && _controller != null) { |
||||
|
print('✅ SVGA 动画播放完成(本地): ${item.svgaFile}'); |
||||
|
|
||||
|
// 先停止动画 |
||||
|
_controller!.stop(); |
||||
|
|
||||
|
// 清理当前项和 controller |
||||
|
final wasCurrentItem = _currentItem; |
||||
|
_currentItem = null; |
||||
|
_controller?.dispose(); |
||||
|
_controller = null; |
||||
|
|
||||
|
// 通知管理器播放完成(这会触发下一个动画) |
||||
|
if (wasCurrentItem == item) { |
||||
|
_manager.onAnimationFinished(); |
||||
|
} |
||||
|
} else { |
||||
|
print( |
||||
|
'⚠️ 动画已变更,跳过完成回调: _currentItem=${_currentItem?.svgaFile ?? "null"}, item=${item.svgaFile}', |
||||
|
); |
||||
|
} |
||||
|
}); |
||||
|
}) |
||||
|
.catchError((error) { |
||||
|
print('❌ SVGA 动画加载失败(本地): $error'); |
||||
|
_currentItem = null; |
||||
|
_controller?.dispose(); |
||||
|
_controller = null; |
||||
|
_manager.onAnimationError(error.toString()); |
||||
|
}); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
print('❌ SVGA 播放异常: $e'); |
||||
|
_currentItem = null; |
||||
|
_controller?.dispose(); |
||||
|
_controller = null; |
||||
|
_manager.onAnimationError(e.toString()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
return Obx(() { |
||||
|
final currentItem = _manager.currentItem.value; |
||||
|
final isPlaying = _manager.isPlaying.value; |
||||
|
|
||||
|
if (!isPlaying || currentItem == null || _controller == null) { |
||||
|
return const SizedBox.shrink(); |
||||
|
} |
||||
|
|
||||
|
return Positioned.fill( |
||||
|
child: IgnorePointer(child: SVGAImage(_controller!)), |
||||
|
); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save