735 lines
20 KiB
Dart
735 lines
20 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_svga/flutter_svga.dart';
|
||
import 'dart:async';
|
||
import 'dart:typed_data';
|
||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
|
||
|
||
// 动画队列项
|
||
class AnimationQueueItem {
|
||
final String resource;
|
||
final Map<String, dynamic>? dynamicData; // 动态数据(文本、图片等)
|
||
final VoidCallback? onCompleted;
|
||
final Function(String)? onError;
|
||
Completer<void>? completer;
|
||
|
||
AnimationQueueItem({
|
||
required this.resource,
|
||
this.dynamicData,
|
||
this.onCompleted,
|
||
this.onError,
|
||
});
|
||
}
|
||
|
||
// 动态数据类型
|
||
class DynamicData {
|
||
final Map<String, String>? textReplacements; // 文本替换
|
||
final Map<String, Uint8List>? imageReplacements; // 图片替换(二进制数据)
|
||
final Map<String, String>? imageAssetPaths; // 图片资源路径
|
||
|
||
DynamicData({
|
||
this.textReplacements,
|
||
this.imageReplacements,
|
||
this.imageAssetPaths,
|
||
});
|
||
}
|
||
|
||
// 动画队列管理器
|
||
class RoomEntranceQueueManager {
|
||
static final RoomEntranceQueueManager _instance =
|
||
RoomEntranceQueueManager._internal();
|
||
|
||
factory RoomEntranceQueueManager() => _instance;
|
||
|
||
RoomEntranceQueueManager._internal();
|
||
|
||
// 队列列表
|
||
final List<AnimationQueueItem> _queue = [];
|
||
|
||
// 当前是否正在播放
|
||
bool _isPlaying = false;
|
||
|
||
// 当前播放器实例
|
||
_RoomEntranceWidgetState? _currentPlayer;
|
||
|
||
// 队列回调
|
||
void Function()? onQueueUpdated;
|
||
|
||
// 最大队列长度限制
|
||
int maxQueueLength = 20;
|
||
|
||
// 自动播放延迟时间(毫秒)
|
||
int autoPlayDelay = 500;
|
||
|
||
// 添加动画到队列
|
||
Future<void> addToQueue(AnimationQueueItem item) {
|
||
final completer = Completer<void>();
|
||
item.completer = completer;
|
||
|
||
// 检查队列长度限制
|
||
if (_queue.length >= maxQueueLength) {
|
||
// 移除最早的动画
|
||
final removedItem = _queue.removeAt(0);
|
||
removedItem.completer?.completeError('Queue overflow, item removed');
|
||
}
|
||
|
||
_queue.add(item);
|
||
|
||
// 通知队列更新
|
||
if (onQueueUpdated != null) {
|
||
onQueueUpdated!();
|
||
}
|
||
|
||
// 如果没有正在播放,则开始播放队列
|
||
if (!_isPlaying) {
|
||
_startNextAnimation();
|
||
}
|
||
|
||
return completer.future;
|
||
}
|
||
|
||
// 获取当前队列长度
|
||
int get queueLength => _queue.length;
|
||
|
||
// 获取等待中的队列项
|
||
List<AnimationQueueItem> get waitingItems =>
|
||
_queue.isNotEmpty && _isPlaying
|
||
? _queue.sublist(1)
|
||
: List<AnimationQueueItem>.from(_queue);
|
||
|
||
// 设置当前播放器
|
||
void setCurrentPlayer(_RoomEntranceWidgetState player) {
|
||
_currentPlayer = player;
|
||
}
|
||
|
||
// 清除当前播放器
|
||
void clearCurrentPlayer() {
|
||
_currentPlayer = null;
|
||
}
|
||
|
||
// 开始播放下一个动画
|
||
void _startNextAnimation() {
|
||
if (_queue.isEmpty) {
|
||
_isPlaying = false;
|
||
if (onQueueUpdated != null) {
|
||
onQueueUpdated!();
|
||
}
|
||
return;
|
||
}
|
||
|
||
_isPlaying = true;
|
||
final nextItem = _queue.first;
|
||
|
||
// 如果有延迟,则延迟播放
|
||
Future.delayed(Duration(milliseconds: autoPlayDelay), () {
|
||
if (_currentPlayer != null && _currentPlayer!.mounted) {
|
||
// 播放队列中的动画
|
||
_currentPlayer!.playFromQueue(
|
||
resource: nextItem.resource,
|
||
dynamicData:
|
||
nextItem.dynamicData != null
|
||
? DynamicData(
|
||
textReplacements: nextItem.dynamicData?['textReplacements'],
|
||
imageReplacements:
|
||
nextItem.dynamicData?['imageReplacements'],
|
||
imageAssetPaths: nextItem.dynamicData?['imageAssetPaths'],
|
||
)
|
||
: null,
|
||
onCompleted: () {
|
||
// 动画完成回调
|
||
if (nextItem.onCompleted != null) {
|
||
nextItem.onCompleted!();
|
||
}
|
||
|
||
// 完成Completer
|
||
nextItem.completer?.complete();
|
||
|
||
// 从队列中移除当前项
|
||
_queue.removeAt(0);
|
||
|
||
// 通知队列更新
|
||
if (onQueueUpdated != null) {
|
||
onQueueUpdated!();
|
||
}
|
||
|
||
// 播放下一个
|
||
_startNextAnimation();
|
||
},
|
||
onError: (error) {
|
||
// 错误处理
|
||
if (nextItem.onError != null) {
|
||
nextItem.onError!(error);
|
||
}
|
||
|
||
// 完成Completer(带错误)
|
||
nextItem.completer?.completeError(error);
|
||
|
||
// 从队列中移除当前项
|
||
_queue.removeAt(0);
|
||
|
||
// 通知队列更新
|
||
if (onQueueUpdated != null) {
|
||
onQueueUpdated!();
|
||
}
|
||
|
||
// 播放下一个
|
||
_startNextAnimation();
|
||
},
|
||
);
|
||
} else {
|
||
// 如果播放器不可用,跳过当前项
|
||
_queue.removeAt(0);
|
||
_startNextAnimation();
|
||
}
|
||
});
|
||
}
|
||
|
||
// 清空队列
|
||
void clearQueue() {
|
||
for (final item in _queue) {
|
||
item.completer?.completeError('Queue cleared');
|
||
}
|
||
_queue.clear();
|
||
_isPlaying = false;
|
||
|
||
if (onQueueUpdated != null) {
|
||
onQueueUpdated!();
|
||
}
|
||
}
|
||
|
||
// 移除指定位置的队列项
|
||
void removeAtIndex(int index) {
|
||
if (index >= 0 && index < _queue.length) {
|
||
final removedItem = _queue.removeAt(index);
|
||
removedItem.completer?.completeError('Item removed from queue');
|
||
|
||
if (onQueueUpdated != null) {
|
||
onQueueUpdated!();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取队列状态
|
||
Map<String, dynamic> get queueStatus {
|
||
return {
|
||
'isPlaying': _isPlaying,
|
||
'queueLength': _queue.length,
|
||
'currentResource': _queue.isNotEmpty ? _queue.first.resource : null,
|
||
'waitingCount':
|
||
_queue.length > (_isPlaying ? 1 : 0)
|
||
? _queue.length - (_isPlaying ? 1 : 0)
|
||
: 0,
|
||
};
|
||
}
|
||
}
|
||
|
||
class RoomEntranceWidget extends StatefulWidget {
|
||
// 尺寸
|
||
final double? width;
|
||
final double? height;
|
||
|
||
// 动画控制参数
|
||
final int loops; // 循环次数(0表示无限循环)
|
||
final bool clearsAfterStop;
|
||
|
||
// 缓存控制
|
||
final bool useCache;
|
||
|
||
// 队列相关参数
|
||
final bool useQueue; // 是否使用队列
|
||
final int queueDelay; // 队列播放延迟(毫秒)
|
||
|
||
// 回调函数(不再需要onCompleted和onError,因为通过队列管理)
|
||
final VoidCallback? onStartLoading;
|
||
final VoidCallback? onFinishLoading;
|
||
|
||
// 是否在初始化时自动开始监听队列
|
||
final bool autoStartQueue;
|
||
|
||
const RoomEntranceWidget({
|
||
Key? key,
|
||
this.width,
|
||
this.height,
|
||
this.loops = 0,
|
||
this.clearsAfterStop = true,
|
||
this.useCache = true,
|
||
this.useQueue = true,
|
||
this.queueDelay = 500,
|
||
this.onStartLoading,
|
||
this.onFinishLoading,
|
||
this.autoStartQueue = true,
|
||
}) : super(key: key);
|
||
|
||
@override
|
||
_RoomEntranceWidgetState createState() => _RoomEntranceWidgetState();
|
||
}
|
||
|
||
class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
|
||
with SingleTickerProviderStateMixin {
|
||
SVGAAnimationController? _animationController;
|
||
bool _isLoading = false;
|
||
bool _hasError = false;
|
||
String? _currentResource;
|
||
|
||
// 用于存储SVGAImage组件的key,以便刷新占位符
|
||
GlobalKey _svgaKey = GlobalKey();
|
||
|
||
// 队列管理器实例
|
||
final RoomEntranceQueueManager _queueManager = RoomEntranceQueueManager();
|
||
|
||
// 队列播放相关
|
||
VoidCallback? _queueCompletedCallback;
|
||
Function(String)? _queueErrorCallback;
|
||
|
||
// 动态数据
|
||
DynamicData? _currentDynamicData;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_animationController = SVGAAnimationController(vsync: this);
|
||
|
||
// 设置队列延迟
|
||
_queueManager.autoPlayDelay = widget.queueDelay;
|
||
|
||
// 设置当前播放器
|
||
_queueManager.setCurrentPlayer(this);
|
||
|
||
// 添加动画状态监听
|
||
_animationController?.addStatusListener((status) {
|
||
if (status == AnimationStatus.completed) {
|
||
// 如果是在队列播放中,调用队列完成回调
|
||
if (_queueCompletedCallback != null) {
|
||
_queueCompletedCallback!();
|
||
_queueCompletedCallback = null;
|
||
_queueErrorCallback = null;
|
||
}
|
||
}
|
||
});
|
||
|
||
// 如果需要自动开始队列,则检查队列
|
||
if (widget.autoStartQueue && widget.useQueue) {
|
||
// 延迟一小段时间,确保组件已经挂载
|
||
Future.delayed(Duration(milliseconds: 100), () {
|
||
_queueManager._startNextAnimation();
|
||
});
|
||
}
|
||
}
|
||
|
||
// 从队列播放(内部使用)
|
||
void playFromQueue({
|
||
required String resource,
|
||
DynamicData? dynamicData,
|
||
VoidCallback? onCompleted,
|
||
Function(String)? onError,
|
||
}) {
|
||
_queueCompletedCallback = onCompleted;
|
||
_queueErrorCallback = onError;
|
||
_currentDynamicData = dynamicData;
|
||
_currentResource = resource;
|
||
|
||
// 加载动画
|
||
_loadAnimation(
|
||
resource: resource,
|
||
dynamicData: dynamicData,
|
||
isFromQueue: true,
|
||
);
|
||
}
|
||
|
||
// 加载动画
|
||
Future<void> _loadAnimation({
|
||
required String resource,
|
||
DynamicData? dynamicData,
|
||
bool isFromQueue = false,
|
||
}) async {
|
||
if (widget.onStartLoading != null) {
|
||
widget.onStartLoading!();
|
||
}
|
||
|
||
setState(() {
|
||
_isLoading = true;
|
||
_hasError = false;
|
||
});
|
||
|
||
try {
|
||
final isNetworkResource = resource.startsWith('http');
|
||
MovieEntity? videoItem;
|
||
|
||
if (widget.useCache) {
|
||
videoItem = SCGiftVapSvgaManager().videoItemCache[resource];
|
||
}
|
||
|
||
if (videoItem == null) {
|
||
videoItem =
|
||
isNetworkResource
|
||
? await SVGAParser.shared.decodeFromURL(resource)
|
||
: await SVGAParser.shared.decodeFromAssets(resource);
|
||
videoItem.autorelease = false;
|
||
|
||
if (widget.useCache) {
|
||
SCGiftVapSvgaManager().videoItemCache[resource] = videoItem;
|
||
}
|
||
}
|
||
|
||
if (mounted) {
|
||
// 应用动态数据(文本、图片替换)
|
||
await _applyDynamicData(videoItem, dynamicData);
|
||
|
||
setState(() {
|
||
_animationController?.videoItem = videoItem;
|
||
_isLoading = false;
|
||
});
|
||
|
||
// 开始播放动画
|
||
_startAnimation();
|
||
|
||
if (widget.onFinishLoading != null) {
|
||
widget.onFinishLoading!();
|
||
}
|
||
}
|
||
} catch (e) {
|
||
if (mounted) {
|
||
setState(() {
|
||
_isLoading = false;
|
||
_hasError = true;
|
||
});
|
||
}
|
||
|
||
// 队列错误回调
|
||
if (isFromQueue && _queueErrorCallback != null) {
|
||
_queueErrorCallback!(e.toString());
|
||
_queueErrorCallback = null;
|
||
_queueCompletedCallback = null;
|
||
}
|
||
|
||
// 显示错误信息
|
||
SCTts.show("SVGA加载失败: ${e.toString()}");
|
||
}
|
||
}
|
||
|
||
// 应用动态数据(文本、图片替换)
|
||
Future<void> _applyDynamicData(
|
||
MovieEntity videoItem,
|
||
DynamicData? dynamicData,
|
||
) async {
|
||
if (dynamicData == null) return;
|
||
|
||
// 1. 应用文本替换
|
||
if (dynamicData.textReplacements != null) {
|
||
await _applyTextReplacements(videoItem, dynamicData.textReplacements!);
|
||
}
|
||
|
||
// 2. 应用图片替换(二进制数据)
|
||
if (dynamicData.imageReplacements != null) {
|
||
await _applyImageReplacements(videoItem, dynamicData.imageReplacements!);
|
||
}
|
||
|
||
// 3. 应用图片资源路径替换
|
||
if (dynamicData.imageAssetPaths != null) {
|
||
await _applyImageAssetReplacements(
|
||
videoItem,
|
||
dynamicData.imageAssetPaths!,
|
||
);
|
||
}
|
||
}
|
||
|
||
// 应用文本替换
|
||
Future<void> _applyTextReplacements(
|
||
MovieEntity videoItem,
|
||
Map<String, String> textReplacements,
|
||
) async {
|
||
// 这里需要根据具体的SVGA插件API来替换文本
|
||
// 示例代码,具体实现取决于插件支持
|
||
if (videoItem.dynamicItem != null) {
|
||
textReplacements.forEach((key, value) {
|
||
try {
|
||
// 根据插件API设置文本
|
||
// 注意:flutter_svga插件的API可能会有所不同
|
||
videoItem.dynamicItem!.setText(
|
||
TextPainter(
|
||
text: TextSpan(
|
||
text: "Hello, World!",
|
||
style: TextStyle(
|
||
fontSize: 28,
|
||
color: Colors.white,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
),
|
||
key,
|
||
);
|
||
} catch (e) {
|
||
print('设置文本占位符 $key 失败: $e');
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// 应用图片替换(二进制数据)
|
||
Future<void> _applyImageReplacements(
|
||
MovieEntity videoItem,
|
||
Map<String, Uint8List> imageReplacements,
|
||
) async {
|
||
// 检查是否支持图片替换
|
||
if (videoItem.images != null) {
|
||
imageReplacements.forEach((key, imageData) {
|
||
try {
|
||
// 替换图片数据
|
||
// 注意:flutter_svga插件的API可能会有所不同
|
||
if (videoItem.images!.containsKey(key)) {
|
||
videoItem.images![key] = imageData;
|
||
}
|
||
} catch (e) {
|
||
print('设置图片占位符 $key 失败: $e');
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// 应用图片资源路径替换
|
||
Future<void> _applyImageAssetReplacements(
|
||
MovieEntity videoItem,
|
||
Map<String, String> imageAssetPaths,
|
||
) async {
|
||
// 如果需要从assets加载图片,可以实现这里
|
||
// 这通常需要将assets图片转换为Uint8List
|
||
}
|
||
|
||
// 开始播放动画
|
||
void _startAnimation() {
|
||
_animationController?.reset();
|
||
|
||
// 根据循环次数设置播放方式
|
||
if (widget.loops == 0) {
|
||
_animationController?.repeat();
|
||
} else {
|
||
_animationController?.repeat(count: widget.loops);
|
||
}
|
||
}
|
||
|
||
// 播放指定资源(外部调用)
|
||
Future<void> playResource({
|
||
required String resource,
|
||
DynamicData? dynamicData,
|
||
}) async {
|
||
if (widget.useQueue) {
|
||
// 使用队列,添加到队列
|
||
final queueItem = AnimationQueueItem(
|
||
resource: resource,
|
||
dynamicData:
|
||
dynamicData != null
|
||
? {
|
||
'textReplacements': dynamicData.textReplacements,
|
||
'imageReplacements': dynamicData.imageReplacements,
|
||
'imageAssetPaths': dynamicData.imageAssetPaths,
|
||
}
|
||
: null,
|
||
);
|
||
|
||
return _queueManager.addToQueue(queueItem);
|
||
} else {
|
||
// 不使用队列,直接播放
|
||
return _loadAnimation(
|
||
resource: resource,
|
||
dynamicData: dynamicData,
|
||
isFromQueue: false,
|
||
);
|
||
}
|
||
}
|
||
|
||
// 暂停动画
|
||
void pause() {
|
||
_animationController?.stop();
|
||
}
|
||
|
||
// 停止动画
|
||
void stop() {
|
||
_animationController?.stop();
|
||
if (widget.clearsAfterStop) {
|
||
_animationController?.videoItem = null;
|
||
setState(() {
|
||
_currentResource = null;
|
||
});
|
||
}
|
||
}
|
||
|
||
// 重新加载当前动画
|
||
void reload() {
|
||
if (_currentResource != null) {
|
||
if (widget.useCache) {
|
||
SCGiftVapSvgaManager().videoItemCache.remove(_currentResource);
|
||
}
|
||
|
||
_loadAnimation(
|
||
resource: _currentResource!,
|
||
dynamicData: _currentDynamicData,
|
||
isFromQueue: false,
|
||
);
|
||
}
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
stop();
|
||
_animationController?.dispose();
|
||
// 从队列管理器中移除当前播放器
|
||
_queueManager.clearCurrentPlayer();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
// 显示加载状态
|
||
if (_isLoading) {
|
||
return Container(
|
||
width: widget.width,
|
||
height: widget.height,
|
||
child: Center(child: CircularProgressIndicator()),
|
||
);
|
||
}
|
||
|
||
// 显示错误状态
|
||
if (_hasError) {
|
||
return Container(
|
||
width: widget.width,
|
||
height: widget.height,
|
||
child: Center(child: Icon(Icons.error, color: Colors.red)),
|
||
);
|
||
}
|
||
|
||
// 显示空状态(没有动画时)
|
||
if (_animationController?.videoItem == null) {
|
||
return Container(
|
||
width: widget.width,
|
||
height: widget.height,
|
||
child: Center(
|
||
child: Text('等待动画...', style: TextStyle(color: Colors.grey)),
|
||
),
|
||
);
|
||
}
|
||
|
||
// 显示SVGA动画
|
||
return Container(
|
||
width: widget.width,
|
||
height: widget.height,
|
||
child: SVGAImage(
|
||
key: _svgaKey,
|
||
_animationController!,
|
||
fit: BoxFit.contain,
|
||
clearsAfterStop: widget.clearsAfterStop,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// 便捷使用的全局方法
|
||
class RoomEntranceHelper {
|
||
// 添加入场动画到队列
|
||
static Future<void> addEntranceAnimation({
|
||
required String resource,
|
||
Map<String, String>? textReplacements,
|
||
Map<String, Uint8List>? imageReplacements,
|
||
Map<String, String>? imageAssetPaths,
|
||
VoidCallback? onCompleted,
|
||
Function(String)? onError,
|
||
}) async {
|
||
final dynamicData = DynamicData(
|
||
textReplacements: textReplacements,
|
||
imageReplacements: imageReplacements,
|
||
imageAssetPaths: imageAssetPaths,
|
||
);
|
||
|
||
final queueItem = AnimationQueueItem(
|
||
resource: resource,
|
||
dynamicData: {
|
||
'textReplacements': dynamicData.textReplacements,
|
||
'imageReplacements': dynamicData.imageReplacements,
|
||
'imageAssetPaths': dynamicData.imageAssetPaths,
|
||
},
|
||
onCompleted: onCompleted,
|
||
onError: onError,
|
||
);
|
||
|
||
return RoomEntranceQueueManager().addToQueue(queueItem);
|
||
}
|
||
|
||
// 获取队列状态
|
||
static Map<String, dynamic> getQueueStatus() {
|
||
return RoomEntranceQueueManager().queueStatus;
|
||
}
|
||
|
||
// 清空队列
|
||
static void clearQueue() {
|
||
RoomEntranceQueueManager().clearQueue();
|
||
}
|
||
|
||
// 设置队列更新回调
|
||
static void setQueueUpdateCallback(void Function() callback) {
|
||
RoomEntranceQueueManager().onQueueUpdated = callback;
|
||
}
|
||
|
||
// 设置队列最大长度
|
||
static void setMaxQueueLength(int length) {
|
||
RoomEntranceQueueManager().maxQueueLength = length;
|
||
}
|
||
|
||
// 设置自动播放延迟
|
||
static void setAutoPlayDelay(int delayMs) {
|
||
RoomEntranceQueueManager().autoPlayDelay = delayMs;
|
||
}
|
||
}
|
||
|
||
// 使用示例
|
||
class RoomEntranceExample extends StatelessWidget {
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Column(
|
||
children: [
|
||
// 创建播放器(不需要初始资源)
|
||
RoomEntranceWidget(
|
||
width: 200,
|
||
height: 200,
|
||
useQueue: true,
|
||
autoStartQueue: true,
|
||
),
|
||
|
||
SizedBox(height: 20),
|
||
|
||
// 添加动画到队列的按钮
|
||
ElevatedButton(
|
||
onPressed: () {
|
||
RoomEntranceHelper.addEntranceAnimation(
|
||
resource: 'assets/animations/entrance.svga',
|
||
textReplacements: {'username': '张三', 'level': 'VIP 10'},
|
||
onCompleted: () {
|
||
print('入场动画播放完成');
|
||
},
|
||
);
|
||
},
|
||
child: Text('播放入场动画'),
|
||
),
|
||
|
||
// 添加另一个动画
|
||
ElevatedButton(
|
||
onPressed: () {
|
||
RoomEntranceHelper.addEntranceAnimation(
|
||
resource: 'https://example.com/animations/vip_entrance.svga',
|
||
textReplacements: {'username': '李四', 'gift_name': '超级火箭'},
|
||
);
|
||
},
|
||
child: Text('播放VIP入场动画'),
|
||
),
|
||
|
||
// 查看队列状态
|
||
ElevatedButton(
|
||
onPressed: () {
|
||
final status = RoomEntranceHelper.getQueueStatus();
|
||
print('队列状态: $status');
|
||
},
|
||
child: Text('查看队列状态'),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|