yumi-flutter/lib/shared/data_sources/sources/local/floating_screen_manager.dart
roxy bd6d35ee1b 🚀 UI
2026-05-14 17:10:53 +08:00

745 lines
21 KiB
Dart

import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/main.dart';
import 'package:provider/provider.dart';
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart';
import 'package:yumi/shared/tools/sc_entrance_vap_svga_manager.dart';
import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
import 'package:yumi/ui_kit/widgets/room/floating/floating_game_screen_widget.dart';
import 'package:yumi/ui_kit/widgets/room/floating/floating_gift_screen_widget.dart';
import 'package:yumi/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart';
import 'package:yumi/ui_kit/widgets/room/floating/floating_room_redenvelope_screen_widget.dart';
import 'package:yumi/ui_kit/widgets/room/floating/floating_room_rocket_screen_widget.dart';
import 'package:yumi/ui_kit/widgets/room/floating/floating_vip_entry_screen_widget.dart';
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
typedef FloatingScreenManager = OverlayManager;
class OverlayManager {
static const int _maxLowPerformanceCompensationQueueLength = 8;
static const int _maxDeferredRegionRedPacketCount = 12;
static const Duration _compensationDedupWindow = Duration(seconds: 3);
static const Duration _redPacketFloatingDedupWindow = Duration(minutes: 30);
final SCPriorityQueue<SCFloatingMessage> _messageQueue = SCPriorityQueue(
(a, b) => b.priority.compareTo(a.priority),
);
final List<SCFloatingMessage> _deferredRegionRedPacketMessages = [];
final Map<String, DateTime> _recentCompensationKeys = {};
final Map<String, DateTime> _recentFloatingKeys = {};
bool _isPlaying = false;
OverlayEntry? _currentOverlayEntry;
SCFloatingMessage? _currentMessage;
bool _homeRootTabsVisible = false;
int _suppressedCount = 0;
bool _isProcessing = false;
bool _isDisposed = false;
static final OverlayManager _instance = OverlayManager._internal();
factory OverlayManager() => _instance;
OverlayManager._internal();
void addMessage(SCFloatingMessage message) {
if (_isDisposed || _isSuppressed) return;
if (!SCGlobalConfig.isFloatingBroadcastClosed) {
if (_isRecentFloatingDuplicate(message)) {
return;
}
if (message.type == 4) {}
_rememberFloatingMessage(message);
_enqueueMessage(
message,
debugLabel: 'floating_message_type_${message.type ?? -1}',
);
} else {
_removeActiveMessage(scheduleNext: false);
_messageQueue.clear();
_deferredRegionRedPacketMessages.clear();
_isPlaying = false;
_isProcessing = false;
_isDisposed = false;
}
}
void addLowPerformanceCompensationMessage(
SCFloatingMessage message, {
String? dedupKey,
}) {
if (_isDisposed ||
_isSuppressed ||
!SCGlobalConfig.isLowPerformanceDevice ||
SCGlobalConfig.isFloatingBroadcastClosed) {
return;
}
final key = dedupKey ?? _compensationKeyFor(message);
if (_isRecentCompensationDuplicate(key)) {
return;
}
_rememberCompensationKey(key);
_trimLowPerformanceCompensationQueue();
_enqueueMessage(
message,
debugLabel: 'low_performance_compensation_${message.type ?? -1}',
);
}
void _enqueueMessage(
SCFloatingMessage message, {
required String debugLabel,
}) {
unawaited(_warmMessageImages(message));
SCRoomEffectScheduler().scheduleDeferredEffect(
debugLabel: debugLabel,
action: () {
if (_isDisposed || _isSuppressed) {
return;
}
_messageQueue.add(message);
_safeScheduleNext();
},
);
}
bool _isRecentCompensationDuplicate(String key) {
_pruneRecentCompensationKeys();
final recentTime = _recentCompensationKeys[key];
if (recentTime == null) {
return false;
}
return DateTime.now().difference(recentTime) < _compensationDedupWindow;
}
void _rememberCompensationKey(String key) {
_recentCompensationKeys[key] = DateTime.now();
_pruneRecentCompensationKeys();
}
void _pruneRecentCompensationKeys() {
final now = DateTime.now();
_recentCompensationKeys.removeWhere(
(_, time) => now.difference(time) > _compensationDedupWindow,
);
}
bool _isRecentFloatingDuplicate(SCFloatingMessage message) {
_pruneRecentFloatingKeys();
final key = _floatingDedupKeyFor(message);
if (key == null) {
return false;
}
final recentTime = _recentFloatingKeys[key];
if (recentTime == null) {
return false;
}
return DateTime.now().difference(recentTime) <
_redPacketFloatingDedupWindow;
}
void _rememberFloatingMessage(SCFloatingMessage message) {
final key = _floatingDedupKeyFor(message);
if (key == null) {
return;
}
_recentFloatingKeys[key] = DateTime.now();
_pruneRecentFloatingKeys();
}
void _pruneRecentFloatingKeys() {
final now = DateTime.now();
_recentFloatingKeys.removeWhere(
(_, time) => now.difference(time) > _redPacketFloatingDedupWindow,
);
}
String? _floatingDedupKeyFor(SCFloatingMessage message) {
if (message.type == 3) {
final launchNo = message.giftId?.trim() ?? '';
if (launchNo.isEmpty) {
return null;
}
return 'room_rocket_launch|$launchNo';
}
if (message.type == 4) {
final packetId = message.toUserId?.trim() ?? '';
if (packetId.isEmpty) {
return null;
}
return 'room_red_packet|$packetId';
}
return null;
}
String _compensationKeyFor(SCFloatingMessage message) {
return [
message.type,
message.roomId,
message.userId,
message.toUserId,
message.giftId,
message.number,
message.giftUrl,
].join('|');
}
void _trimLowPerformanceCompensationQueue() {
if (_messageQueue.length < _maxLowPerformanceCompensationQueueLength) {
return;
}
final retained =
_messageQueue.unorderedElements.cast<SCFloatingMessage>().toList()
..sort((a, b) => b.priority.compareTo(a.priority));
_messageQueue.clear();
for (final message in retained.take(
_maxLowPerformanceCompensationQueueLength - 1,
)) {
_messageQueue.add(message);
}
}
Future<void> _warmMessageImages(SCFloatingMessage message) async {
unawaited(
warmImageResource(
message.userAvatarUrl ?? "",
logicalWidth: 80,
logicalHeight: 80,
),
);
unawaited(
warmImageResource(
message.toUserAvatarUrl ?? "",
logicalWidth: 80,
logicalHeight: 80,
),
);
unawaited(
warmImageResource(
message.giftUrl ?? "",
logicalWidth: message.type == 5 ? 350 : 96,
logicalHeight: message.type == 5 ? 84 : 96,
),
);
if (message.type == 0) {
unawaited(_warmLuckyGiftBannerSvga());
}
}
Future<void> _warmLuckyGiftBannerSvga() async {
try {
await SCSvgaAssetWidget.preloadAsset(
FloatingLuckGiftScreenWidget.backgroundSvgaAssetPath,
);
} catch (error) {}
}
void _safeScheduleNext() {
if (_isSuppressed) return;
if (_isProcessing || _isPlaying || _messageQueue.isEmpty) return;
_isProcessing = true;
try {
_scheduleNext();
} finally {
_isProcessing = false;
}
}
void _scheduleNext() {
if (_isPlaying || _messageQueue.isEmpty || _isDisposed || _isSuppressed) {
return;
}
try {
final context = navigatorKey.currentState?.context;
if (context == null || !context.mounted) return;
// 安全地获取第一个消息
if (_messageQueue.isEmpty) return;
final messageToProcess = _messageQueue.first;
if (messageToProcess == null) {
_messageQueue.removeFirst();
_safeScheduleNext();
return;
}
if (!_shouldDisplayMessage(context, messageToProcess)) {
_messageQueue.removeFirst();
if (_shouldDeferRegionRedPacketMessage(context, messageToProcess)) {
_deferRegionRedPacketMessage(context, messageToProcess);
_safeScheduleNext();
return;
}
_logRegionRedPacketDrop(context, messageToProcess);
_safeScheduleNext();
return;
}
// 安全地移除并播放消息
final messageToPlay = _messageQueue.removeFirst();
_playMessage(messageToPlay);
} catch (e) {
_isPlaying = false;
_safeScheduleNext();
}
}
void _playMessage(SCFloatingMessage message) {
_isPlaying = true;
_currentMessage = message;
final context = navigatorKey.currentState?.context;
if (context == null || !context.mounted) {
_isPlaying = false;
_currentMessage = null;
_safeScheduleNext();
return;
}
_currentOverlayEntry = OverlayEntry(
builder:
(_) => Align(
alignment: AlignmentDirectional.topStart,
child: Transform.translate(
offset: Offset(0, 70.w),
child: RepaintBoundary(child: _buildScreenWidget(message)),
),
),
);
Overlay.of(context).insert(_currentOverlayEntry!);
}
Widget _buildScreenWidget(SCFloatingMessage message) {
bool completed = false;
void onComplete() {
if (completed) return;
completed = true;
try {
_currentOverlayEntry?.remove();
_currentOverlayEntry = null;
_isPlaying = false;
_currentMessage = null;
_safeScheduleNext();
} catch (e) {
_isPlaying = false;
_currentMessage = null;
_safeScheduleNext();
}
}
switch (message.type) {
case 0:
return FloatingLuckGiftScreenWidget(
message: message,
onAnimationCompleted: onComplete,
);
case 1:
//房间礼物
return FloatingGiftScreenWidget(
message: message,
onAnimationCompleted: onComplete,
);
case 2:
//游戏中奖
return FloatingGameScreenWidget(
message: message,
onAnimationCompleted: onComplete,
);
case 3:
//火箭
return FloatingRoomRocketScreenWidget(
message: message,
onAnimationCompleted: onComplete,
);
case 4:
//红包
return FloatingRoomRedenvelopeScreenWidget(
message: message,
onAnimationCompleted: onComplete,
);
case 5:
//VIP进房飘窗
return FloatingVipEntryScreenWidget(
message: message,
onAnimationCompleted: onComplete,
);
default:
onComplete();
return Container();
}
}
void activate() {
_isDisposed = false;
_loadSavedDisplayPreference();
}
void _loadSavedDisplayPreference() {
final account = AccountStorage().getCurrentUser()?.userProfile?.account;
final savedFloatingBroadcastScope = DataPersistence.getString(
SCGlobalConfig.floatingBroadcastScopeStorageKey(account),
);
if (savedFloatingBroadcastScope.isNotEmpty) {
final scope = SCGlobalConfig.clampFloatingBroadcastScope(
savedFloatingBroadcastScope,
);
SCGlobalConfig.floatingBroadcastScope = scope;
return;
}
final defaultEffectsEnabled = !SCGlobalConfig.isLowPerformanceDevice;
final isLegacyFloatingAnimationInGlobal = DataPersistence.getBool(
SCGlobalConfig.legacyFloatingAnimationInGlobalStorageKey(account),
defaultValue: defaultEffectsEnabled,
);
SCGlobalConfig.floatingBroadcastScope =
SCGlobalConfig.clampVisualEffectPreference(
isLegacyFloatingAnimationInGlobal,
)
? SCGlobalConfig.floatingBroadcastScopeAll
: SCGlobalConfig.floatingBroadcastScopeOff;
}
void setHomeRootTabsVisible(bool visible) {
if (_homeRootTabsVisible == visible) {
return;
}
_homeRootTabsVisible = visible;
if (!visible) {
_removeActiveRegionBroadcastMessage();
_removeRegionBroadcastMessages();
return;
}
_restoreDeferredRegionRedPacketMessages();
_safeScheduleNext();
}
void beginSuppressFloatingScreens({String reason = 'unknown'}) {
_suppressedCount += 1;
_removeActiveMessage(scheduleNext: false);
_messageQueue.clear();
_isPlaying = false;
_isProcessing = false;
}
void endSuppressFloatingScreens({String reason = 'unknown'}) {
if (_suppressedCount > 0) {
_suppressedCount -= 1;
}
if (!_isSuppressed) {
_safeScheduleNext();
}
}
void refreshDisplayPreference() {
if (_isDisposed) {
return;
}
if (SCGlobalConfig.isFloatingBroadcastClosed) {
_removeActiveMessage(scheduleNext: false);
_messageQueue.clear();
_deferredRegionRedPacketMessages.clear();
return;
}
_restoreDeferredRegionRedPacketMessages();
final context = navigatorKey.currentState?.context;
final activeMessage = _currentMessage;
if (context != null &&
context.mounted &&
activeMessage != null &&
!_shouldDisplayMessage(context, activeMessage)) {
_removeActiveMessage();
return;
}
_safeScheduleNext();
}
void dispose() {
_isDisposed = true;
_homeRootTabsVisible = false;
_suppressedCount = 0;
_currentOverlayEntry?.remove();
_currentOverlayEntry = null;
_currentMessage = null;
_messageQueue.clear();
_deferredRegionRedPacketMessages.clear();
_recentCompensationKeys.clear();
_isPlaying = false;
_isProcessing = false;
}
void removeRoom({String? roomId}) {
final normalizedRoomId = roomId?.trim() ?? '';
_removeActiveRoomMessage(
roomId: normalizedRoomId,
includeRedPacket: normalizedRoomId.isNotEmpty,
);
_removeMessagesByType(1);
_removeMessagesByType(0);
_removeMessagesByType(5);
if (normalizedRoomId.isNotEmpty) {
_removeMessagesByType(4, roomId: normalizedRoomId);
}
}
// 辅助方法:移除特定类型的消息
void _removeMessagesByType(int type, {String? roomId}) {
final normalizedRoomId = roomId?.trim() ?? '';
// 由于 PriorityQueue 没有直接的方法来移除特定元素,
// 我们需要重建队列,排除指定类型的消息
final newQueue = SCPriorityQueue<SCFloatingMessage>(
(a, b) => b.priority.compareTo(a.priority),
);
// 遍历原始队列,只添加非指定类型的消息
while (_messageQueue.isNotEmpty) {
final message = _messageQueue.removeFirst();
final matchesType = message.type == type;
final matchesRoom =
normalizedRoomId.isEmpty ||
(message.roomId ?? "").trim() == normalizedRoomId;
if (!matchesType || !matchesRoom) {
newQueue.add(message);
}
}
// 将新队列赋值给原队列
while (newQueue.isNotEmpty) {
_messageQueue.add(newQueue.removeFirst());
}
}
// 辅助方法:获取队列信息
int get queueLength => _messageQueue.length;
bool get isPlaying => _isPlaying;
bool get _isSuppressed => _suppressedCount > 0;
bool _isRegionRedPacketMessage(SCFloatingMessage message) {
return message.type == 4 && message.isRegionBroadcast;
}
bool _shouldDeferRegionRedPacketMessage(
BuildContext context,
SCFloatingMessage message,
) {
if (!_isRegionRedPacketMessage(message) ||
SCGlobalConfig.isFloatingBroadcastClosed) {
return false;
}
return !_shouldDisplayRegionBroadcastMessage(context, message);
}
void _deferRegionRedPacketMessage(
BuildContext context,
SCFloatingMessage message,
) {
final key = _floatingDedupKeyFor(message);
if (key != null &&
_deferredRegionRedPacketMessages.any(
(item) => _floatingDedupKeyFor(item) == key,
)) {
return;
}
while (_deferredRegionRedPacketMessages.length >=
_maxDeferredRegionRedPacketCount) {
_deferredRegionRedPacketMessages.removeAt(0);
}
_deferredRegionRedPacketMessages.add(message);
}
void _restoreDeferredRegionRedPacketMessages() {
if (_deferredRegionRedPacketMessages.isEmpty) {
return;
}
final messages = List<SCFloatingMessage>.from(
_deferredRegionRedPacketMessages,
);
_deferredRegionRedPacketMessages.clear();
for (final message in messages) {
_messageQueue.add(message);
}
}
void _logRegionRedPacketDrop(
BuildContext context,
SCFloatingMessage message,
) {
if (message.type != 4) {
return;
}
}
String _displayStateForLog(BuildContext context) {
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
context,
listen: false,
);
final currentRoomId =
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
return 'homeVisible=$_homeRootTabsVisible '
'roomVisible=${rtcProvider.shouldShowRoomVisualEffects} '
'currentRoomId=$currentRoomId '
'scope=${SCGlobalConfig.floatingBroadcastScope}';
}
bool _shouldDisplayMessage(BuildContext context, SCFloatingMessage? message) {
if (message == null) {
return false;
}
if (SCGlobalConfig.isFloatingBroadcastClosed) {
return false;
}
if (message.isRegionBroadcast) {
return _shouldDisplayRegionBroadcastMessage(context, message);
}
if (message.type == 4) {
return _isCurrentRoomFloatingMessage(context, message);
}
if (SCGlobalConfig.isFloatingBroadcastRoomOnly) {
return _isCurrentRoomFloatingMessage(context, message);
}
if (message.type != 0 && message.type != 1 && message.type != 5) {
return true;
}
return _isCurrentRoomFloatingMessage(context, message);
}
bool _isCurrentRoomFloatingMessage(
BuildContext context,
SCFloatingMessage message,
) {
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
context,
listen: false,
);
if (!rtcProvider.shouldShowRoomVisualEffects ||
!rtcProvider.isVoiceRoomRouteVisible) {
return false;
}
final currentRoomId =
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
final messageRoomId = (message.roomId ?? "").trim();
if (currentRoomId.isEmpty || messageRoomId.isEmpty) {
return false;
}
return currentRoomId == messageRoomId;
}
bool _shouldDisplayRegionBroadcastMessage(
BuildContext context,
SCFloatingMessage message,
) {
if (SCGlobalConfig.isFloatingBroadcastClosed) {
return false;
}
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
context,
listen: false,
);
final currentRoomId =
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
final messageRoomId = (message.roomId ?? "").trim();
final isCurrentVisibleRoom =
rtcProvider.shouldShowRoomVisualEffects &&
rtcProvider.isVoiceRoomRouteVisible &&
currentRoomId.isNotEmpty &&
messageRoomId.isNotEmpty &&
currentRoomId == messageRoomId;
if (isCurrentVisibleRoom) {
// 区域红包在当前房间仍然需要展示;区域礼物当前房间由 RTM 层跳过,避免和房间 IM 礼物飘屏重复。
return message.type == 4;
}
final isCurrentRoomMessage =
currentRoomId.isNotEmpty &&
messageRoomId.isNotEmpty &&
currentRoomId == messageRoomId;
if (message.type == 4 && isCurrentRoomMessage) {
return false;
}
if (message.type == 4 && rtcProvider.shouldShowRoomVisualEffects) {
return true;
}
if (message.type == 4) {
return _homeRootTabsVisible;
}
if (SCGlobalConfig.isFloatingBroadcastRoomOnly) {
return false;
}
if (rtcProvider.shouldShowRoomVisualEffects) {
return false;
}
return SCGlobalConfig.isFloatingBroadcastAll && _homeRootTabsVisible;
}
void _removeRegionBroadcastMessages() {
final newQueue = SCPriorityQueue<SCFloatingMessage>(
(a, b) => b.priority.compareTo(a.priority),
);
while (_messageQueue.isNotEmpty) {
final message = _messageQueue.removeFirst();
if (!message.isRegionBroadcast || _isRegionRedPacketMessage(message)) {
newQueue.add(message);
}
}
while (newQueue.isNotEmpty) {
_messageQueue.add(newQueue.removeFirst());
}
}
void _removeActiveRegionBroadcastMessage() {
final activeMessage = _currentMessage;
if (activeMessage == null || !activeMessage.isRegionBroadcast) {
return;
}
_currentOverlayEntry?.remove();
_currentOverlayEntry = null;
_currentMessage = null;
_isPlaying = false;
_safeScheduleNext();
}
void _removeActiveRoomMessage({
String? roomId,
bool includeRedPacket = false,
}) {
final activeMessage = _currentMessage;
final normalizedRoomId = roomId?.trim() ?? '';
if (activeMessage == null ||
(activeMessage.type != 0 &&
activeMessage.type != 1 &&
activeMessage.type != 5 &&
!(includeRedPacket && activeMessage.type == 4))) {
return;
}
if (normalizedRoomId.isNotEmpty &&
(activeMessage.roomId ?? "").trim() != normalizedRoomId) {
return;
}
_removeActiveMessage();
}
void _removeActiveMessage({bool scheduleNext = true}) {
_currentOverlayEntry?.remove();
_currentOverlayEntry = null;
_currentMessage = null;
_isPlaying = false;
if (scheduleNext) {
_safeScheduleNext();
}
}
}