import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/ui_kit/components/text/sc_text.dart'; import 'package:provider/provider.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/shared/business_logic/models/res/login_res.dart'; import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; import 'package:yumi/shared/tools/sc_network_image_utils.dart'; import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart'; import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; class RoomAnimationQueueScreen extends StatefulWidget { const RoomAnimationQueueScreen({super.key}); @override State createState() => _RoomAnimationQueueScreenState(); } class _RoomAnimationQueueScreenState extends State { static const int _maxAnimationQueueLength = 12; final List _animationQueue = []; bool _isQueueProcessing = false; final Map> _animationKeys = {}; @override void initState() { super.initState(); Provider.of(context, listen: false).msgUserJoinListener = _msgUserJoinListener; } _msgUserJoinListener(Msg msg) { Future.delayed(Duration(milliseconds: 550), () { if (!_effectsEnabled) { return; } _enqueueVipEntryFloatingMessage(msg); unawaited( warmImageResource( msg.user?.userAvatar ?? "", logicalWidth: 28.w, logicalHeight: 28.w, ), ); SCRoomEffectScheduler().scheduleDeferredEffect( debugLabel: 'room_entrance_join', action: () { if (!_effectsEnabled || !mounted) { return; } _addToQueue(msg); }, ); }); } void _enqueueVipEntryFloatingMessage(Msg msg) { if (SCGlobalConfig.isLowPerformanceDevice && ((msg.user?.getMountains()?.sourceUrl ?? "").trim().isNotEmpty)) { return; } final floatPicture = msg.user?.getFloatPicture(); final floatPictureUrl = _resolveVipFloatPictureUrl(floatPicture); final floatPictureFallbackUrl = _resolveVipFloatPictureFallbackUrl( floatPicture, ); if (floatPictureUrl.isEmpty) { return; } final roomId = Provider.of( context, listen: false, ).currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ""; if (roomId.isEmpty) { return; } final isLowPerformance = SCGlobalConfig.isLowPerformanceDevice; final displayUrl = isLowPerformance && floatPictureFallbackUrl.isNotEmpty ? floatPictureFallbackUrl : floatPictureUrl; final floatingMessage = SCFloatingMessage( type: 5, userId: msg.user?.id, roomId: roomId, userAvatarUrl: msg.user?.userAvatar, userName: msg.user?.userNickname, giftUrl: displayUrl, fallbackUrl: floatPictureFallbackUrl, priority: 900, ); if (isLowPerformance) { OverlayManager().addLowPerformanceCompensationMessage( floatingMessage, dedupKey: [ "entry_float", roomId, msg.user?.id ?? "", floatPicture?.id ?? "", ].join("|"), ); return; } OverlayManager().addMessage(floatingMessage); } String _resolveVipFloatPictureUrl(PropsResources? resource) { return _firstNonBlank([ resource?.sourceUrl, resource?.roomSendCoverUrl, resource?.roomOpenedUrl, resource?.cover, resource?.roomNotOpenedUrl, ]); } String _resolveVipFloatPictureFallbackUrl(PropsResources? resource) { return _firstNonBlank([ resource?.roomSendCoverUrl, resource?.roomOpenedUrl, resource?.cover, resource?.roomNotOpenedUrl, ]); } String _firstNonBlank(Iterable values) { for (final value in values) { final text = value?.trim(); if (text != null && text.isNotEmpty) { return text; } } return ""; } void _addToQueue(Msg msg) { if (!_effectsEnabled || !mounted) { return; } bool shouldStartProcessing = false; setState(() { final taskId = DateTime.now().millisecondsSinceEpoch; final animationKey = GlobalKey<_RoomEntranceAnimationState>(); _trimQueueOverflow(); _animationKeys[taskId] = animationKey; final task = AnimationTask( id: taskId, msg: msg, onComplete: () { if (_animationQueue.isNotEmpty && _animationQueue.first.id == taskId) { setState(() { _animationQueue.removeAt(0); _animationKeys.remove(taskId); }); if (_animationQueue.isNotEmpty) { _startNextAnimation(); } else { setState(() => _isQueueProcessing = false); } } }, ); _animationQueue.add(task); if (!_isQueueProcessing && _animationQueue.isNotEmpty) { _isQueueProcessing = true; shouldStartProcessing = true; } }); if (shouldStartProcessing) { _startNextAnimation(); } } void _trimQueueOverflow() { while (_animationQueue.length >= _maxAnimationQueueLength) { final removeIndex = _isQueueProcessing && _animationQueue.length > 1 ? 1 : 0; final removedTask = _animationQueue.removeAt(removeIndex); _animationKeys.remove(removedTask.id); } } void _startNextAnimation({int retryCount = 0}) { if (_animationQueue.isEmpty) return; SchedulerBinding.instance.addPostFrameCallback((_) { if (_animationQueue.isNotEmpty) { final task = _animationQueue.first; final key = _animationKeys[task.id]; if (key?.currentState != null) { key!.currentState!._startAnimation(); } else if (retryCount < 3) { // 有限次重试,避免无限循环 Future.delayed(const Duration(milliseconds: 50), () { _startNextAnimation(retryCount: retryCount + 1); }); } else { // 重试多次失败后,跳过当前动画 task.onComplete(); } } }); } void _clearQueue() { if (!mounted) { _animationQueue.clear(); _animationKeys.clear(); _isQueueProcessing = false; return; } setState(() { _animationQueue.clear(); _animationKeys.clear(); _isQueueProcessing = false; }); } bool get _effectsEnabled => SCGlobalConfig.isEntryVehicleAnimation && Provider.of( context, listen: false, ).shouldShowRoomVisualEffects; @override void dispose() { final rtmProvider = Provider.of(context, listen: false); if (rtmProvider.msgUserJoinListener == _msgUserJoinListener) { rtmProvider.msgUserJoinListener = null; } _animationQueue.clear(); _animationKeys.clear(); _isQueueProcessing = false; super.dispose(); } @override Widget build(BuildContext context) { final effectsEnabled = context.select( (rtcProvider) => SCGlobalConfig.isEntryVehicleAnimation && rtcProvider.shouldShowRoomVisualEffects, ); if (!effectsEnabled) { if (_animationQueue.isNotEmpty || _isQueueProcessing) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { _clearQueue(); } }); } return const SizedBox.shrink(); } return Padding( padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Stack( children: [ if (_animationQueue.isNotEmpty) RoomEntranceAnimation( key: _animationKeys[_animationQueue.first.id], msg: _animationQueue.first.msg, onComplete: _animationQueue.first.onComplete, ), ], ), ], ), ); } } class AnimationTask { final int id; final Msg msg; final VoidCallback onComplete; AnimationTask({ required this.id, required this.msg, required this.onComplete, }); } class RoomEntranceAnimation extends StatefulWidget { final VoidCallback onComplete; final Msg msg; const RoomEntranceAnimation({ super.key, required this.onComplete, required this.msg, }); @override State createState() => _RoomEntranceAnimationState(); } class _RoomEntranceAnimationState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _offsetAnimation; bool _isAnimating = false; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 4), vsync: this, ); _offsetAnimation = TweenSequence([ TweenSequenceItem( tween: Tween( begin: const Offset(1.0, 0.0), end: const Offset(0.0, 0.0), ).chain(CurveTween(curve: Curves.easeOut)), weight: 40.0, ), TweenSequenceItem( tween: ConstantTween(const Offset(0.0, 0.0)), weight: 20.0, ), TweenSequenceItem( tween: Tween( begin: const Offset(0.0, 0.0), end: const Offset(-1.0, 0.0), ).chain(CurveTween(curve: Curves.easeIn)), weight: 40.0, ), ]).animate(_controller); _controller.addStatusListener((status) { if (status == AnimationStatus.completed) { setState(() => _isAnimating = false); widget.onComplete(); } }); } void _startAnimation() { if (_isAnimating) return; setState(() => _isAnimating = true); _controller.reset(); _controller.forward(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return RepaintBoundary( child: SlideTransition( position: _offsetAnimation, child: Container( width: ScreenUtil().screenWidth * 0.8, height: 37.w, decoration: BoxDecoration( image: DecorationImage( image: AssetImage(getEntranceBg("")), fit: BoxFit.fill, ), ), child: Row( textDirection: TextDirection.ltr, children: [ Padding( padding: EdgeInsets.all(2.w), child: netImage( url: widget.msg.user?.userAvatar ?? "", width: 28.w, height: 28.w, shape: BoxShape.circle, ), ), SizedBox(width: 5.w), Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 2.w), socialchatNickNameText( maxWidth: 150.w, widget.msg.user?.userNickname ?? "", fontSize: 12.sp, type: "", needScroll: (widget.msg.user?.userNickname?.characters.length ?? 0) > 15, ), text( SCAppLocalizations.of(context)!.enterTheRoom, fontSize: 12.sp, lineHeight: 0.9, textColor: Colors.white, ), ], ), ], ), ), ), ); } String getEntranceBg(String type) { return "sc_images/room/entrance/sc_icon_room_entrance_no_vip_bg.png"; } }