修复一些已知bug

This commit is contained in:
NIGGER SLAYER 2026-04-21 13:44:03 +08:00
parent 7c7b205ffb
commit 1b0dd6f3ea
17 changed files with 1709 additions and 946 deletions

View File

@ -231,7 +231,7 @@ class _SeatRenderSnapshot {
}); });
factory _SeatRenderSnapshot.fromProvider(RtcProvider provider, num index) { factory _SeatRenderSnapshot.fromProvider(RtcProvider provider, num index) {
final roomSeat = provider.roomWheatMap[index]; final roomSeat = provider.micAtIndexForDisplay(index);
final user = roomSeat?.user; final user = roomSeat?.user;
return _SeatRenderSnapshot( return _SeatRenderSnapshot(
isExitingCurrentVoiceRoomSession: isExitingCurrentVoiceRoomSession:
@ -398,8 +398,9 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<RtcProvider, _SeatEmojiSnapshot>( return Selector<RtcProvider, _SeatEmojiSnapshot>(
selector: selector:
(context, provider) => (context, provider) => _SeatEmojiSnapshot.fromMic(
_SeatEmojiSnapshot.fromMic(provider.roomWheatMap[widget.index]), provider.micAtIndexForDisplay(widget.index),
),
builder: ( builder: (
BuildContext context, BuildContext context,
_SeatEmojiSnapshot snapshot, _SeatEmojiSnapshot snapshot,

View File

@ -14,7 +14,9 @@ import 'package:yumi/services/gift/gift_animation_manager.dart';
import 'package:yumi/services/gift/gift_system_manager.dart'; import 'package:yumi/services/gift/gift_system_manager.dart';
import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
import 'package:yumi/shared/tools/sc_path_utils.dart'; import 'package:yumi/shared/tools/sc_path_utils.dart';
import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart';
import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart'; import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart';
import 'package:yumi/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart'; import 'package:yumi/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart';
import 'package:yumi/ui_kit/widgets/room/anim/room_entrance_screen.dart'; import 'package:yumi/ui_kit/widgets/room/anim/room_entrance_screen.dart';
@ -138,6 +140,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
RoomEntranceHelper.clearQueue(); RoomEntranceHelper.clearQueue();
_clearLuckyGiftComboSessions(); _clearLuckyGiftComboSessions();
_giftSeatFlightController.clear(); _giftSeatFlightController.clear();
SCRoomEffectScheduler().clearDeferredTasks(reason: 'voice_room_suspend');
SCGiftVapSvgaManager().stopPlayback(); SCGiftVapSvgaManager().stopPlayback();
} }
@ -355,6 +358,21 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
giftModel.sendUserPic = msg.user?.userAvatar ?? ""; giftModel.sendUserPic = msg.user?.userAvatar ?? "";
giftModel.giftPic = msg.gift?.giftPhoto ?? ""; giftModel.giftPic = msg.gift?.giftPhoto ?? "";
giftModel.giftCount = msg.number ?? 0; giftModel.giftCount = msg.number ?? 0;
giftModel.giftCountStepUnit = _resolveGiftCountStepUnit(msg);
unawaited(
warmImageResource(
giftModel.sendUserPic,
logicalWidth: 26.w,
logicalHeight: 26.w,
),
);
unawaited(
warmImageResource(
giftModel.giftPic,
logicalWidth: 34.w,
logicalHeight: 34.w,
),
);
Provider.of<GiftAnimationManager>( Provider.of<GiftAnimationManager>(
context, context,
listen: false, listen: false,
@ -398,6 +416,20 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
giftModel.rewardAmount = awardAmount; giftModel.rewardAmount = awardAmount;
giftModel.showLuckyRewardFrame = true; giftModel.showLuckyRewardFrame = true;
giftModel.rewardAmountText = _formatLuckyRewardAmount(awardAmount); giftModel.rewardAmountText = _formatLuckyRewardAmount(awardAmount);
unawaited(
warmImageResource(
giftModel.sendUserPic,
logicalWidth: 26.w,
logicalHeight: 26.w,
),
);
unawaited(
warmImageResource(
giftModel.giftPic,
logicalWidth: 34.w,
logicalHeight: 34.w,
),
);
Provider.of<GiftAnimationManager>( Provider.of<GiftAnimationManager>(
context, context,
listen: false, listen: false,
@ -443,11 +475,13 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
); );
session.endTimer?.cancel(); session.endTimer?.cancel();
session.clearQueueTimer?.cancel(); session.clearQueueTimer?.cancel();
final previousTotal = session.totalCount;
session.totalCount += quantity; session.totalCount += quantity;
final highestMilestone = final highestMilestone =
SocialChatGiftSystemManager.resolveHighestReachedComboMilestone( SocialChatGiftSystemManager.resolveHighestCrossedComboEffectMilestone(
session.totalCount, previousCount: previousTotal,
currentCount: session.totalCount,
); );
if (highestMilestone != null && if (highestMilestone != null &&
highestMilestone > session.highestPlayedMilestone && highestMilestone > session.highestPlayedMilestone &&
@ -527,12 +561,28 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
return math.max(msg.customAnimationCount ?? 1, 1); return math.max(msg.customAnimationCount ?? 1, 1);
} }
num _resolveGiftCountStepUnit(Msg msg) {
final quantity = msg.number ?? 0;
final clickCount = math.max(msg.customAnimationCount ?? 1, 1);
if (quantity <= 0) {
return 1;
}
final stepUnit = quantity / clickCount;
if (stepUnit <= 0) {
return quantity;
}
return stepUnit;
}
void _enqueueTrackedSeatFlightAnimations({ void _enqueueTrackedSeatFlightAnimations({
required String sessionKey, required String sessionKey,
required String giftPhoto, required String giftPhoto,
required String targetUserId, required String targetUserId,
required int animationCount, required int animationCount,
}) { }) {
unawaited(
warmImageResource(giftPhoto, logicalWidth: 96.w, logicalHeight: 96.w),
);
final normalizedAnimationCount = math.max(animationCount, 1); final normalizedAnimationCount = math.max(animationCount, 1);
final cappedAnimationCount = math.min( final cappedAnimationCount = math.min(
normalizedAnimationCount, normalizedAnimationCount,

View File

@ -48,7 +48,7 @@ typedef RtcProvider = RealTimeCommunicationManager;
class RealTimeCommunicationManager extends ChangeNotifier { class RealTimeCommunicationManager extends ChangeNotifier {
static const Duration _micListPollingInterval = Duration(seconds: 2); static const Duration _micListPollingInterval = Duration(seconds: 2);
static const Duration _onlineUsersPollingInterval = Duration(seconds: 3); static const Duration _onlineUsersPollingInterval = Duration(seconds: 3);
static const Duration _selfMicSwitchGracePeriod = Duration(seconds: 4); static const Duration _selfMicStateGracePeriod = Duration(seconds: 4);
static const Duration _giftTriggeredMicRefreshMinInterval = Duration( static const Duration _giftTriggeredMicRefreshMinInterval = Duration(
milliseconds: 900, milliseconds: 900,
); );
@ -66,6 +66,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
num? _preferredSelfMicIndex; num? _preferredSelfMicIndex;
num? _pendingSelfMicSourceIndex; num? _pendingSelfMicSourceIndex;
int? _pendingSelfMicSwitchGuardUntilMs; int? _pendingSelfMicSwitchGuardUntilMs;
num? _pendingSelfMicReleaseIndex;
int? _pendingSelfMicReleaseGuardUntilMs;
ClientRoleType? _lastAppliedClientRole; ClientRoleType? _lastAppliedClientRole;
bool? _lastAppliedLocalAudioMuted; bool? _lastAppliedLocalAudioMuted;
bool? _lastScheduledVoiceLiveOnMic; bool? _lastScheduledVoiceLiveOnMic;
@ -285,10 +287,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
required num sourceIndex, required num sourceIndex,
required num targetIndex, required num targetIndex,
}) { }) {
_clearSelfMicReleaseGuard();
_preferredSelfMicIndex = targetIndex; _preferredSelfMicIndex = targetIndex;
_pendingSelfMicSourceIndex = sourceIndex; _pendingSelfMicSourceIndex = sourceIndex;
_pendingSelfMicSwitchGuardUntilMs = _pendingSelfMicSwitchGuardUntilMs =
DateTime.now().add(_selfMicSwitchGracePeriod).millisecondsSinceEpoch; DateTime.now().add(_selfMicStateGracePeriod).millisecondsSinceEpoch;
} }
void _clearSelfMicSwitchGuard({bool clearPreferredIndex = false}) { void _clearSelfMicSwitchGuard({bool clearPreferredIndex = false}) {
@ -299,6 +302,41 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} }
} }
void _startSelfMicReleaseGuard({required num sourceIndex}) {
_pendingSelfMicReleaseIndex = sourceIndex;
_pendingSelfMicReleaseGuardUntilMs =
DateTime.now().add(_selfMicStateGracePeriod).millisecondsSinceEpoch;
}
void _clearSelfMicReleaseGuard() {
_pendingSelfMicReleaseIndex = null;
_pendingSelfMicReleaseGuardUntilMs = null;
}
bool _shouldSuppressCurrentUserOnSeat(num index, {MicRes? seat}) {
final currentUserId =
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
if (currentUserId.isEmpty) {
return false;
}
final releaseIndex = _pendingSelfMicReleaseIndex;
final releaseGuardUntilMs = _pendingSelfMicReleaseGuardUntilMs ?? 0;
if (releaseIndex != index ||
releaseGuardUntilMs <= DateTime.now().millisecondsSinceEpoch) {
return false;
}
final resolvedSeat = seat ?? roomWheatMap[index];
return (resolvedSeat?.user?.id ?? "").trim() == currentUserId;
}
MicRes? micAtIndexForDisplay(num index) {
final seat = roomWheatMap[index];
if (!_shouldSuppressCurrentUserOnSeat(index, seat: seat)) {
return seat;
}
return seat?.copyWith(clearUser: true);
}
Map<num, MicRes> _stabilizeSelfMicSnapshot( Map<num, MicRes> _stabilizeSelfMicSnapshot(
Map<num, MicRes> nextMap, { Map<num, MicRes> nextMap, {
required Map<num, MicRes> previousMap, required Map<num, MicRes> previousMap,
@ -308,14 +346,13 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (currentUserId.isEmpty) { if (currentUserId.isEmpty) {
return nextMap; return nextMap;
} }
final nowMs = DateTime.now().millisecondsSinceEpoch;
final targetIndex = _preferredSelfMicIndex; final targetIndex = _preferredSelfMicIndex;
final sourceIndex = _pendingSelfMicSourceIndex; final sourceIndex = _pendingSelfMicSourceIndex;
final guardUntilMs = _pendingSelfMicSwitchGuardUntilMs ?? 0; final guardUntilMs = _pendingSelfMicSwitchGuardUntilMs ?? 0;
final hasActiveGuard = final hasActiveGuard =
targetIndex != null && targetIndex != null && sourceIndex != null && guardUntilMs > nowMs;
sourceIndex != null &&
guardUntilMs > DateTime.now().millisecondsSinceEpoch;
final selfSeatIndices = final selfSeatIndices =
nextMap.entries nextMap.entries
.where((entry) => entry.value.user?.id == currentUserId) .where((entry) => entry.value.user?.id == currentUserId)
@ -330,30 +367,22 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (!hasActiveGuard) { if (!hasActiveGuard) {
_clearSelfMicSwitchGuard(); _clearSelfMicSwitchGuard();
return nextMap; } else {
}
final resolvedTargetIndex = targetIndex; final resolvedTargetIndex = targetIndex;
final resolvedSourceIndex = sourceIndex; final resolvedSourceIndex = sourceIndex;
final selfOnlyOnSource = final selfOnlyOnSource =
selfSeatIndices.isEmpty || selfSeatIndices.isEmpty ||
selfSeatIndices.every((seatIndex) => seatIndex == resolvedSourceIndex); selfSeatIndices.every(
if (!selfOnlyOnSource) { (seatIndex) => seatIndex == resolvedSourceIndex,
return nextMap; );
} if (selfOnlyOnSource) {
final optimisticTargetSeat = previousMap[resolvedTargetIndex]; final optimisticTargetSeat = previousMap[resolvedTargetIndex];
if (optimisticTargetSeat == null || if (optimisticTargetSeat != null &&
optimisticTargetSeat.user?.id != currentUserId) { optimisticTargetSeat.user?.id == currentUserId) {
return nextMap;
}
final incomingTargetSeat = nextMap[resolvedTargetIndex]; final incomingTargetSeat = nextMap[resolvedTargetIndex];
if (incomingTargetSeat?.user != null && if (incomingTargetSeat?.user == null ||
incomingTargetSeat?.user?.id != currentUserId) { incomingTargetSeat?.user?.id == currentUserId) {
return nextMap;
}
final stabilizedMap = Map<num, MicRes>.from(nextMap); final stabilizedMap = Map<num, MicRes>.from(nextMap);
for (final seatIndex in selfSeatIndices) { for (final seatIndex in selfSeatIndices) {
if (seatIndex == resolvedTargetIndex) { if (seatIndex == resolvedTargetIndex) {
@ -369,10 +398,13 @@ class RealTimeCommunicationManager extends ChangeNotifier {
final baseSeat = incomingTargetSeat ?? optimisticTargetSeat; final baseSeat = incomingTargetSeat ?? optimisticTargetSeat;
stabilizedMap[resolvedTargetIndex] = baseSeat.copyWith( stabilizedMap[resolvedTargetIndex] = baseSeat.copyWith(
user: optimisticTargetSeat.user, user: optimisticTargetSeat.user,
micMute: incomingTargetSeat?.micMute ?? optimisticTargetSeat.micMute, micMute:
micLock: incomingTargetSeat?.micLock ?? optimisticTargetSeat.micLock, incomingTargetSeat?.micMute ?? optimisticTargetSeat.micMute,
micLock:
incomingTargetSeat?.micLock ?? optimisticTargetSeat.micLock,
roomToken: roomToken:
incomingTargetSeat?.roomToken ?? optimisticTargetSeat.roomToken, incomingTargetSeat?.roomToken ??
optimisticTargetSeat.roomToken,
emojiPath: emojiPath:
(incomingTargetSeat?.emojiPath ?? "").isNotEmpty (incomingTargetSeat?.emojiPath ?? "").isNotEmpty
? incomingTargetSeat?.emojiPath ? incomingTargetSeat?.emojiPath
@ -389,6 +421,38 @@ class RealTimeCommunicationManager extends ChangeNotifier {
return stabilizedMap; return stabilizedMap;
} }
}
}
}
final releaseIndex = _pendingSelfMicReleaseIndex;
final releaseGuardUntilMs = _pendingSelfMicReleaseGuardUntilMs ?? 0;
final hasActiveReleaseGuard =
releaseIndex != null && releaseGuardUntilMs > nowMs;
if (!hasActiveReleaseGuard) {
_clearSelfMicReleaseGuard();
return nextMap;
}
if (selfSeatIndices.isEmpty) {
return nextMap;
}
if (selfSeatIndices.any((seatIndex) => seatIndex != releaseIndex)) {
_clearSelfMicReleaseGuard();
return nextMap;
}
final releaseSeat = nextMap[releaseIndex];
if (releaseSeat?.user?.id != currentUserId) {
_clearSelfMicReleaseGuard();
return nextMap;
}
final stabilizedMap = Map<num, MicRes>.from(nextMap);
stabilizedMap[releaseIndex] = releaseSeat!.copyWith(clearUser: true);
return stabilizedMap;
}
bool _shouldPreferDuplicateMicSeat({ bool _shouldPreferDuplicateMicSeat({
required String userId, required String userId,
@ -1208,6 +1272,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
needUpDataUserInfo = false; needUpDataUserInfo = false;
SCRoomUtils.roomUsersMap.clear(); SCRoomUtils.roomUsersMap.clear();
_clearSelfMicSwitchGuard(clearPreferredIndex: true); _clearSelfMicSwitchGuard(clearPreferredIndex: true);
_clearSelfMicReleaseGuard();
roomIsMute = false; roomIsMute = false;
rtmProvider?.roomAllMsgList.clear(); rtmProvider?.roomAllMsgList.clear();
rtmProvider?.roomChatMsgList.clear(); rtmProvider?.roomChatMsgList.clear();
@ -1405,6 +1470,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
final isSeatSwitching = final isSeatSwitching =
previousSelfSeatIndex > -1 && previousSelfSeatIndex != targetIndex; previousSelfSeatIndex > -1 && previousSelfSeatIndex != targetIndex;
if (myUser != null) { if (myUser != null) {
_clearSelfMicReleaseGuard();
if (isSeatSwitching) { if (isSeatSwitching) {
_startSelfMicSwitchGuard( _startSelfMicSwitchGuard(
sourceIndex: previousSelfSeatIndex, sourceIndex: previousSelfSeatIndex,
@ -1454,20 +1520,30 @@ class RealTimeCommunicationManager extends ChangeNotifier {
index, index,
); );
if (roomWheatMap[index]?.user?.id == final currentUserId =
AccountStorage().getCurrentUser()?.userProfile?.id) { AccountStorage().getCurrentUser()?.userProfile?.id ?? "";
final currentUserSeatIndex = userOnMaiInIndex(currentUserId);
if (roomWheatMap[index]?.user?.id == currentUserId) {
isMic = true; isMic = true;
engine?.muteLocalAudioStream(true); engine?.muteLocalAudioStream(true);
} }
_clearSelfMicSwitchGuard(clearPreferredIndex: true); _clearSelfMicSwitchGuard(clearPreferredIndex: true);
if (currentUserSeatIndex > -1) {
_startSelfMicReleaseGuard(sourceIndex: currentUserSeatIndex);
} else {
_clearSelfMicReleaseGuard();
}
SCHeartbeatUtils.cancelAnchorTimer(); SCHeartbeatUtils.cancelAnchorTimer();
/// ///
engine?.renewToken(""); engine?.renewToken("");
engine?.setClientRole(role: ClientRoleType.clientRoleAudience); engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
_clearUserFromSeats(AccountStorage().getCurrentUser()?.userProfile?.id); _clearUserFromSeats(currentUserId);
notifyListeners(); notifyListeners();
_refreshMicListSilently(); requestMicrophoneListRefresh(
notifyIfUnchanged: false,
minInterval: const Duration(milliseconds: 350),
);
} catch (ex) { } catch (ex) {
SCTts.show('Failed to leave the microphone, $ex'); SCTts.show('Failed to leave the microphone, $ex');
} }
@ -1510,23 +1586,41 @@ class RealTimeCommunicationManager extends ChangeNotifier {
/// ///
bool isOnMai() { bool isOnMai() {
return roomWheatMap.values final currentUserId =
.map((userWheat) => userWheat.user?.id) (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
.toList() if (currentUserId.isEmpty) {
.contains(AccountStorage().getCurrentUser()?.userProfile?.id); return false;
}
for (final entry in roomWheatMap.entries) {
if ((micAtIndexForDisplay(entry.key)?.user?.id ?? "").trim() ==
currentUserId) {
return true;
}
}
return false;
} }
/// ///
bool isOnMaiInIndex(num index) { bool isOnMaiInIndex(num index) {
return roomWheatMap[index]?.user?.id == return (micAtIndexForDisplay(index)?.user?.id ?? "").trim() ==
AccountStorage().getCurrentUser()?.userProfile?.id; (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
} }
/// ///
num userOnMaiInIndex(String userId) { num userOnMaiInIndex(String userId) {
final normalizedUserId = userId.trim();
if (normalizedUserId.isEmpty) {
return -1;
}
num index = -1; num index = -1;
roomWheatMap.forEach((k, value) { roomWheatMap.forEach((k, value) {
if (value.user?.id == userId) { final visibleSeat =
normalizedUserId ==
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "")
.trim()
? micAtIndexForDisplay(k)
: value;
if ((visibleSeat?.user?.id ?? "").trim() == normalizedUserId) {
index = k; index = k;
} }
}); });

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
@ -6,6 +7,9 @@ import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart';
class GiftAnimationManager extends ChangeNotifier { class GiftAnimationManager extends ChangeNotifier {
static const int _maxPendingAnimations = 24; static const int _maxPendingAnimations = 24;
static const Duration _activeComboIdleDismissDelay = Duration(
milliseconds: 3200,
);
Queue<LGiftModel> pendingAnimationsQueue = Queue<LGiftModel>(); Queue<LGiftModel> pendingAnimationsQueue = Queue<LGiftModel>();
List<LGiftScrollingScreenAnimsBean> animationControllerList = []; List<LGiftScrollingScreenAnimsBean> animationControllerList = [];
@ -16,6 +20,29 @@ class GiftAnimationManager extends ChangeNotifier {
bool get _controllersReady => bool get _controllersReady =>
animationControllerList.length >= giftMap.length; animationControllerList.length >= giftMap.length;
GiftAnimationSlotSnapshot? slotSnapshotAt(int index) {
final gift = giftMap[index];
if (gift == null || animationControllerList.length <= index) {
return null;
}
final bean = animationControllerList[index];
return GiftAnimationSlotSnapshot(
index: index,
bean: bean,
sendUserName: gift.sendUserName,
sendToUserName: gift.sendToUserName,
sendUserPic: gift.sendUserPic,
giftPic: gift.giftPic,
giftName: gift.giftName,
giftCount: gift.giftCount,
rewardAmountText: gift.rewardAmountText,
rewardAmount: gift.rewardAmount,
showLuckyRewardFrame: gift.showLuckyRewardFrame,
labelId: gift.labelId,
giftCountStepUnit: gift.giftCountStepUnit,
);
}
void enqueueGiftAnimation(LGiftModel giftModel) { void enqueueGiftAnimation(LGiftModel giftModel) {
if (_mergeIntoActiveAnimation(giftModel)) { if (_mergeIntoActiveAnimation(giftModel)) {
return; return;
@ -42,9 +69,7 @@ class GiftAnimationManager extends ChangeNotifier {
} }
_mergeGiftModel(target: current, incoming: incoming); _mergeGiftModel(target: current, incoming: incoming);
notifyListeners(); notifyListeners();
if (animationControllerList.length > entry.key) { _refreshSlotAnimation(entry.key, restartEntry: false);
animationControllerList[entry.key].controller.forward(from: 0.45);
}
return true; return true;
} }
return false; return false;
@ -87,6 +112,9 @@ class GiftAnimationManager extends ChangeNotifier {
} }
target.showLuckyRewardFrame = target.showLuckyRewardFrame =
target.showLuckyRewardFrame || incoming.showLuckyRewardFrame; target.showLuckyRewardFrame || incoming.showLuckyRewardFrame;
if (incoming.giftCountStepUnit > 0) {
target.giftCountStepUnit = incoming.giftCountStepUnit;
}
} }
/// ///
@ -101,14 +129,14 @@ class GiftAnimationManager extends ChangeNotifier {
giftMap[key] = playGift; giftMap[key] = playGift;
pendingAnimationsQueue.removeFirst(); pendingAnimationsQueue.removeFirst();
notifyListeners(); notifyListeners();
animationControllerList[key].controller.forward(from: 0); _refreshSlotAnimation(key, restartEntry: true);
break; break;
} else { } else {
if (value.labelId == playGift.labelId) { if (value.labelId == playGift.labelId) {
_mergeGiftModel(target: value, incoming: playGift); _mergeGiftModel(target: value, incoming: playGift);
pendingAnimationsQueue.removeFirst(); pendingAnimationsQueue.removeFirst();
notifyListeners(); notifyListeners();
animationControllerList[key].controller.forward(from: 0.45); _refreshSlotAnimation(key, restartEntry: false);
break; break;
} }
} }
@ -127,14 +155,125 @@ class GiftAnimationManager extends ChangeNotifier {
giftMap[2] = null; giftMap[2] = null;
giftMap[3] = null; giftMap[3] = null;
for (var element in animationControllerList) { for (var element in animationControllerList) {
element.dismissTimer?.cancel();
element.controller.dispose(); element.controller.dispose();
element.countPulseController.dispose();
} }
animationControllerList.clear(); animationControllerList.clear();
} }
void markAnimationAsFinished(int index) { void markAnimationAsFinished(int index) {
_cancelSlotDismissTimer(index);
if (giftMap[index] == null) {
return;
}
giftMap[index] = null; giftMap[index] = null;
notifyListeners(); notifyListeners();
proceedToNextAnimation(); proceedToNextAnimation();
} }
void _refreshSlotAnimation(int index, {required bool restartEntry}) {
if (animationControllerList.length <= index) {
return;
}
final bean = animationControllerList[index];
_restartSlotDismissTimer(index);
bean.countPulseController.forward(from: 0);
if (restartEntry) {
bean.controller.forward(from: 0);
}
}
void _restartSlotDismissTimer(int index) {
if (animationControllerList.length <= index) {
return;
}
final bean = animationControllerList[index];
bean.dismissTimer?.cancel();
bean.dismissTimer = Timer(_activeComboIdleDismissDelay, () {
if (giftMap[index] == null) {
return;
}
markAnimationAsFinished(index);
});
}
void _cancelSlotDismissTimer(int index) {
if (animationControllerList.length <= index) {
return;
}
animationControllerList[index].dismissTimer?.cancel();
animationControllerList[index].dismissTimer = null;
}
}
@immutable
class GiftAnimationSlotSnapshot {
const GiftAnimationSlotSnapshot({
required this.index,
required this.bean,
required this.sendUserName,
required this.sendToUserName,
required this.sendUserPic,
required this.giftPic,
required this.giftName,
required this.giftCount,
required this.rewardAmountText,
required this.rewardAmount,
required this.showLuckyRewardFrame,
required this.labelId,
required this.giftCountStepUnit,
});
final int index;
final LGiftScrollingScreenAnimsBean bean;
final String sendUserName;
final String sendToUserName;
final String sendUserPic;
final String giftPic;
final String giftName;
final num giftCount;
final String rewardAmountText;
final num rewardAmount;
final bool showLuckyRewardFrame;
final String labelId;
final num giftCountStepUnit;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
return other is GiftAnimationSlotSnapshot &&
other.index == index &&
identical(other.bean, bean) &&
other.sendUserName == sendUserName &&
other.sendToUserName == sendToUserName &&
other.sendUserPic == sendUserPic &&
other.giftPic == giftPic &&
other.giftName == giftName &&
other.giftCount == giftCount &&
other.rewardAmountText == rewardAmountText &&
other.rewardAmount == rewardAmount &&
other.showLuckyRewardFrame == showLuckyRewardFrame &&
other.labelId == labelId &&
other.giftCountStepUnit == giftCountStepUnit;
}
@override
int get hashCode => Object.hash(
index,
bean,
sendUserName,
sendToUserName,
sendUserPic,
giftPic,
giftName,
giftCount,
rewardAmountText,
rewardAmount,
showLuckyRewardFrame,
labelId,
giftCountStepUnit,
);
} }

View File

@ -27,6 +27,8 @@ class SocialChatGiftSystemManager extends ChangeNotifier {
8000: "sc_images/room/anim/luck_gift/luck_gift_combo_count_8000.svga", 8000: "sc_images/room/anim/luck_gift/luck_gift_combo_count_8000.svga",
10000: "sc_images/room/anim/luck_gift/luck_gift_combo_count_10000.svga", 10000: "sc_images/room/anim/luck_gift/luck_gift_combo_count_10000.svga",
}; };
static final List<int> _luckGiftComboEffectMilestones =
(_luckGiftComboEffectAssets.keys.toList()..sort());
static const List<int> _luckGiftMilestones = <int>[ static const List<int> _luckGiftMilestones = <int>[
10, 10,
20, 20,
@ -211,7 +213,7 @@ class SocialChatGiftSystemManager extends ChangeNotifier {
} }
void startGiftAnimation() { void startGiftAnimation() {
final milestone = resolveHighestReachedComboMilestone(number); final milestone = resolveHighestReachedComboEffectMilestone(number);
if (milestone == null || (isPlayed[milestone] ?? false)) { if (milestone == null || (isPlayed[milestone] ?? false)) {
return; return;
} }
@ -261,19 +263,45 @@ class SocialChatGiftSystemManager extends ChangeNotifier {
return resolveHighestReachedComboMilestone(count); return resolveHighestReachedComboMilestone(count);
} }
static int? resolveHighestReachedComboEffectMilestone(num count) {
final normalizedCount = count.floor();
int? highest;
for (final milestone in _luckGiftComboEffectMilestones) {
if (milestone > normalizedCount) {
break;
}
highest = milestone;
}
return highest;
}
static int? resolveHighestCrossedComboEffectMilestone({
required num previousCount,
required num currentCount,
}) {
final previous = previousCount.floor();
final current = currentCount.floor();
if (current <= previous) {
return null;
}
int? highest;
for (final milestone in _luckGiftComboEffectMilestones) {
if (milestone <= previous) {
continue;
}
if (milestone > current) {
break;
}
highest = milestone;
}
return highest;
}
static String? resolveComboMilestoneEffectPath(num count) { static String? resolveComboMilestoneEffectPath(num count) {
if (count % 1 != 0) { if (count % 1 != 0) {
return null; return null;
} }
final normalizedCount = count.toInt(); return _luckGiftComboEffectAssets[count.toInt()];
final comboEffectPath = _luckGiftComboEffectAssets[normalizedCount];
if (comboEffectPath != null) {
return comboEffectPath;
}
if (normalizedCount > 9999) {
return "sc_images/room/anim/luck_gift_count_5000_mor.mp4";
}
return "sc_images/room/anim/luck_gift_count_$normalizedCount.mp4";
} }
static String? resolveLuckGiftComboEffectPath(num count) { static String? resolveLuckGiftComboEffectPath(num count) {

View File

@ -1,8 +1,12 @@
import 'dart:async';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/main.dart'; import 'package:yumi/main.dart';
import 'package:provider/provider.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/shared/tools/sc_entrance_vap_svga_manager.dart';
import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/ui_kit/widgets/room/floating/floating_game_screen_widget.dart'; import 'package:yumi/ui_kit/widgets/room/floating/floating_game_screen_widget.dart';
@ -33,8 +37,17 @@ class OverlayManager {
void addMessage(SCFloatingMessage message) { void addMessage(SCFloatingMessage message) {
if (_isDisposed) return; if (_isDisposed) return;
if (SCGlobalConfig.isFloatingAnimationInGlobal) { if (SCGlobalConfig.isFloatingAnimationInGlobal) {
unawaited(_warmMessageImages(message));
SCRoomEffectScheduler().scheduleDeferredEffect(
debugLabel: 'floating_message_type_${message.type ?? -1}',
action: () {
if (_isDisposed) {
return;
}
_messageQueue.add(message); _messageQueue.add(message);
_safeScheduleNext(); _safeScheduleNext();
},
);
} else { } else {
_messageQueue.clear(); _messageQueue.clear();
_isPlaying = false; _isPlaying = false;
@ -43,6 +56,30 @@ class OverlayManager {
} }
} }
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: 96,
logicalHeight: 96,
),
);
}
void _safeScheduleNext() { void _safeScheduleNext() {
if (_isProcessing || _isPlaying || _messageQueue.isEmpty) return; if (_isProcessing || _isPlaying || _messageQueue.isEmpty) return;
_isProcessing = true; _isProcessing = true;
@ -103,7 +140,7 @@ class OverlayManager {
alignment: AlignmentDirectional.topStart, alignment: AlignmentDirectional.topStart,
child: Transform.translate( child: Transform.translate(
offset: Offset(0, 70.w), offset: Offset(0, 70.w),
child: _buildScreenWidget(message), child: RepaintBoundary(child: _buildScreenWidget(message)),
), ),
), ),
); );

View File

@ -130,6 +130,7 @@ class BaseNetworkClient {
String path, { String path, {
Map<String, dynamic>? queryParams, Map<String, dynamic>? queryParams,
Map<String, dynamic>? extra, Map<String, dynamic>? extra,
bool allowNullBody = false,
required T Function(dynamic) fromJson, required T Function(dynamic) fromJson,
CancelToken? cancelToken, CancelToken? cancelToken,
ProgressCallback? onSendProgress, ProgressCallback? onSendProgress,
@ -140,6 +141,7 @@ class BaseNetworkClient {
method: 'GET', method: 'GET',
queryParams: queryParams, queryParams: queryParams,
extra: extra, extra: extra,
allowNullBody: allowNullBody,
fromJson: fromJson, fromJson: fromJson,
cancelToken: cancelToken, cancelToken: cancelToken,
onSendProgress: onSendProgress, onSendProgress: onSendProgress,
@ -153,6 +155,7 @@ class BaseNetworkClient {
dynamic data, dynamic data,
Map<String, dynamic>? queryParams, Map<String, dynamic>? queryParams,
Map<String, dynamic>? extra, Map<String, dynamic>? extra,
bool allowNullBody = false,
required T Function(dynamic) fromJson, required T Function(dynamic) fromJson,
CancelToken? cancelToken, CancelToken? cancelToken,
ProgressCallback? onSendProgress, ProgressCallback? onSendProgress,
@ -164,6 +167,7 @@ class BaseNetworkClient {
data: data, data: data,
queryParams: queryParams, queryParams: queryParams,
extra: extra, extra: extra,
allowNullBody: allowNullBody,
fromJson: fromJson, fromJson: fromJson,
cancelToken: cancelToken, cancelToken: cancelToken,
onSendProgress: onSendProgress, onSendProgress: onSendProgress,
@ -177,6 +181,7 @@ class BaseNetworkClient {
dynamic data, dynamic data,
Map<String, dynamic>? queryParams, Map<String, dynamic>? queryParams,
Map<String, dynamic>? extra, Map<String, dynamic>? extra,
bool allowNullBody = false,
required T Function(dynamic) fromJson, required T Function(dynamic) fromJson,
CancelToken? cancelToken, CancelToken? cancelToken,
ProgressCallback? onSendProgress, ProgressCallback? onSendProgress,
@ -188,6 +193,7 @@ class BaseNetworkClient {
data: data, data: data,
queryParams: queryParams, queryParams: queryParams,
extra: extra, extra: extra,
allowNullBody: allowNullBody,
fromJson: fromJson, fromJson: fromJson,
cancelToken: cancelToken, cancelToken: cancelToken,
onSendProgress: onSendProgress, onSendProgress: onSendProgress,
@ -201,6 +207,7 @@ class BaseNetworkClient {
dynamic data, dynamic data,
Map<String, dynamic>? queryParams, Map<String, dynamic>? queryParams,
Map<String, dynamic>? extra, Map<String, dynamic>? extra,
bool allowNullBody = false,
required T Function(dynamic) fromJson, required T Function(dynamic) fromJson,
CancelToken? cancelToken, CancelToken? cancelToken,
ProgressCallback? onSendProgress, ProgressCallback? onSendProgress,
@ -212,6 +219,7 @@ class BaseNetworkClient {
data: data, data: data,
queryParams: queryParams, queryParams: queryParams,
extra: extra, extra: extra,
allowNullBody: allowNullBody,
fromJson: fromJson, fromJson: fromJson,
cancelToken: cancelToken, cancelToken: cancelToken,
onSendProgress: onSendProgress, onSendProgress: onSendProgress,
@ -226,6 +234,7 @@ class BaseNetworkClient {
dynamic data, dynamic data,
Map<String, dynamic>? queryParams, Map<String, dynamic>? queryParams,
Map<String, dynamic>? extra, Map<String, dynamic>? extra,
bool allowNullBody = false,
CancelToken? cancelToken, CancelToken? cancelToken,
ProgressCallback? onSendProgress, ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress, ProgressCallback? onReceiveProgress,
@ -251,6 +260,8 @@ class BaseNetworkClient {
if (baseResponse.success) { if (baseResponse.success) {
if (baseResponse.body != null) { if (baseResponse.body != null) {
return baseResponse.body!; return baseResponse.body!;
} else if (allowNullBody && null is T) {
return null as T;
} else { } else {
if (T == bool) return false as T; if (T == bool) return false as T;
if (T == int) return 0 as T; if (T == int) return 0 as T;

View File

@ -213,6 +213,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
final result = await http.post( final result = await http.post(
"11335ecd55d99c9100f5cd070e5b73f1574aff257ce7e668d08f4caccd1c6232", "11335ecd55d99c9100f5cd070e5b73f1574aff257ce7e668d08f4caccd1c6232",
data: {"roomId": roomId, "mickIndex": mickIndex}, data: {"roomId": roomId, "mickIndex": mickIndex},
allowNullBody: true,
fromJson: (json) => null, fromJson: (json) => null,
); );
return result; return result;
@ -224,6 +225,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
final result = await http.post( final result = await http.post(
"70ffb9681e0103463b1faf883935e55d", "70ffb9681e0103463b1faf883935e55d",
data: {"roomId": roomId, "mickIndex": mickIndex, "lock": lock}, data: {"roomId": roomId, "mickIndex": mickIndex, "lock": lock},
allowNullBody: true,
fromJson: (json) => null, fromJson: (json) => null,
); );
return result; return result;
@ -405,6 +407,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
final result = await http.post( final result = await http.post(
"68900c787a89c1092b21ee1a610ef323380d4f62784dacdb4e3d4e1f60770a39", "68900c787a89c1092b21ee1a610ef323380d4f62784dacdb4e3d4e1f60770a39",
data: {"roomId": roomId, "userId": userId}, data: {"roomId": roomId, "userId": userId},
allowNullBody: true,
fromJson: (json) => null, fromJson: (json) => null,
); );
return result; return result;

View File

@ -348,6 +348,7 @@ class SCAccountRepository implements SocialChatUserRepository {
final result = await http.post( final result = await http.post(
"745ce65f1d68f106702ad3c636aca0e0ac57f82127b3ac414551924946593db7", "745ce65f1d68f106702ad3c636aca0e0ac57f82127b3ac414551924946593db7",
data: {"pwd": pwd}, data: {"pwd": pwd},
allowNullBody: true,
fromJson: (json) => null, fromJson: (json) => null,
); );
return result; return result;

View File

@ -6,6 +6,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter_svga/flutter_svga.dart'; import 'package:flutter_svga/flutter_svga.dart';
import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/shared/tools/sc_path_utils.dart'; import 'package:yumi/shared/tools/sc_path_utils.dart';
import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart';
import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart';
import 'package:tancent_vap/utils/constant.dart'; import 'package:tancent_vap/utils/constant.dart';
import 'package:tancent_vap/widgets/vap_view.dart'; import 'package:tancent_vap/widgets/vap_view.dart';
@ -267,6 +268,9 @@ class SCGiftVapSvgaManager {
if (_dis) { if (_dis) {
return; return;
} }
SCRoomEffectScheduler().completeHighCostTask(
debugLabel: _currentTask?.path,
);
_play = false; _play = false;
_currentTask = null; _currentTask = null;
_scheduleNextTask(delay: delay); _scheduleNextTask(delay: delay);
@ -316,6 +320,9 @@ class SCGiftVapSvgaManager {
if (status.isCompleted) { if (status.isCompleted) {
_log('svga completed path=${_currentTask?.path}'); _log('svga completed path=${_currentTask?.path}');
_rsc?.reset(); _rsc?.reset();
SCRoomEffectScheduler().completeHighCostTask(
debugLabel: _currentTask?.path,
);
_play = false; _play = false;
_currentTask = null; _currentTask = null;
_pn(); _pn();
@ -363,8 +370,13 @@ class SCGiftVapSvgaManager {
return; return;
} }
_tq.add(task); _tq.add(task);
SCRoomEffectScheduler().registerHighCostTaskQueued(debugLabel: path);
unawaited(preload(path));
while (_tq.length > _maxPendingTaskCount) { while (_tq.length > _maxPendingTaskCount) {
final removedTask = _tq.removeLast(); final removedTask = _tq.removeLast();
SCRoomEffectScheduler().completeHighCostTask(
debugLabel: removedTask.path,
);
_log( _log(
'trim queued task path=${removedTask.path} ' 'trim queued task path=${removedTask.path} '
'priority=${removedTask.priority} queue=${_tq.length}', 'priority=${removedTask.priority} queue=${_tq.length}',
@ -547,6 +559,7 @@ class SCGiftVapSvgaManager {
_rsc?.stop(); _rsc?.stop();
_rsc?.reset(); _rsc?.reset();
_rsc?.videoItem = null; _rsc?.videoItem = null;
SCRoomEffectScheduler().clearHighCostTasks(reason: 'stop_playback');
} }
// //

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:ui';
import 'package:extended_image/extended_image.dart'; import 'package:extended_image/extended_image.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
@ -37,26 +38,56 @@ Map<String, String>? buildNetworkImageHeaders(String url) {
return headers; return headers;
} }
ImageProvider<Object> buildCachedImageProvider(String url) { int? _resolveImageProviderCacheDimension(double? logicalSize) {
if (logicalSize == null || !logicalSize.isFinite || logicalSize <= 0) {
return null;
}
final devicePixelRatio =
PlatformDispatcher.instance.implicitView?.devicePixelRatio ?? 2.0;
final maxDimension = SCGlobalConfig.isLowPerformanceDevice ? 1280 : 1920;
final pixelSize = (logicalSize * devicePixelRatio).round();
return pixelSize.clamp(1, maxDimension).toInt();
}
ImageProvider<Object> buildCachedImageProvider(
String url, {
double? logicalWidth,
double? logicalHeight,
}) {
late final ImageProvider<Object> provider;
if (url.startsWith("http")) { if (url.startsWith("http")) {
return ExtendedNetworkImageProvider( provider = ExtendedNetworkImageProvider(
url, url,
headers: buildNetworkImageHeaders(url), headers: buildNetworkImageHeaders(url),
cache: true, cache: true,
cacheMaxAge: const Duration(days: 7), cacheMaxAge: const Duration(days: 7),
); );
} else if (url.startsWith('/')) {
provider = FileImage(File(url));
} else {
provider = AssetImage(url);
} }
return FileImage(File(url)); return ResizeImage.resizeIfNeeded(
_resolveImageProviderCacheDimension(logicalWidth),
_resolveImageProviderCacheDimension(logicalHeight),
provider,
);
} }
Future<bool> warmNetworkImage( Future<bool> warmImageResource(
String url, { String resource, {
double? logicalWidth,
double? logicalHeight,
Duration timeout = const Duration(seconds: 6), Duration timeout = const Duration(seconds: 6),
}) async { }) async {
if (url.isEmpty) { if (resource.isEmpty) {
return false; return false;
} }
final provider = buildCachedImageProvider(url); final provider = buildCachedImageProvider(
resource,
logicalWidth: logicalWidth,
logicalHeight: logicalHeight,
);
final stream = provider.resolve(ImageConfiguration.empty); final stream = provider.resolve(ImageConfiguration.empty);
final completer = Completer<bool>(); final completer = Completer<bool>();
late final ImageStreamListener listener; late final ImageStreamListener listener;
@ -83,3 +114,17 @@ Future<bool> warmNetworkImage(
}, },
); );
} }
Future<bool> warmNetworkImage(
String url, {
double? logicalWidth,
double? logicalHeight,
Duration timeout = const Duration(seconds: 6),
}) {
return warmImageResource(
url,
logicalWidth: logicalWidth,
logicalHeight: logicalHeight,
timeout: timeout,
);
}

View File

@ -0,0 +1,140 @@
import 'dart:async';
import 'dart:collection';
import 'package:flutter/foundation.dart';
class SCRoomEffectScheduler {
static const Duration _deferredDrainInterval = Duration(milliseconds: 60);
static final SCRoomEffectScheduler _instance =
SCRoomEffectScheduler._internal();
factory SCRoomEffectScheduler() => _instance;
SCRoomEffectScheduler._internal();
final Queue<_DeferredRoomEffectTask> _deferredTasks =
Queue<_DeferredRoomEffectTask>();
Timer? _deferredDrainTimer;
int _pendingHighCostTaskCount = 0;
int _nextTaskId = 0;
bool get hasActiveHighCostEffects => _pendingHighCostTaskCount > 0;
void registerHighCostTaskQueued({String? debugLabel}) {
_pendingHighCostTaskCount += 1;
_cancelDeferredDrain();
_log(
'register_high_cost task=${debugLabel ?? "unknown"} '
'pending=$_pendingHighCostTaskCount deferred=${_deferredTasks.length}',
);
}
void completeHighCostTask({String? debugLabel}) {
if (_pendingHighCostTaskCount > 0) {
_pendingHighCostTaskCount -= 1;
}
_log(
'complete_high_cost task=${debugLabel ?? "unknown"} '
'pending=$_pendingHighCostTaskCount deferred=${_deferredTasks.length}',
);
_scheduleDeferredDrainIfNeeded();
}
void clearHighCostTasks({String reason = 'unknown'}) {
if (_pendingHighCostTaskCount == 0) {
_scheduleDeferredDrainIfNeeded();
return;
}
_pendingHighCostTaskCount = 0;
_log(
'clear_high_cost reason=$reason '
'pending=$_pendingHighCostTaskCount deferred=${_deferredTasks.length}',
);
_scheduleDeferredDrainIfNeeded();
}
void scheduleDeferredEffect({
required String debugLabel,
required VoidCallback action,
}) {
if (!hasActiveHighCostEffects) {
action();
return;
}
final task = _DeferredRoomEffectTask(
id: _nextTaskId++,
debugLabel: debugLabel,
action: action,
);
_deferredTasks.add(task);
_log(
'defer task=${task.debugLabel} id=${task.id} '
'pending=$_pendingHighCostTaskCount deferred=${_deferredTasks.length}',
);
}
void clearDeferredTasks({String reason = 'unknown'}) {
if (_deferredTasks.isEmpty) {
_cancelDeferredDrain();
return;
}
_deferredTasks.clear();
_cancelDeferredDrain();
_log('clear_deferred reason=$reason');
}
void _scheduleDeferredDrainIfNeeded() {
if (hasActiveHighCostEffects ||
_deferredTasks.isEmpty ||
_deferredDrainTimer != null) {
return;
}
_deferredDrainTimer = Timer(_deferredDrainInterval, _drainNextDeferredTask);
}
void _drainNextDeferredTask() {
_deferredDrainTimer = null;
if (hasActiveHighCostEffects || _deferredTasks.isEmpty) {
return;
}
final task = _deferredTasks.removeFirst();
_log(
'drain_deferred task=${task.debugLabel} id=${task.id} '
'remaining=${_deferredTasks.length}',
);
try {
task.action();
} catch (error, stackTrace) {
debugPrint(
'[RoomEffectScheduler] deferred task failed: $error\n$stackTrace',
);
}
_scheduleDeferredDrainIfNeeded();
}
void _cancelDeferredDrain() {
_deferredDrainTimer?.cancel();
_deferredDrainTimer = null;
}
void _log(String message) {
debugPrint('[RoomEffectScheduler] $message');
}
}
class _DeferredRoomEffectTask {
const _DeferredRoomEffectTask({
required this.id,
required this.debugLabel,
required this.action,
});
final int id;
final String debugLabel;
final VoidCallback action;
}

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/app_localizations.dart'; import 'package:yumi/app_localizations.dart';
@ -19,62 +21,145 @@ class LGiftAnimalPage extends StatefulWidget {
class _GiftAnimalPageState extends State<LGiftAnimalPage> class _GiftAnimalPageState extends State<LGiftAnimalPage>
with TickerProviderStateMixin { with TickerProviderStateMixin {
static const String _luckyGiftRewardFrameAssetPath =
"sc_images/room/anim/luck_gift/luck_gift_reward_frame.svga";
late final GiftAnimationManager _giftAnimationManager; late final GiftAnimationManager _giftAnimationManager;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
height: 332.w, height: 332.w,
child: Consumer<GiftAnimationManager>( child: Stack(
builder: (context, ref, child) {
return Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: List.generate( children: List.generate(4, (index) => _GiftTickerSlot(index: index)),
4,
(index) => _buildGiftTickerItem(context, ref, index),
),
);
},
), ),
); );
} }
Widget _buildGiftTickerItem( @override
BuildContext context, void dispose() {
GiftAnimationManager ref, _giftAnimationManager.cleanupAnimationResources();
int index, super.dispose();
) { }
final gift = ref.giftMap[index];
if (gift == null || ref.animationControllerList.length <= index) { @override
void initState() {
super.initState();
_giftAnimationManager = Provider.of<GiftAnimationManager>(
context,
listen: false,
);
initAnimal();
}
void initAnimal() {
List<LGiftScrollingScreenAnimsBean> beans = [];
double top = 60;
for (int i = 0; i < 4; i++) {
var bean = LGiftScrollingScreenAnimsBean();
var controller = AnimationController(
value: 0,
duration: const Duration(milliseconds: 360),
vsync: this,
);
bean.controller = controller;
bean.countPulseController = AnimationController(
value: 1,
duration: const Duration(milliseconds: 280),
vsync: this,
);
bean.luckyGiftPinnedMargin = EdgeInsets.only(top: top);
bean.verticalAnimation = EdgeInsetsTween(
begin: EdgeInsets.only(top: top),
end: EdgeInsets.only(top: 0),
).animate(
CurvedAnimation(parent: controller, curve: Curves.easeOutCubic),
);
bean.sizeAnimation = TweenSequence<double>([
TweenSequenceItem(
tween: Tween<double>(
begin: 1,
end: 1.18,
).chain(CurveTween(curve: Curves.easeOutCubic)),
weight: 65,
),
TweenSequenceItem(
tween: Tween<double>(
begin: 1.18,
end: 1,
).chain(CurveTween(curve: Curves.easeInCubic)),
weight: 35,
),
]).animate(
CurvedAnimation(
parent: bean.countPulseController,
curve: Curves.linear,
),
);
beans.add(bean);
top = top + 70;
}
_giftAnimationManager.attachAnimationControllers(beans);
}
String getBg(String type) {
return "sc_images/room/sc_icon_room_gift_left_no_vip_bg.png";
}
}
class _GiftTickerSlot extends StatelessWidget {
const _GiftTickerSlot({required this.index});
final int index;
@override
Widget build(BuildContext context) {
return Selector<GiftAnimationManager, GiftAnimationSlotSnapshot?>(
selector: (context, manager) => manager.slotSnapshotAt(index),
builder: (context, snapshot, child) {
if (snapshot == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final bean = ref.animationControllerList[index]; return RepaintBoundary(
return AnimatedBuilder( child: AnimatedBuilder(
animation: bean.controller, animation: Listenable.merge([
snapshot.bean.controller,
snapshot.bean.countPulseController,
]),
builder: (context, child) { builder: (context, child) {
final showLuckyRewardFrame = gift.showLuckyRewardFrame; final showLuckyRewardFrame = snapshot.showLuckyRewardFrame;
final tickerMargin = final tickerMargin =
showLuckyRewardFrame showLuckyRewardFrame
? bean.luckyGiftPinnedMargin ? snapshot.bean.luckyGiftPinnedMargin
: bean.verticalAnimation.value; : snapshot.bean.verticalAnimation.value;
return Container( return Container(
margin: tickerMargin, margin: tickerMargin,
width: width:
ScreenUtil().screenWidth * (showLuckyRewardFrame ? 0.96 : 0.78), ScreenUtil().screenWidth *
child: _buildGiftTickerCard(context, gift, bean.sizeAnimation.value), (showLuckyRewardFrame ? 0.96 : 0.78),
child: _GiftTickerCard(
snapshot: snapshot,
animatedSize: 22.sp * snapshot.bean.sizeAnimation.value,
),
);
},
),
); );
}, },
); );
} }
}
Widget _buildGiftTickerCard( class _GiftTickerCard extends StatelessWidget {
BuildContext context, const _GiftTickerCard({required this.snapshot, required this.animatedSize});
LGiftModel gift,
double animatedSize, static const String _luckyGiftRewardFrameAssetPath =
) { "sc_images/room/anim/luck_gift/luck_gift_reward_frame.svga";
final showLuckyRewardFrame = gift.showLuckyRewardFrame;
final GiftAnimationSlotSnapshot snapshot;
final double animatedSize;
@override
Widget build(BuildContext context) {
final showLuckyRewardFrame = snapshot.showLuckyRewardFrame;
return SizedBox( return SizedBox(
height: 52.w, height: 52.w,
child: Stack( child: Stack(
@ -91,16 +176,18 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
top: 3.w, top: 3.w,
bottom: 3.w, bottom: 3.w,
), ),
decoration: BoxDecoration( decoration: const BoxDecoration(
image: DecorationImage( image: DecorationImage(
image: AssetImage(getBg("")), image: AssetImage(
"sc_images/room/sc_icon_room_gift_left_no_vip_bg.png",
),
fit: BoxFit.fill, fit: BoxFit.fill,
), ),
), ),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
netImage( netImage(
url: gift.sendUserPic, url: snapshot.sendUserPic,
shape: BoxShape.circle, shape: BoxShape.circle,
width: 26.w, width: 26.w,
height: 26.w, height: 26.w,
@ -114,11 +201,11 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
children: [ children: [
socialchatNickNameText( socialchatNickNameText(
maxWidth: 88.w, maxWidth: 88.w,
gift.sendUserName, snapshot.sendUserName,
fontSize: 12.sp, fontSize: 12.sp,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
type: "", type: "",
needScroll: gift.sendUserName.characters.length > 8, needScroll: snapshot.sendUserName.characters.length > 8,
), ),
SizedBox(height: 1.w), SizedBox(height: 1.w),
Row( Row(
@ -133,7 +220,7 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
), ),
Flexible( Flexible(
child: Text( child: Text(
gift.sendToUserName, snapshot.sendToUserName,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
@ -165,25 +252,25 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
netImage( netImage(
url: gift.giftPic, url: snapshot.giftPic,
fit: BoxFit.cover, fit: BoxFit.cover,
borderRadius: BorderRadius.circular(4.w), borderRadius: BorderRadius.circular(4.w),
width: 34.w, width: 34.w,
height: 34.w, height: 34.w,
), ),
if (gift.giftCount > 0) ...[ if (snapshot.giftCount > 0) ...[
SizedBox(width: 8.w), SizedBox(width: 8.w),
Flexible( Flexible(
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: _buildGiftCountLabel(gift, animatedSize), child: _buildGiftCountLabel(snapshot.giftCount),
), ),
), ),
], ],
if (showLuckyRewardFrame) ...[ if (showLuckyRewardFrame) ...[
SizedBox(width: 6.w), SizedBox(width: 6.w),
_buildLuckyGiftRewardFrame(gift), _buildLuckyGiftRewardFrame(),
], ],
], ],
), ),
@ -194,9 +281,7 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
); );
} }
Widget _buildGiftCountLabel(LGiftModel gift, double animatedSize) { Widget _buildGiftCountLabel(num giftCount) {
final xFontSize = animatedSize;
final countFontSize = animatedSize;
return Directionality( return Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Row( child: Row(
@ -206,7 +291,7 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
Text( Text(
"x", "x",
style: TextStyle( style: TextStyle(
fontSize: xFontSize, fontSize: animatedSize,
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
color: const Color(0xFFFFD400), color: const Color(0xFFFFD400),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -214,10 +299,11 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
), ),
), ),
SizedBox(width: 2.w), SizedBox(width: 2.w),
Text( _AnimatedGiftCountText(
_giftCountText(gift.giftCount), targetCount: giftCount,
stepUnit: snapshot.giftCountStepUnit,
style: TextStyle( style: TextStyle(
fontSize: countFontSize, fontSize: animatedSize,
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
color: const Color(0xFFFFD400), color: const Color(0xFFFFD400),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -229,21 +315,23 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
); );
} }
Widget _buildLuckyGiftRewardFrame(LGiftModel gift) { Widget _buildLuckyGiftRewardFrame() {
return SizedBox( return SizedBox(
width: 108.w, width: 108.w,
height: 52.w, height: 52.w,
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
SCSvgaAssetWidget( RepaintBoundary(
child: SCSvgaAssetWidget(
assetPath: _luckyGiftRewardFrameAssetPath, assetPath: _luckyGiftRewardFrameAssetPath,
width: 108.w, width: 108.w,
height: 52.w, height: 52.w,
fit: BoxFit.cover, fit: BoxFit.cover,
loop: true, loop: true,
allowDrawingOverflow: true, allowDrawingOverflow: true,
fallback: _buildLuckyGiftRewardFrameFallback(), fallback: const _LuckyGiftRewardFrameFallback(),
),
), ),
Padding( Padding(
padding: EdgeInsetsDirectional.only( padding: EdgeInsetsDirectional.only(
@ -254,7 +342,7 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
child: Text( child: Text(
"+${_giftRewardAmountText(gift)}", "+${_giftRewardAmountText()}",
maxLines: 1, maxLines: 1,
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
@ -278,7 +366,26 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
); );
} }
Widget _buildLuckyGiftRewardFrameFallback() { String _giftRewardAmountText() {
final rewardAmount = snapshot.rewardAmount;
if (rewardAmount > 0) {
if (rewardAmount > 9999) {
return "${(rewardAmount / 1000).toStringAsFixed(0)}k";
}
if (rewardAmount % 1 == 0) {
return rewardAmount.toInt().toString();
}
return rewardAmount.toString();
}
return snapshot.rewardAmountText;
}
}
class _LuckyGiftRewardFrameFallback extends StatelessWidget {
const _LuckyGiftRewardFrameFallback();
@override
Widget build(BuildContext context) {
return Container( return Container(
width: 108.w, width: 108.w,
height: 52.w, height: 52.w,
@ -300,118 +407,154 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
), ),
); );
} }
}
String _giftCountText(num count) { class _AnimatedGiftCountText extends StatefulWidget {
return count % 1 == 0 ? count.toInt().toString() : count.toString(); const _AnimatedGiftCountText({
} required this.targetCount,
required this.stepUnit,
required this.style,
});
String _giftRewardAmountText(LGiftModel gift) { final num targetCount;
final rewardAmount = gift.rewardAmount; final num stepUnit;
if (rewardAmount > 0) { final TextStyle style;
if (rewardAmount > 9999) {
return "${(rewardAmount / 1000).toStringAsFixed(0)}k";
}
if (rewardAmount % 1 == 0) {
return rewardAmount.toInt().toString();
}
return rewardAmount.toString();
}
return gift.rewardAmountText;
}
@override @override
void dispose() { State<_AnimatedGiftCountText> createState() => _AnimatedGiftCountTextState();
_giftAnimationManager.cleanupAnimationResources(); }
super.dispose();
} class _AnimatedGiftCountTextState extends State<_AnimatedGiftCountText> {
Timer? _stepTimer;
late double _displayValue;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_giftAnimationManager = Provider.of<GiftAnimationManager>( _displayValue = _resolveInitialDisplayValue();
context, WidgetsBinding.instance.addPostFrameCallback((_) {
listen: false, if (!mounted) {
); return;
initAnimal(); }
_startSteppedAnimation(widget.targetCount.toDouble());
});
} }
void initAnimal() { @override
List<LGiftScrollingScreenAnimsBean> beans = []; void didUpdateWidget(covariant _AnimatedGiftCountText oldWidget) {
double top = 60; super.didUpdateWidget(oldWidget);
for (int i = 0; i < 4; i++) { if (oldWidget.targetCount == widget.targetCount &&
var bean = LGiftScrollingScreenAnimsBean(); oldWidget.stepUnit == widget.stepUnit) {
var controller = AnimationController( return;
value: 0,
duration: const Duration(milliseconds: 5000),
vsync: this,
);
bean.controller = controller;
bean.luckyGiftPinnedMargin = EdgeInsets.only(top: top);
// bean.transverseAnimation = Tween<Offset>(
// begin: Offset(ScreenUtil().screenWidth, 0),
// end: Offset(0, 0),
// ).animate(
// CurvedAnimation(
// parent: controller,
// curve: Interval(0.0, 0.45, curve: Curves.ease),
// ),
// );
bean.verticalAnimation = EdgeInsetsTween(
begin: EdgeInsets.only(top: top),
end: EdgeInsets.only(top: 0),
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(0.2, 1, curve: Curves.easeOut),
),
);
bean.sizeAnimation = Tween<double>(begin: 0, end: 22).animate(
CurvedAnimation(
parent: controller,
curve: Interval(0.45, 0.55, curve: Curves.ease),
),
);
beans.add(bean);
top = top + 70;
} }
beans[0].controller.addStatusListener((state) { _startSteppedAnimation(widget.targetCount.toDouble());
if (state == AnimationStatus.completed) {
//
_giftAnimationManager.markAnimationAsFinished(0);
}
});
beans[1].controller.addStatusListener((state) {
if (state == AnimationStatus.completed) {
//
_giftAnimationManager.markAnimationAsFinished(1);
}
});
beans[2].controller.addStatusListener((state) {
if (state == AnimationStatus.completed) {
//
_giftAnimationManager.markAnimationAsFinished(2);
}
});
beans[3].controller.addStatusListener((state) {
if (state == AnimationStatus.completed) {
//
_giftAnimationManager.markAnimationAsFinished(3);
}
});
_giftAnimationManager.attachAnimationControllers(beans);
} }
String getBg(String type) { @override
return "sc_images/room/sc_icon_room_gift_left_no_vip_bg.png"; void dispose() {
_stepTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text(_formatGiftCount(_resolvedDisplayCount()), style: widget.style);
}
double _resolveInitialDisplayValue() {
final targetValue = widget.targetCount.toDouble();
final stepUnit = _resolveStepUnit(targetValue.abs());
if (targetValue <= 0 || stepUnit <= 0) {
return targetValue;
}
return targetValue <= stepUnit ? targetValue : stepUnit;
}
void _startSteppedAnimation(double nextValue) {
_stepTimer?.cancel();
final delta = nextValue - _displayValue;
if (delta.abs() < 0.0001) {
return;
}
if (delta < 0) {
if (!mounted) {
return;
}
setState(() {
_displayValue = nextValue;
});
return;
}
final stepUnit = _resolveStepUnit(delta.abs());
if (stepUnit <= 0) {
if (!mounted) {
return;
}
setState(() {
_displayValue = nextValue;
});
return;
}
var tickCount = (delta.abs() / stepUnit).ceil();
if (tickCount <= 1) {
if (!mounted) {
return;
}
setState(() {
_displayValue = nextValue;
});
return;
}
tickCount = tickCount.clamp(2, 40);
final increment = delta / tickCount;
final intervalMs = ((700 / tickCount).round()).clamp(28, 80);
var completedTicks = 0;
_stepTimer = Timer.periodic(Duration(milliseconds: intervalMs), (timer) {
if (!mounted) {
timer.cancel();
return;
}
completedTicks += 1;
final reachedEnd = completedTicks >= tickCount;
setState(() {
_displayValue = reachedEnd ? nextValue : (_displayValue + increment);
});
if (reachedEnd) {
timer.cancel();
_stepTimer = null;
}
});
}
double _resolveStepUnit(double delta) {
final candidate = widget.stepUnit.abs().toDouble();
if (candidate <= 0) {
return delta;
}
return candidate.clamp(0.0001, delta);
}
num _resolvedDisplayCount() {
if (widget.targetCount % 1 == 0) {
return _displayValue.round();
}
return double.parse(_displayValue.toStringAsFixed(1));
} }
} }
String _formatGiftCount(num count) {
return count % 1 == 0 ? count.toInt().toString() : count.toString();
}
class LGiftScrollingScreenAnimsBean { class LGiftScrollingScreenAnimsBean {
// late Animation<Offset> transverseAnimation;
late Animation<EdgeInsets> verticalAnimation; late Animation<EdgeInsets> verticalAnimation;
late EdgeInsets luckyGiftPinnedMargin; late EdgeInsets luckyGiftPinnedMargin;
late AnimationController controller; late AnimationController controller;
late AnimationController countPulseController;
late Animation<double> sizeAnimation; late Animation<double> sizeAnimation;
Timer? dismissTimer;
} }
class LGiftModel { class LGiftModel {
@ -444,4 +587,7 @@ class LGiftModel {
//id //id
String labelId = ""; String labelId = "";
// 1 1 25 25
num giftCountStepUnit = 1;
} }

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
@ -7,6 +9,8 @@ import 'package:yumi/ui_kit/components/text/sc_text.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/services/audio/rtm_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'; import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
class RoomAnimationQueueScreen extends StatefulWidget { class RoomAnimationQueueScreen extends StatefulWidget {
@ -36,7 +40,22 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
if (!_effectsEnabled) { if (!_effectsEnabled) {
return; return;
} }
unawaited(
warmImageResource(
msg.user?.userAvatar ?? "",
logicalWidth: 28.w,
logicalHeight: 28.w,
),
);
SCRoomEffectScheduler().scheduleDeferredEffect(
debugLabel: 'room_entrance_join',
action: () {
if (!_effectsEnabled || !mounted) {
return;
}
_addToQueue(msg); _addToQueue(msg);
},
);
}); });
} }
@ -44,6 +63,7 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
if (!_effectsEnabled || !mounted) { if (!_effectsEnabled || !mounted) {
return; return;
} }
bool shouldStartProcessing = false;
setState(() { setState(() {
final taskId = DateTime.now().millisecondsSinceEpoch; final taskId = DateTime.now().millisecondsSinceEpoch;
final animationKey = GlobalKey<_RoomEntranceAnimationState>(); final animationKey = GlobalKey<_RoomEntranceAnimationState>();
@ -72,10 +92,13 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
); );
_animationQueue.add(task); _animationQueue.add(task);
if (!_isQueueProcessing && _animationQueue.isNotEmpty) {
_isQueueProcessing = true;
shouldStartProcessing = true;
}
}); });
if (!_isQueueProcessing && _animationQueue.isNotEmpty) { if (shouldStartProcessing) {
setState(() => _isQueueProcessing = true);
_startNextAnimation(); _startNextAnimation();
} }
} }
@ -267,7 +290,8 @@ class _RoomEntranceAnimationState extends State<RoomEntranceAnimation>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SlideTransition( return RepaintBoundary(
child: SlideTransition(
position: _offsetAnimation, position: _offsetAnimation,
child: Container( child: Container(
width: ScreenUtil().screenWidth * 0.8, width: ScreenUtil().screenWidth * 0.8,
@ -302,7 +326,8 @@ class _RoomEntranceAnimationState extends State<RoomEntranceAnimation>
fontSize: 12.sp, fontSize: 12.sp,
type: "", type: "",
needScroll: needScroll:
(widget.msg.user?.userNickname?.characters.length ?? 0) > (widget.msg.user?.userNickname?.characters.length ??
0) >
15, 15,
), ),
text( text(
@ -316,6 +341,7 @@ class _RoomEntranceAnimationState extends State<RoomEntranceAnimation>
], ],
), ),
), ),
),
); );
} }

View File

@ -1,5 +1,4 @@
import 'dart:collection'; import 'dart:collection';
import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -212,6 +211,8 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
static const int _maxQueuedRequests = 24; static const int _maxQueuedRequests = 24;
final Queue<_QueuedRoomGiftSeatFlightRequest> _queue = Queue(); final Queue<_QueuedRoomGiftSeatFlightRequest> _queue = Queue();
final Map<String, ImageProvider<Object>> _imageProviderCache =
<String, ImageProvider<Object>>{};
final GlobalKey _overlayKey = GlobalKey(); final GlobalKey _overlayKey = GlobalKey();
late final AnimationController _controller; late final AnimationController _controller;
@ -287,6 +288,7 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
_activeRequest = null; _activeRequest = null;
_activeImageProvider = null; _activeImageProvider = null;
_activeTargetOffset = null; _activeTargetOffset = null;
_imageProviderCache.clear();
_isPlaying = false; _isPlaying = false;
return; return;
} }
@ -296,6 +298,7 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
_activeRequest = null; _activeRequest = null;
_activeImageProvider = null; _activeImageProvider = null;
_activeTargetOffset = null; _activeTargetOffset = null;
_imageProviderCache.clear();
_isPlaying = false; _isPlaying = false;
}); });
} }
@ -495,17 +498,30 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
} }
ImageProvider<Object> _resolveImageProvider(String imagePath) { ImageProvider<Object> _resolveImageProvider(String imagePath) {
if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) { final currentRequest = _activeRequest ?? _centerRequest;
return buildCachedImageProvider(imagePath); final logicalSize =
currentRequest == null
? null
: currentRequest.beginSize > currentRequest.endSize
? currentRequest.beginSize
: currentRequest.endSize;
final cacheKey = '$imagePath|${logicalSize?.toStringAsFixed(2) ?? "auto"}';
final cachedProvider = _imageProviderCache[cacheKey];
if (cachedProvider != null) {
return cachedProvider;
} }
if (imagePath.startsWith('/')) { final provider = buildCachedImageProvider(
return FileImage(File(imagePath)); imagePath,
} logicalWidth: logicalSize,
return AssetImage(imagePath); logicalHeight: logicalSize,
);
_imageProviderCache[cacheKey] = provider;
return provider;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final overlaySize = MediaQuery.sizeOf(context);
return IgnorePointer( return IgnorePointer(
child: SizedBox.expand( child: SizedBox.expand(
key: _overlayKey, key: _overlayKey,
@ -518,11 +534,6 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
: AnimatedBuilder( : AnimatedBuilder(
animation: _controller, animation: _controller,
builder: (context, child) { builder: (context, child) {
final renderBox =
_overlayKey.currentContext?.findRenderObject()
as RenderBox?;
final overlaySize =
renderBox?.size ?? MediaQuery.sizeOf(context);
final center = overlaySize.center(Offset.zero); final center = overlaySize.center(Offset.zero);
final currentCenterRequest = final currentCenterRequest =
_centerRequest ?? _activeRequest; _centerRequest ?? _activeRequest;
@ -576,7 +587,9 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
); );
} }
return Stack(clipBehavior: Clip.none, children: children); return RepaintBoundary(
child: Stack(clipBehavior: Clip.none, children: children),
);
}, },
), ),
), ),

View File

@ -277,9 +277,11 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
provider.isMic = !provider.isMic; provider.isMic = !provider.isMic;
provider.roomWheatMap.forEach((k, v) { provider.roomWheatMap.forEach((k, v) {
if (v.user?.id == final seat = provider.micAtIndexForDisplay(k);
AccountStorage().getCurrentUser()?.userProfile?.id && if ((seat?.user?.id ?? "").trim() ==
!provider.roomWheatMap[k]!.micMute!) { (AccountStorage().getCurrentUser()?.userProfile?.id ?? "")
.trim() &&
!(seat?.micMute ?? true)) {
if (!provider.isMic) { if (!provider.isMic) {
provider.engine?.adjustRecordingSignalVolume(100); provider.engine?.adjustRecordingSignalVolume(100);
provider.engine?.setClientRole( provider.engine?.setClientRole(
@ -313,15 +315,7 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
} }
bool _shouldShowMic(RtcProvider provider) { bool _shouldShowMic(RtcProvider provider) {
var show = false; return provider.isOnMai();
provider.roomWheatMap.forEach((k, v) {
if (v.user?.id == AccountStorage().getCurrentUser()?.userProfile?.id) {
show = true;
}
});
return show;
} }
} }

View File

@ -3,6 +3,21 @@
## 当前总目标 ## 当前总目标
- 控制当前 Flutter Android 发包体积,持续定位冗余组件、超大资源和不合理构建配置,并把每一步处理结果落盘记录。 - 控制当前 Flutter Android 发包体积,持续定位冗余组件、超大资源和不合理构建配置,并把每一步处理结果落盘记录。
## 本轮房间动效优化(进行中)
- 已按 2026-04-21 当前房间卡顿优化方案先落第一步“统一调度层”:新增房间特效调度器,在全屏 `VAP/SVGA` 高成本特效播放或排队期间,暂缓低优先级的房间进场动画、全局飘屏和座位飞屏入队,待高成本特效清空后按小间隔续播,先减少多条动画链路同一时刻抢主线程的情况。
- 本步只处理“调度时机”,没有改动现有动画内部逻辑:礼物/飞屏/飘屏的现有倍率触发规则、现有上限截断、`customAnimationCount` 现有循环方式和当前视觉表现均保持原样,避免第一步就引入联动回归。
- 已继续落第二步“局部重建/重绘隔离”:房间礼物播报条已从整层 `Consumer` 重建改成每个动画槽位独立 `Selector` 订阅,并把运行态数据拍平成快照,减少 `notifyListeners()` 时 4 个槽位整层一起重建;同时为礼物播报条、进场动画、座位飞屏和全局飘屏 overlay 补充 `RepaintBoundary`,先把高频位移动画和底层页面绘制隔离开,降低同屏重绘面积。
- 第二步同样没有改动任何动画业务规则:礼物播报条合并策略、幸运礼物奖励展示、现有队列时长、`customAnimationCount` 的逐个入队方式、现有上限和触发条件都保持原样,这一步只收敛 rebuild / repaint 成本。
- 已顺手补上高成本特效资源预热接线:当前全屏 `VAP/SVGA` 任务一入队就会触发已有的 preload 逻辑,让排队中的特效在前一个播放期间优先完成 `svga decode / 网络文件落缓存`,减少切到下一段时的首帧等待;这一步复用现有预热能力,不改变任何播放顺序和触发条件。
- 已继续补上图片链路的“按展示尺寸解码 + 排队阶段预热”:`buildCachedImageProvider` 现已支持 `network/file/asset` 三类资源统一按逻辑尺寸生成 provider并通过 `ResizeImage` 把解码尺寸压到实际展示大小;同时房间礼物播报条、进场动画、全局飘屏与座位飞屏在入队时会对头像/礼物图做异步预热,减少正式播到该条动画时首次解码和图片闪一下的概率。
- 这一轮同样没有改动任何动画规则或队列行为:只补资源命中率和解码尺寸控制,不变更现有播放次数、上限、倍率阈值、`customAnimationCount` 逐个飞行方式和当前视觉逻辑。
- 已继续收口座位飞屏的图片命中链路:当前在真正批量入队逐个飞行前,会先按飞行动画的大图尺寸预热一次礼物图;同时飞屏 overlay 内部会复用同路径同尺寸的 `ImageProvider`,减少 burst 场景里同一张礼物图连续多次构造 provider 和重复命中解析链路的开销。
- 已继续专项收口“顶部礼物信息播报”的连击卡顿:当前已把礼物播报条的“整条入场动画”和“右侧 `xN` 数量脉冲/续命”拆成独立状态,连击到来时不再通过重启整条 5 秒控制器来硬续播,而是只刷新数量脉冲并延长当前播报条的空闲存活时间;同时右侧数量已改为从当前显示值平滑补间到目标值,减少高频连击或本地批量合并时出现的数字跳变、断档和忽大忽小。
- 这一轮同样没有改礼物业务判断口径:未调整礼物连击的触发条件、上限、截断、倍率门槛和 `customAnimationCount` 等既有规则,只优化顶部播报条自身的显示状态机与数量刷新方式。
- 已继续修正连击播报条“单点送 1 个却直接跳 `+5`”的问题:顶部礼物播报条现已接入 `customAnimationCount` 的点击粒度信息,数量动画不再只按合并后的总量做补间,而是按“单次点击对应的数量步长”逐步递进;例如连续点击 5 次、每次送 1 个时,会按 `+1 +1 +1 +1 +1` 的视觉节奏补齐,而不是直接把右侧数量跳成 `+5`
- 已继续修正连击档位特效偶发“不播”的问题:当前连击 `SVGA` 触发已改为只命中仓库里真实存在资源的档位集合,不再把 `20/88/200...` 这类当前工程里没有对应特效文件的档位当成可播放资源;同时命中判断会按“本次新增后跨过的最高有效资源档位”来触发,避免因为本地批量窗口把多次点击合成一包后,先命中到空资源路径而看起来像整段特效都没播。
- 已按 2026-04-21 最新回归继续修正“飞向麦位的自制动画”缺失问题:座位飞屏现已从房间统一调度器的延后队列中移出,恢复为收到礼物消息后直接入队到全局 `RoomGiftSeatFlightOverlay`;保留既有的图片预热、同会话队列上限和 `customAnimationCount` 逐个飞行逻辑,只撤回此前对这一核心反馈动画的延迟调度,避免用户在礼物命中全屏特效或调度状态未及时清空时误以为飞屏动画消失。
## 本轮启动优化(非网络) ## 本轮启动优化(非网络)
- 已补回启动页正式展示逻辑:当前 `Weekly Star / Last Weekly CP` 两套自定义启动页都改为“本地有缓存才展示、无缓存不展示”;由于现阶段周榜与 CP 榜接口链路都还未 ready缓存刷新逻辑已先关闭所以当前启动阶段会直接回退到默认 splash不再展示这两套定制视觉稿。相关恢复入口已在缓存类里用 `api-ready-launch-splash` 注释标记,后续接口 ready 后可直接搜索接回。 - 已补回启动页正式展示逻辑:当前 `Weekly Star / Last Weekly CP` 两套自定义启动页都改为“本地有缓存才展示、无缓存不展示”;由于现阶段周榜与 CP 榜接口链路都还未 ready缓存刷新逻辑已先关闭所以当前启动阶段会直接回退到默认 splash不再展示这两套定制视觉稿。相关恢复入口已在缓存类里用 `api-ready-launch-splash` 注释标记,后续接口 ready 后可直接搜索接回。
- 已将启动页展示时间收敛为 `3` 秒,并在右上角新增通用 `skip 倒计时` 按钮:当前按钮会按秒级动态展示剩余时间,点击可立即跳过;文案已补齐 `en/ar/tr/bn` 多语言翻译,并按 locale 输出倒计时文本,便于后续继续做 RTL 语言验收。 - 已将启动页展示时间收敛为 `3` 秒,并在右上角新增通用 `skip 倒计时` 按钮:当前按钮会按秒级动态展示剩余时间,点击可立即跳过;文案已补齐 `en/ar/tr/bn` 多语言翻译,并按 locale 输出倒计时文本,便于后续继续做 RTL 语言验收。
@ -181,11 +196,18 @@
- `lib/shared/data_sources/models/enum/sc_gift_type.dart` - `lib/shared/data_sources/models/enum/sc_gift_type.dart`
- `lib/shared/tools/sc_network_image_utils.dart` - `lib/shared/tools/sc_network_image_utils.dart`
- `lib/shared/tools/sc_gift_vap_svga_manager.dart` - `lib/shared/tools/sc_gift_vap_svga_manager.dart`
- `lib/shared/tools/sc_room_effect_scheduler.dart`
- `lib/services/general/sc_app_general_manager.dart` - `lib/services/general/sc_app_general_manager.dart`
- `lib/shared/data_sources/sources/local/floating_screen_manager.dart`
- `lib/ui_kit/widgets/room/floating/floating_gift_screen_widget.dart` - `lib/ui_kit/widgets/room/floating/floating_gift_screen_widget.dart`
- `lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart` - `lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart`
- `lib/services/room/rc_room_manager.dart` - `lib/services/room/rc_room_manager.dart`
- `lib/services/gift/gift_animation_manager.dart`
- `lib/services/audio/rtc_manager.dart` - `lib/services/audio/rtc_manager.dart`
- `lib/modules/room/voice_room_page.dart`
- `lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart`
- `lib/ui_kit/widgets/room/anim/room_entrance_screen.dart`
- `lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart`
- `lib/modules/room/edit/room_edit_page.dart` - `lib/modules/room/edit/room_edit_page.dart`
- `lib/modules/user/edit/edit_user_info_page2.dart` - `lib/modules/user/edit/edit_user_info_page2.dart`
- `lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart` - `lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart`