修复一些已知bug
This commit is contained in:
parent
7c7b205ffb
commit
1b0dd6f3ea
@ -231,7 +231,7 @@ class _SeatRenderSnapshot {
|
||||
});
|
||||
|
||||
factory _SeatRenderSnapshot.fromProvider(RtcProvider provider, num index) {
|
||||
final roomSeat = provider.roomWheatMap[index];
|
||||
final roomSeat = provider.micAtIndexForDisplay(index);
|
||||
final user = roomSeat?.user;
|
||||
return _SeatRenderSnapshot(
|
||||
isExitingCurrentVoiceRoomSession:
|
||||
@ -398,8 +398,9 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<RtcProvider, _SeatEmojiSnapshot>(
|
||||
selector:
|
||||
(context, provider) =>
|
||||
_SeatEmojiSnapshot.fromMic(provider.roomWheatMap[widget.index]),
|
||||
(context, provider) => _SeatEmojiSnapshot.fromMic(
|
||||
provider.micAtIndexForDisplay(widget.index),
|
||||
),
|
||||
builder: (
|
||||
BuildContext context,
|
||||
_SeatEmojiSnapshot snapshot,
|
||||
|
||||
@ -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/audio/rtm_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_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/room_gift_seat_flight_overlay.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/anim/room_entrance_screen.dart';
|
||||
@ -138,6 +140,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
RoomEntranceHelper.clearQueue();
|
||||
_clearLuckyGiftComboSessions();
|
||||
_giftSeatFlightController.clear();
|
||||
SCRoomEffectScheduler().clearDeferredTasks(reason: 'voice_room_suspend');
|
||||
SCGiftVapSvgaManager().stopPlayback();
|
||||
}
|
||||
|
||||
@ -355,6 +358,21 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
giftModel.sendUserPic = msg.user?.userAvatar ?? "";
|
||||
giftModel.giftPic = msg.gift?.giftPhoto ?? "";
|
||||
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>(
|
||||
context,
|
||||
listen: false,
|
||||
@ -398,6 +416,20 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
giftModel.rewardAmount = awardAmount;
|
||||
giftModel.showLuckyRewardFrame = true;
|
||||
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>(
|
||||
context,
|
||||
listen: false,
|
||||
@ -443,11 +475,13 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
);
|
||||
session.endTimer?.cancel();
|
||||
session.clearQueueTimer?.cancel();
|
||||
final previousTotal = session.totalCount;
|
||||
session.totalCount += quantity;
|
||||
|
||||
final highestMilestone =
|
||||
SocialChatGiftSystemManager.resolveHighestReachedComboMilestone(
|
||||
session.totalCount,
|
||||
SocialChatGiftSystemManager.resolveHighestCrossedComboEffectMilestone(
|
||||
previousCount: previousTotal,
|
||||
currentCount: session.totalCount,
|
||||
);
|
||||
if (highestMilestone != null &&
|
||||
highestMilestone > session.highestPlayedMilestone &&
|
||||
@ -527,12 +561,28 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
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({
|
||||
required String sessionKey,
|
||||
required String giftPhoto,
|
||||
required String targetUserId,
|
||||
required int animationCount,
|
||||
}) {
|
||||
unawaited(
|
||||
warmImageResource(giftPhoto, logicalWidth: 96.w, logicalHeight: 96.w),
|
||||
);
|
||||
final normalizedAnimationCount = math.max(animationCount, 1);
|
||||
final cappedAnimationCount = math.min(
|
||||
normalizedAnimationCount,
|
||||
|
||||
@ -48,7 +48,7 @@ typedef RtcProvider = RealTimeCommunicationManager;
|
||||
class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
static const Duration _micListPollingInterval = Duration(seconds: 2);
|
||||
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(
|
||||
milliseconds: 900,
|
||||
);
|
||||
@ -66,6 +66,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
num? _preferredSelfMicIndex;
|
||||
num? _pendingSelfMicSourceIndex;
|
||||
int? _pendingSelfMicSwitchGuardUntilMs;
|
||||
num? _pendingSelfMicReleaseIndex;
|
||||
int? _pendingSelfMicReleaseGuardUntilMs;
|
||||
ClientRoleType? _lastAppliedClientRole;
|
||||
bool? _lastAppliedLocalAudioMuted;
|
||||
bool? _lastScheduledVoiceLiveOnMic;
|
||||
@ -285,10 +287,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
required num sourceIndex,
|
||||
required num targetIndex,
|
||||
}) {
|
||||
_clearSelfMicReleaseGuard();
|
||||
_preferredSelfMicIndex = targetIndex;
|
||||
_pendingSelfMicSourceIndex = sourceIndex;
|
||||
_pendingSelfMicSwitchGuardUntilMs =
|
||||
DateTime.now().add(_selfMicSwitchGracePeriod).millisecondsSinceEpoch;
|
||||
DateTime.now().add(_selfMicStateGracePeriod).millisecondsSinceEpoch;
|
||||
}
|
||||
|
||||
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> nextMap, {
|
||||
required Map<num, MicRes> previousMap,
|
||||
@ -308,14 +346,13 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
if (currentUserId.isEmpty) {
|
||||
return nextMap;
|
||||
}
|
||||
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
final targetIndex = _preferredSelfMicIndex;
|
||||
final sourceIndex = _pendingSelfMicSourceIndex;
|
||||
final guardUntilMs = _pendingSelfMicSwitchGuardUntilMs ?? 0;
|
||||
final hasActiveGuard =
|
||||
targetIndex != null &&
|
||||
sourceIndex != null &&
|
||||
guardUntilMs > DateTime.now().millisecondsSinceEpoch;
|
||||
targetIndex != null && sourceIndex != null && guardUntilMs > nowMs;
|
||||
final selfSeatIndices =
|
||||
nextMap.entries
|
||||
.where((entry) => entry.value.user?.id == currentUserId)
|
||||
@ -330,63 +367,90 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
if (!hasActiveGuard) {
|
||||
_clearSelfMicSwitchGuard();
|
||||
return nextMap;
|
||||
}
|
||||
final resolvedTargetIndex = targetIndex;
|
||||
final resolvedSourceIndex = sourceIndex;
|
||||
} else {
|
||||
final resolvedTargetIndex = targetIndex;
|
||||
final resolvedSourceIndex = sourceIndex;
|
||||
|
||||
final selfOnlyOnSource =
|
||||
selfSeatIndices.isEmpty ||
|
||||
selfSeatIndices.every((seatIndex) => seatIndex == resolvedSourceIndex);
|
||||
if (!selfOnlyOnSource) {
|
||||
final selfOnlyOnSource =
|
||||
selfSeatIndices.isEmpty ||
|
||||
selfSeatIndices.every(
|
||||
(seatIndex) => seatIndex == resolvedSourceIndex,
|
||||
);
|
||||
if (selfOnlyOnSource) {
|
||||
final optimisticTargetSeat = previousMap[resolvedTargetIndex];
|
||||
if (optimisticTargetSeat != null &&
|
||||
optimisticTargetSeat.user?.id == currentUserId) {
|
||||
final incomingTargetSeat = nextMap[resolvedTargetIndex];
|
||||
if (incomingTargetSeat?.user == null ||
|
||||
incomingTargetSeat?.user?.id == currentUserId) {
|
||||
final stabilizedMap = Map<num, MicRes>.from(nextMap);
|
||||
for (final seatIndex in selfSeatIndices) {
|
||||
if (seatIndex == resolvedTargetIndex) {
|
||||
continue;
|
||||
}
|
||||
final seat = stabilizedMap[seatIndex];
|
||||
if (seat == null) {
|
||||
continue;
|
||||
}
|
||||
stabilizedMap[seatIndex] = seat.copyWith(clearUser: true);
|
||||
}
|
||||
|
||||
final baseSeat = incomingTargetSeat ?? optimisticTargetSeat;
|
||||
stabilizedMap[resolvedTargetIndex] = baseSeat.copyWith(
|
||||
user: optimisticTargetSeat.user,
|
||||
micMute:
|
||||
incomingTargetSeat?.micMute ?? optimisticTargetSeat.micMute,
|
||||
micLock:
|
||||
incomingTargetSeat?.micLock ?? optimisticTargetSeat.micLock,
|
||||
roomToken:
|
||||
incomingTargetSeat?.roomToken ??
|
||||
optimisticTargetSeat.roomToken,
|
||||
emojiPath:
|
||||
(incomingTargetSeat?.emojiPath ?? "").isNotEmpty
|
||||
? incomingTargetSeat?.emojiPath
|
||||
: optimisticTargetSeat.emojiPath,
|
||||
type:
|
||||
(incomingTargetSeat?.type ?? "").isNotEmpty
|
||||
? incomingTargetSeat?.type
|
||||
: optimisticTargetSeat.type,
|
||||
number:
|
||||
(incomingTargetSeat?.number ?? "").isNotEmpty
|
||||
? incomingTargetSeat?.number
|
||||
: optimisticTargetSeat.number,
|
||||
);
|
||||
|
||||
return stabilizedMap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final releaseIndex = _pendingSelfMicReleaseIndex;
|
||||
final releaseGuardUntilMs = _pendingSelfMicReleaseGuardUntilMs ?? 0;
|
||||
final hasActiveReleaseGuard =
|
||||
releaseIndex != null && releaseGuardUntilMs > nowMs;
|
||||
if (!hasActiveReleaseGuard) {
|
||||
_clearSelfMicReleaseGuard();
|
||||
return nextMap;
|
||||
}
|
||||
|
||||
final optimisticTargetSeat = previousMap[resolvedTargetIndex];
|
||||
if (optimisticTargetSeat == null ||
|
||||
optimisticTargetSeat.user?.id != currentUserId) {
|
||||
if (selfSeatIndices.isEmpty) {
|
||||
return nextMap;
|
||||
}
|
||||
|
||||
final incomingTargetSeat = nextMap[resolvedTargetIndex];
|
||||
if (incomingTargetSeat?.user != null &&
|
||||
incomingTargetSeat?.user?.id != currentUserId) {
|
||||
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);
|
||||
for (final seatIndex in selfSeatIndices) {
|
||||
if (seatIndex == resolvedTargetIndex) {
|
||||
continue;
|
||||
}
|
||||
final seat = stabilizedMap[seatIndex];
|
||||
if (seat == null) {
|
||||
continue;
|
||||
}
|
||||
stabilizedMap[seatIndex] = seat.copyWith(clearUser: true);
|
||||
}
|
||||
|
||||
final baseSeat = incomingTargetSeat ?? optimisticTargetSeat;
|
||||
stabilizedMap[resolvedTargetIndex] = baseSeat.copyWith(
|
||||
user: optimisticTargetSeat.user,
|
||||
micMute: incomingTargetSeat?.micMute ?? optimisticTargetSeat.micMute,
|
||||
micLock: incomingTargetSeat?.micLock ?? optimisticTargetSeat.micLock,
|
||||
roomToken:
|
||||
incomingTargetSeat?.roomToken ?? optimisticTargetSeat.roomToken,
|
||||
emojiPath:
|
||||
(incomingTargetSeat?.emojiPath ?? "").isNotEmpty
|
||||
? incomingTargetSeat?.emojiPath
|
||||
: optimisticTargetSeat.emojiPath,
|
||||
type:
|
||||
(incomingTargetSeat?.type ?? "").isNotEmpty
|
||||
? incomingTargetSeat?.type
|
||||
: optimisticTargetSeat.type,
|
||||
number:
|
||||
(incomingTargetSeat?.number ?? "").isNotEmpty
|
||||
? incomingTargetSeat?.number
|
||||
: optimisticTargetSeat.number,
|
||||
);
|
||||
|
||||
stabilizedMap[releaseIndex] = releaseSeat!.copyWith(clearUser: true);
|
||||
return stabilizedMap;
|
||||
}
|
||||
|
||||
@ -1208,6 +1272,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
needUpDataUserInfo = false;
|
||||
SCRoomUtils.roomUsersMap.clear();
|
||||
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
||||
_clearSelfMicReleaseGuard();
|
||||
roomIsMute = false;
|
||||
rtmProvider?.roomAllMsgList.clear();
|
||||
rtmProvider?.roomChatMsgList.clear();
|
||||
@ -1405,6 +1470,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
final isSeatSwitching =
|
||||
previousSelfSeatIndex > -1 && previousSelfSeatIndex != targetIndex;
|
||||
if (myUser != null) {
|
||||
_clearSelfMicReleaseGuard();
|
||||
if (isSeatSwitching) {
|
||||
_startSelfMicSwitchGuard(
|
||||
sourceIndex: previousSelfSeatIndex,
|
||||
@ -1454,20 +1520,30 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
index,
|
||||
);
|
||||
|
||||
if (roomWheatMap[index]?.user?.id ==
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id) {
|
||||
final currentUserId =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id ?? "";
|
||||
final currentUserSeatIndex = userOnMaiInIndex(currentUserId);
|
||||
if (roomWheatMap[index]?.user?.id == currentUserId) {
|
||||
isMic = true;
|
||||
engine?.muteLocalAudioStream(true);
|
||||
}
|
||||
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
||||
if (currentUserSeatIndex > -1) {
|
||||
_startSelfMicReleaseGuard(sourceIndex: currentUserSeatIndex);
|
||||
} else {
|
||||
_clearSelfMicReleaseGuard();
|
||||
}
|
||||
SCHeartbeatUtils.cancelAnchorTimer();
|
||||
|
||||
/// 设置成主持人角色
|
||||
engine?.renewToken("");
|
||||
engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
|
||||
_clearUserFromSeats(AccountStorage().getCurrentUser()?.userProfile?.id);
|
||||
_clearUserFromSeats(currentUserId);
|
||||
notifyListeners();
|
||||
_refreshMicListSilently();
|
||||
requestMicrophoneListRefresh(
|
||||
notifyIfUnchanged: false,
|
||||
minInterval: const Duration(milliseconds: 350),
|
||||
);
|
||||
} catch (ex) {
|
||||
SCTts.show('Failed to leave the microphone, $ex');
|
||||
}
|
||||
@ -1510,23 +1586,41 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
///自己是否在麦上
|
||||
bool isOnMai() {
|
||||
return roomWheatMap.values
|
||||
.map((userWheat) => userWheat.user?.id)
|
||||
.toList()
|
||||
.contains(AccountStorage().getCurrentUser()?.userProfile?.id);
|
||||
final currentUserId =
|
||||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||||
if (currentUserId.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
for (final entry in roomWheatMap.entries) {
|
||||
if ((micAtIndexForDisplay(entry.key)?.user?.id ?? "").trim() ==
|
||||
currentUserId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
///自己是否在指定的麦上
|
||||
bool isOnMaiInIndex(num index) {
|
||||
return roomWheatMap[index]?.user?.id ==
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id;
|
||||
return (micAtIndexForDisplay(index)?.user?.id ?? "").trim() ==
|
||||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||||
}
|
||||
|
||||
///点击的用户在哪个麦上
|
||||
num userOnMaiInIndex(String userId) {
|
||||
final normalizedUserId = userId.trim();
|
||||
if (normalizedUserId.isEmpty) {
|
||||
return -1;
|
||||
}
|
||||
num index = -1;
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
|
||||
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 {
|
||||
static const int _maxPendingAnimations = 24;
|
||||
static const Duration _activeComboIdleDismissDelay = Duration(
|
||||
milliseconds: 3200,
|
||||
);
|
||||
|
||||
Queue<LGiftModel> pendingAnimationsQueue = Queue<LGiftModel>();
|
||||
List<LGiftScrollingScreenAnimsBean> animationControllerList = [];
|
||||
@ -16,6 +20,29 @@ class GiftAnimationManager extends ChangeNotifier {
|
||||
bool get _controllersReady =>
|
||||
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) {
|
||||
if (_mergeIntoActiveAnimation(giftModel)) {
|
||||
return;
|
||||
@ -42,9 +69,7 @@ class GiftAnimationManager extends ChangeNotifier {
|
||||
}
|
||||
_mergeGiftModel(target: current, incoming: incoming);
|
||||
notifyListeners();
|
||||
if (animationControllerList.length > entry.key) {
|
||||
animationControllerList[entry.key].controller.forward(from: 0.45);
|
||||
}
|
||||
_refreshSlotAnimation(entry.key, restartEntry: false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -87,6 +112,9 @@ class GiftAnimationManager extends ChangeNotifier {
|
||||
}
|
||||
target.showLuckyRewardFrame =
|
||||
target.showLuckyRewardFrame || incoming.showLuckyRewardFrame;
|
||||
if (incoming.giftCountStepUnit > 0) {
|
||||
target.giftCountStepUnit = incoming.giftCountStepUnit;
|
||||
}
|
||||
}
|
||||
|
||||
///开始播放
|
||||
@ -101,14 +129,14 @@ class GiftAnimationManager extends ChangeNotifier {
|
||||
giftMap[key] = playGift;
|
||||
pendingAnimationsQueue.removeFirst();
|
||||
notifyListeners();
|
||||
animationControllerList[key].controller.forward(from: 0);
|
||||
_refreshSlotAnimation(key, restartEntry: true);
|
||||
break;
|
||||
} else {
|
||||
if (value.labelId == playGift.labelId) {
|
||||
_mergeGiftModel(target: value, incoming: playGift);
|
||||
pendingAnimationsQueue.removeFirst();
|
||||
notifyListeners();
|
||||
animationControllerList[key].controller.forward(from: 0.45);
|
||||
_refreshSlotAnimation(key, restartEntry: false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -127,14 +155,125 @@ class GiftAnimationManager extends ChangeNotifier {
|
||||
giftMap[2] = null;
|
||||
giftMap[3] = null;
|
||||
for (var element in animationControllerList) {
|
||||
element.dismissTimer?.cancel();
|
||||
element.controller.dispose();
|
||||
element.countPulseController.dispose();
|
||||
}
|
||||
animationControllerList.clear();
|
||||
}
|
||||
|
||||
void markAnimationAsFinished(int index) {
|
||||
_cancelSlotDismissTimer(index);
|
||||
if (giftMap[index] == null) {
|
||||
return;
|
||||
}
|
||||
giftMap[index] = null;
|
||||
notifyListeners();
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -27,6 +27,8 @@ class SocialChatGiftSystemManager extends ChangeNotifier {
|
||||
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",
|
||||
};
|
||||
static final List<int> _luckGiftComboEffectMilestones =
|
||||
(_luckGiftComboEffectAssets.keys.toList()..sort());
|
||||
static const List<int> _luckGiftMilestones = <int>[
|
||||
10,
|
||||
20,
|
||||
@ -211,7 +213,7 @@ class SocialChatGiftSystemManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void startGiftAnimation() {
|
||||
final milestone = resolveHighestReachedComboMilestone(number);
|
||||
final milestone = resolveHighestReachedComboEffectMilestone(number);
|
||||
if (milestone == null || (isPlayed[milestone] ?? false)) {
|
||||
return;
|
||||
}
|
||||
@ -261,19 +263,45 @@ class SocialChatGiftSystemManager extends ChangeNotifier {
|
||||
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) {
|
||||
if (count % 1 != 0) {
|
||||
return null;
|
||||
}
|
||||
final normalizedCount = 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";
|
||||
return _luckGiftComboEffectAssets[count.toInt()];
|
||||
}
|
||||
|
||||
static String? resolveLuckGiftComboEffectPath(num count) {
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
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/ui_kit/widgets/room/floating/floating_game_screen_widget.dart';
|
||||
@ -33,8 +37,17 @@ class OverlayManager {
|
||||
void addMessage(SCFloatingMessage message) {
|
||||
if (_isDisposed) return;
|
||||
if (SCGlobalConfig.isFloatingAnimationInGlobal) {
|
||||
_messageQueue.add(message);
|
||||
_safeScheduleNext();
|
||||
unawaited(_warmMessageImages(message));
|
||||
SCRoomEffectScheduler().scheduleDeferredEffect(
|
||||
debugLabel: 'floating_message_type_${message.type ?? -1}',
|
||||
action: () {
|
||||
if (_isDisposed) {
|
||||
return;
|
||||
}
|
||||
_messageQueue.add(message);
|
||||
_safeScheduleNext();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
_messageQueue.clear();
|
||||
_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() {
|
||||
if (_isProcessing || _isPlaying || _messageQueue.isEmpty) return;
|
||||
_isProcessing = true;
|
||||
@ -103,7 +140,7 @@ class OverlayManager {
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, 70.w),
|
||||
child: _buildScreenWidget(message),
|
||||
child: RepaintBoundary(child: _buildScreenWidget(message)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -130,6 +130,7 @@ class BaseNetworkClient {
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParams,
|
||||
Map<String, dynamic>? extra,
|
||||
bool allowNullBody = false,
|
||||
required T Function(dynamic) fromJson,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
@ -140,6 +141,7 @@ class BaseNetworkClient {
|
||||
method: 'GET',
|
||||
queryParams: queryParams,
|
||||
extra: extra,
|
||||
allowNullBody: allowNullBody,
|
||||
fromJson: fromJson,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
@ -153,6 +155,7 @@ class BaseNetworkClient {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParams,
|
||||
Map<String, dynamic>? extra,
|
||||
bool allowNullBody = false,
|
||||
required T Function(dynamic) fromJson,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
@ -164,6 +167,7 @@ class BaseNetworkClient {
|
||||
data: data,
|
||||
queryParams: queryParams,
|
||||
extra: extra,
|
||||
allowNullBody: allowNullBody,
|
||||
fromJson: fromJson,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
@ -177,6 +181,7 @@ class BaseNetworkClient {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParams,
|
||||
Map<String, dynamic>? extra,
|
||||
bool allowNullBody = false,
|
||||
required T Function(dynamic) fromJson,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
@ -188,6 +193,7 @@ class BaseNetworkClient {
|
||||
data: data,
|
||||
queryParams: queryParams,
|
||||
extra: extra,
|
||||
allowNullBody: allowNullBody,
|
||||
fromJson: fromJson,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
@ -201,6 +207,7 @@ class BaseNetworkClient {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParams,
|
||||
Map<String, dynamic>? extra,
|
||||
bool allowNullBody = false,
|
||||
required T Function(dynamic) fromJson,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
@ -212,6 +219,7 @@ class BaseNetworkClient {
|
||||
data: data,
|
||||
queryParams: queryParams,
|
||||
extra: extra,
|
||||
allowNullBody: allowNullBody,
|
||||
fromJson: fromJson,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
@ -226,6 +234,7 @@ class BaseNetworkClient {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParams,
|
||||
Map<String, dynamic>? extra,
|
||||
bool allowNullBody = false,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
@ -251,6 +260,8 @@ class BaseNetworkClient {
|
||||
if (baseResponse.success) {
|
||||
if (baseResponse.body != null) {
|
||||
return baseResponse.body!;
|
||||
} else if (allowNullBody && null is T) {
|
||||
return null as T;
|
||||
} else {
|
||||
if (T == bool) return false as T;
|
||||
if (T == int) return 0 as T;
|
||||
|
||||
@ -213,6 +213,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
final result = await http.post(
|
||||
"11335ecd55d99c9100f5cd070e5b73f1574aff257ce7e668d08f4caccd1c6232",
|
||||
data: {"roomId": roomId, "mickIndex": mickIndex},
|
||||
allowNullBody: true,
|
||||
fromJson: (json) => null,
|
||||
);
|
||||
return result;
|
||||
@ -224,6 +225,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
final result = await http.post(
|
||||
"70ffb9681e0103463b1faf883935e55d",
|
||||
data: {"roomId": roomId, "mickIndex": mickIndex, "lock": lock},
|
||||
allowNullBody: true,
|
||||
fromJson: (json) => null,
|
||||
);
|
||||
return result;
|
||||
@ -405,6 +407,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
final result = await http.post(
|
||||
"68900c787a89c1092b21ee1a610ef323380d4f62784dacdb4e3d4e1f60770a39",
|
||||
data: {"roomId": roomId, "userId": userId},
|
||||
allowNullBody: true,
|
||||
fromJson: (json) => null,
|
||||
);
|
||||
return result;
|
||||
|
||||
@ -348,6 +348,7 @@ class SCAccountRepository implements SocialChatUserRepository {
|
||||
final result = await http.post(
|
||||
"745ce65f1d68f106702ad3c636aca0e0ac57f82127b3ac414551924946593db7",
|
||||
data: {"pwd": pwd},
|
||||
allowNullBody: true,
|
||||
fromJson: (json) => null,
|
||||
);
|
||||
return result;
|
||||
|
||||
@ -6,6 +6,7 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_svga/flutter_svga.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_room_effect_scheduler.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart';
|
||||
import 'package:tancent_vap/utils/constant.dart';
|
||||
import 'package:tancent_vap/widgets/vap_view.dart';
|
||||
@ -267,6 +268,9 @@ class SCGiftVapSvgaManager {
|
||||
if (_dis) {
|
||||
return;
|
||||
}
|
||||
SCRoomEffectScheduler().completeHighCostTask(
|
||||
debugLabel: _currentTask?.path,
|
||||
);
|
||||
_play = false;
|
||||
_currentTask = null;
|
||||
_scheduleNextTask(delay: delay);
|
||||
@ -316,6 +320,9 @@ class SCGiftVapSvgaManager {
|
||||
if (status.isCompleted) {
|
||||
_log('svga completed path=${_currentTask?.path}');
|
||||
_rsc?.reset();
|
||||
SCRoomEffectScheduler().completeHighCostTask(
|
||||
debugLabel: _currentTask?.path,
|
||||
);
|
||||
_play = false;
|
||||
_currentTask = null;
|
||||
_pn();
|
||||
@ -363,8 +370,13 @@ class SCGiftVapSvgaManager {
|
||||
return;
|
||||
}
|
||||
_tq.add(task);
|
||||
SCRoomEffectScheduler().registerHighCostTaskQueued(debugLabel: path);
|
||||
unawaited(preload(path));
|
||||
while (_tq.length > _maxPendingTaskCount) {
|
||||
final removedTask = _tq.removeLast();
|
||||
SCRoomEffectScheduler().completeHighCostTask(
|
||||
debugLabel: removedTask.path,
|
||||
);
|
||||
_log(
|
||||
'trim queued task path=${removedTask.path} '
|
||||
'priority=${removedTask.priority} queue=${_tq.length}',
|
||||
@ -547,6 +559,7 @@ class SCGiftVapSvgaManager {
|
||||
_rsc?.stop();
|
||||
_rsc?.reset();
|
||||
_rsc?.videoItem = null;
|
||||
SCRoomEffectScheduler().clearHighCostTasks(reason: 'stop_playback');
|
||||
}
|
||||
|
||||
// 释放资源
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
@ -37,26 +38,56 @@ Map<String, String>? buildNetworkImageHeaders(String url) {
|
||||
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")) {
|
||||
return ExtendedNetworkImageProvider(
|
||||
provider = ExtendedNetworkImageProvider(
|
||||
url,
|
||||
headers: buildNetworkImageHeaders(url),
|
||||
cache: true,
|
||||
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(
|
||||
String url, {
|
||||
Future<bool> warmImageResource(
|
||||
String resource, {
|
||||
double? logicalWidth,
|
||||
double? logicalHeight,
|
||||
Duration timeout = const Duration(seconds: 6),
|
||||
}) async {
|
||||
if (url.isEmpty) {
|
||||
if (resource.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final provider = buildCachedImageProvider(url);
|
||||
final provider = buildCachedImageProvider(
|
||||
resource,
|
||||
logicalWidth: logicalWidth,
|
||||
logicalHeight: logicalHeight,
|
||||
);
|
||||
final stream = provider.resolve(ImageConfiguration.empty);
|
||||
final completer = Completer<bool>();
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
140
lib/shared/tools/sc_room_effect_scheduler.dart
Normal file
140
lib/shared/tools/sc_room_effect_scheduler.dart
Normal 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;
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
@ -19,62 +21,145 @@ class LGiftAnimalPage extends StatefulWidget {
|
||||
|
||||
class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
with TickerProviderStateMixin {
|
||||
static const String _luckyGiftRewardFrameAssetPath =
|
||||
"sc_images/room/anim/luck_gift/luck_gift_reward_frame.svga";
|
||||
late final GiftAnimationManager _giftAnimationManager;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 332.w,
|
||||
child: Consumer<GiftAnimationManager>(
|
||||
builder: (context, ref, child) {
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: List.generate(
|
||||
4,
|
||||
(index) => _buildGiftTickerItem(context, ref, index),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: List.generate(4, (index) => _GiftTickerSlot(index: index)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGiftTickerItem(
|
||||
BuildContext context,
|
||||
GiftAnimationManager ref,
|
||||
int index,
|
||||
) {
|
||||
final gift = ref.giftMap[index];
|
||||
if (gift == null || ref.animationControllerList.length <= index) {
|
||||
return const SizedBox.shrink();
|
||||
@override
|
||||
void dispose() {
|
||||
_giftAnimationManager.cleanupAnimationResources();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
final bean = ref.animationControllerList[index];
|
||||
return AnimatedBuilder(
|
||||
animation: bean.controller,
|
||||
builder: (context, child) {
|
||||
final showLuckyRewardFrame = gift.showLuckyRewardFrame;
|
||||
final tickerMargin =
|
||||
showLuckyRewardFrame
|
||||
? bean.luckyGiftPinnedMargin
|
||||
: bean.verticalAnimation.value;
|
||||
return Container(
|
||||
margin: tickerMargin,
|
||||
width:
|
||||
ScreenUtil().screenWidth * (showLuckyRewardFrame ? 0.96 : 0.78),
|
||||
child: _buildGiftTickerCard(context, gift, bean.sizeAnimation.value),
|
||||
_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 RepaintBoundary(
|
||||
child: AnimatedBuilder(
|
||||
animation: Listenable.merge([
|
||||
snapshot.bean.controller,
|
||||
snapshot.bean.countPulseController,
|
||||
]),
|
||||
builder: (context, child) {
|
||||
final showLuckyRewardFrame = snapshot.showLuckyRewardFrame;
|
||||
final tickerMargin =
|
||||
showLuckyRewardFrame
|
||||
? snapshot.bean.luckyGiftPinnedMargin
|
||||
: snapshot.bean.verticalAnimation.value;
|
||||
return Container(
|
||||
margin: tickerMargin,
|
||||
width:
|
||||
ScreenUtil().screenWidth *
|
||||
(showLuckyRewardFrame ? 0.96 : 0.78),
|
||||
child: _GiftTickerCard(
|
||||
snapshot: snapshot,
|
||||
animatedSize: 22.sp * snapshot.bean.sizeAnimation.value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildGiftTickerCard(
|
||||
BuildContext context,
|
||||
LGiftModel gift,
|
||||
double animatedSize,
|
||||
) {
|
||||
final showLuckyRewardFrame = gift.showLuckyRewardFrame;
|
||||
class _GiftTickerCard extends StatelessWidget {
|
||||
const _GiftTickerCard({required this.snapshot, required this.animatedSize});
|
||||
|
||||
static const String _luckyGiftRewardFrameAssetPath =
|
||||
"sc_images/room/anim/luck_gift/luck_gift_reward_frame.svga";
|
||||
|
||||
final GiftAnimationSlotSnapshot snapshot;
|
||||
final double animatedSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final showLuckyRewardFrame = snapshot.showLuckyRewardFrame;
|
||||
return SizedBox(
|
||||
height: 52.w,
|
||||
child: Stack(
|
||||
@ -91,16 +176,18 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
top: 3.w,
|
||||
bottom: 3.w,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(getBg("")),
|
||||
image: AssetImage(
|
||||
"sc_images/room/sc_icon_room_gift_left_no_vip_bg.png",
|
||||
),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
netImage(
|
||||
url: gift.sendUserPic,
|
||||
url: snapshot.sendUserPic,
|
||||
shape: BoxShape.circle,
|
||||
width: 26.w,
|
||||
height: 26.w,
|
||||
@ -114,11 +201,11 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
children: [
|
||||
socialchatNickNameText(
|
||||
maxWidth: 88.w,
|
||||
gift.sendUserName,
|
||||
snapshot.sendUserName,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
type: "",
|
||||
needScroll: gift.sendUserName.characters.length > 8,
|
||||
needScroll: snapshot.sendUserName.characters.length > 8,
|
||||
),
|
||||
SizedBox(height: 1.w),
|
||||
Row(
|
||||
@ -133,7 +220,7 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
gift.sendToUserName,
|
||||
snapshot.sendToUserName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
@ -165,25 +252,25 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
netImage(
|
||||
url: gift.giftPic,
|
||||
url: snapshot.giftPic,
|
||||
fit: BoxFit.cover,
|
||||
borderRadius: BorderRadius.circular(4.w),
|
||||
width: 34.w,
|
||||
height: 34.w,
|
||||
),
|
||||
if (gift.giftCount > 0) ...[
|
||||
if (snapshot.giftCount > 0) ...[
|
||||
SizedBox(width: 8.w),
|
||||
Flexible(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: _buildGiftCountLabel(gift, animatedSize),
|
||||
child: _buildGiftCountLabel(snapshot.giftCount),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (showLuckyRewardFrame) ...[
|
||||
SizedBox(width: 6.w),
|
||||
_buildLuckyGiftRewardFrame(gift),
|
||||
_buildLuckyGiftRewardFrame(),
|
||||
],
|
||||
],
|
||||
),
|
||||
@ -194,9 +281,7 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGiftCountLabel(LGiftModel gift, double animatedSize) {
|
||||
final xFontSize = animatedSize;
|
||||
final countFontSize = animatedSize;
|
||||
Widget _buildGiftCountLabel(num giftCount) {
|
||||
return Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Row(
|
||||
@ -206,7 +291,7 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
Text(
|
||||
"x",
|
||||
style: TextStyle(
|
||||
fontSize: xFontSize,
|
||||
fontSize: animatedSize,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: const Color(0xFFFFD400),
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -214,10 +299,11 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
),
|
||||
),
|
||||
SizedBox(width: 2.w),
|
||||
Text(
|
||||
_giftCountText(gift.giftCount),
|
||||
_AnimatedGiftCountText(
|
||||
targetCount: giftCount,
|
||||
stepUnit: snapshot.giftCountStepUnit,
|
||||
style: TextStyle(
|
||||
fontSize: countFontSize,
|
||||
fontSize: animatedSize,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: const Color(0xFFFFD400),
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -229,21 +315,23 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLuckyGiftRewardFrame(LGiftModel gift) {
|
||||
Widget _buildLuckyGiftRewardFrame() {
|
||||
return SizedBox(
|
||||
width: 108.w,
|
||||
height: 52.w,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SCSvgaAssetWidget(
|
||||
assetPath: _luckyGiftRewardFrameAssetPath,
|
||||
width: 108.w,
|
||||
height: 52.w,
|
||||
fit: BoxFit.cover,
|
||||
loop: true,
|
||||
allowDrawingOverflow: true,
|
||||
fallback: _buildLuckyGiftRewardFrameFallback(),
|
||||
RepaintBoundary(
|
||||
child: SCSvgaAssetWidget(
|
||||
assetPath: _luckyGiftRewardFrameAssetPath,
|
||||
width: 108.w,
|
||||
height: 52.w,
|
||||
fit: BoxFit.cover,
|
||||
loop: true,
|
||||
allowDrawingOverflow: true,
|
||||
fallback: const _LuckyGiftRewardFrameFallback(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
@ -254,7 +342,7 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
"+${_giftRewardAmountText(gift)}",
|
||||
"+${_giftRewardAmountText()}",
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
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(
|
||||
width: 108.w,
|
||||
height: 52.w,
|
||||
@ -300,118 +407,154 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _giftCountText(num count) {
|
||||
return count % 1 == 0 ? count.toInt().toString() : count.toString();
|
||||
}
|
||||
class _AnimatedGiftCountText extends StatefulWidget {
|
||||
const _AnimatedGiftCountText({
|
||||
required this.targetCount,
|
||||
required this.stepUnit,
|
||||
required this.style,
|
||||
});
|
||||
|
||||
String _giftRewardAmountText(LGiftModel gift) {
|
||||
final rewardAmount = gift.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 gift.rewardAmountText;
|
||||
}
|
||||
final num targetCount;
|
||||
final num stepUnit;
|
||||
final TextStyle style;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_giftAnimationManager.cleanupAnimationResources();
|
||||
super.dispose();
|
||||
}
|
||||
State<_AnimatedGiftCountText> createState() => _AnimatedGiftCountTextState();
|
||||
}
|
||||
|
||||
class _AnimatedGiftCountTextState extends State<_AnimatedGiftCountText> {
|
||||
Timer? _stepTimer;
|
||||
late double _displayValue;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_giftAnimationManager = Provider.of<GiftAnimationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
initAnimal();
|
||||
_displayValue = _resolveInitialDisplayValue();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
_startSteppedAnimation(widget.targetCount.toDouble());
|
||||
});
|
||||
}
|
||||
|
||||
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: 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;
|
||||
@override
|
||||
void didUpdateWidget(covariant _AnimatedGiftCountText oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.targetCount == widget.targetCount &&
|
||||
oldWidget.stepUnit == widget.stepUnit) {
|
||||
return;
|
||||
}
|
||||
beans[0].controller.addStatusListener((state) {
|
||||
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);
|
||||
_startSteppedAnimation(widget.targetCount.toDouble());
|
||||
}
|
||||
|
||||
String getBg(String type) {
|
||||
return "sc_images/room/sc_icon_room_gift_left_no_vip_bg.png";
|
||||
@override
|
||||
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 {
|
||||
// late Animation<Offset> transverseAnimation;
|
||||
late Animation<EdgeInsets> verticalAnimation;
|
||||
late EdgeInsets luckyGiftPinnedMargin;
|
||||
late AnimationController controller;
|
||||
late AnimationController countPulseController;
|
||||
late Animation<double> sizeAnimation;
|
||||
Timer? dismissTimer;
|
||||
}
|
||||
|
||||
class LGiftModel {
|
||||
@ -444,4 +587,7 @@ class LGiftModel {
|
||||
|
||||
//id
|
||||
String labelId = "";
|
||||
|
||||
// 数量视觉步长。单次点击送 1 个时会按 1 递增;单次点击送 25 个时直接按 25 跳。
|
||||
num giftCountStepUnit = 1;
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.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:yumi/services/audio/rtc_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';
|
||||
|
||||
class RoomAnimationQueueScreen extends StatefulWidget {
|
||||
@ -36,7 +40,22 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
|
||||
if (!_effectsEnabled) {
|
||||
return;
|
||||
}
|
||||
_addToQueue(msg);
|
||||
unawaited(
|
||||
warmImageResource(
|
||||
msg.user?.userAvatar ?? "",
|
||||
logicalWidth: 28.w,
|
||||
logicalHeight: 28.w,
|
||||
),
|
||||
);
|
||||
SCRoomEffectScheduler().scheduleDeferredEffect(
|
||||
debugLabel: 'room_entrance_join',
|
||||
action: () {
|
||||
if (!_effectsEnabled || !mounted) {
|
||||
return;
|
||||
}
|
||||
_addToQueue(msg);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -44,6 +63,7 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
|
||||
if (!_effectsEnabled || !mounted) {
|
||||
return;
|
||||
}
|
||||
bool shouldStartProcessing = false;
|
||||
setState(() {
|
||||
final taskId = DateTime.now().millisecondsSinceEpoch;
|
||||
final animationKey = GlobalKey<_RoomEntranceAnimationState>();
|
||||
@ -72,10 +92,13 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
|
||||
);
|
||||
|
||||
_animationQueue.add(task);
|
||||
if (!_isQueueProcessing && _animationQueue.isNotEmpty) {
|
||||
_isQueueProcessing = true;
|
||||
shouldStartProcessing = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!_isQueueProcessing && _animationQueue.isNotEmpty) {
|
||||
setState(() => _isQueueProcessing = true);
|
||||
if (shouldStartProcessing) {
|
||||
_startNextAnimation();
|
||||
}
|
||||
}
|
||||
@ -267,53 +290,56 @@ class _RoomEntranceAnimationState extends State<RoomEntranceAnimation>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SlideTransition(
|
||||
position: _offsetAnimation,
|
||||
child: Container(
|
||||
width: ScreenUtil().screenWidth * 0.8,
|
||||
height: 37.w,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(getEntranceBg("")),
|
||||
fit: BoxFit.fill,
|
||||
return RepaintBoundary(
|
||||
child: SlideTransition(
|
||||
position: _offsetAnimation,
|
||||
child: Container(
|
||||
width: ScreenUtil().screenWidth * 0.8,
|
||||
height: 37.w,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(getEntranceBg("")),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
textDirection: TextDirection.ltr,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(2.w),
|
||||
child: netImage(
|
||||
url: widget.msg.user?.userAvatar ?? "",
|
||||
width: 28.w,
|
||||
height: 28.w,
|
||||
shape: BoxShape.circle,
|
||||
child: Row(
|
||||
textDirection: TextDirection.ltr,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(2.w),
|
||||
child: netImage(
|
||||
url: widget.msg.user?.userAvatar ?? "",
|
||||
width: 28.w,
|
||||
height: 28.w,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5.w),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(height: 2.w),
|
||||
socialchatNickNameText(
|
||||
maxWidth: 150.w,
|
||||
widget.msg.user?.userNickname ?? "",
|
||||
fontSize: 12.sp,
|
||||
type: "",
|
||||
needScroll:
|
||||
(widget.msg.user?.userNickname?.characters.length ?? 0) >
|
||||
15,
|
||||
),
|
||||
text(
|
||||
SCAppLocalizations.of(context)!.enterTheRoom,
|
||||
fontSize: 12.sp,
|
||||
lineHeight: 0.9,
|
||||
textColor: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
SizedBox(width: 5.w),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(height: 2.w),
|
||||
socialchatNickNameText(
|
||||
maxWidth: 150.w,
|
||||
widget.msg.user?.userNickname ?? "",
|
||||
fontSize: 12.sp,
|
||||
type: "",
|
||||
needScroll:
|
||||
(widget.msg.user?.userNickname?.characters.length ??
|
||||
0) >
|
||||
15,
|
||||
),
|
||||
text(
|
||||
SCAppLocalizations.of(context)!.enterTheRoom,
|
||||
fontSize: 12.sp,
|
||||
lineHeight: 0.9,
|
||||
textColor: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@ -212,6 +211,8 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
static const int _maxQueuedRequests = 24;
|
||||
|
||||
final Queue<_QueuedRoomGiftSeatFlightRequest> _queue = Queue();
|
||||
final Map<String, ImageProvider<Object>> _imageProviderCache =
|
||||
<String, ImageProvider<Object>>{};
|
||||
final GlobalKey _overlayKey = GlobalKey();
|
||||
|
||||
late final AnimationController _controller;
|
||||
@ -287,6 +288,7 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
_activeRequest = null;
|
||||
_activeImageProvider = null;
|
||||
_activeTargetOffset = null;
|
||||
_imageProviderCache.clear();
|
||||
_isPlaying = false;
|
||||
return;
|
||||
}
|
||||
@ -296,6 +298,7 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
_activeRequest = null;
|
||||
_activeImageProvider = null;
|
||||
_activeTargetOffset = null;
|
||||
_imageProviderCache.clear();
|
||||
_isPlaying = false;
|
||||
});
|
||||
}
|
||||
@ -495,17 +498,30 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
}
|
||||
|
||||
ImageProvider<Object> _resolveImageProvider(String imagePath) {
|
||||
if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
|
||||
return buildCachedImageProvider(imagePath);
|
||||
final currentRequest = _activeRequest ?? _centerRequest;
|
||||
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('/')) {
|
||||
return FileImage(File(imagePath));
|
||||
}
|
||||
return AssetImage(imagePath);
|
||||
final provider = buildCachedImageProvider(
|
||||
imagePath,
|
||||
logicalWidth: logicalSize,
|
||||
logicalHeight: logicalSize,
|
||||
);
|
||||
_imageProviderCache[cacheKey] = provider;
|
||||
return provider;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final overlaySize = MediaQuery.sizeOf(context);
|
||||
return IgnorePointer(
|
||||
child: SizedBox.expand(
|
||||
key: _overlayKey,
|
||||
@ -518,11 +534,6 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
final renderBox =
|
||||
_overlayKey.currentContext?.findRenderObject()
|
||||
as RenderBox?;
|
||||
final overlaySize =
|
||||
renderBox?.size ?? MediaQuery.sizeOf(context);
|
||||
final center = overlaySize.center(Offset.zero);
|
||||
final currentCenterRequest =
|
||||
_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),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@ -277,9 +277,11 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||
provider.isMic = !provider.isMic;
|
||||
|
||||
provider.roomWheatMap.forEach((k, v) {
|
||||
if (v.user?.id ==
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id &&
|
||||
!provider.roomWheatMap[k]!.micMute!) {
|
||||
final seat = provider.micAtIndexForDisplay(k);
|
||||
if ((seat?.user?.id ?? "").trim() ==
|
||||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "")
|
||||
.trim() &&
|
||||
!(seat?.micMute ?? true)) {
|
||||
if (!provider.isMic) {
|
||||
provider.engine?.adjustRecordingSignalVolume(100);
|
||||
provider.engine?.setClientRole(
|
||||
@ -313,15 +315,7 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||
}
|
||||
|
||||
bool _shouldShowMic(RtcProvider provider) {
|
||||
var show = false;
|
||||
|
||||
provider.roomWheatMap.forEach((k, v) {
|
||||
if (v.user?.id == AccountStorage().getCurrentUser()?.userProfile?.id) {
|
||||
show = true;
|
||||
}
|
||||
});
|
||||
|
||||
return show;
|
||||
return provider.isOnMai();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
22
需求进度.md
22
需求进度.md
@ -3,6 +3,21 @@
|
||||
## 当前总目标
|
||||
- 控制当前 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 后可直接搜索接回。
|
||||
- 已将启动页展示时间收敛为 `3` 秒,并在右上角新增通用 `skip 倒计时` 按钮:当前按钮会按秒级动态展示剩余时间,点击可立即跳过;文案已补齐 `en/ar/tr/bn` 多语言翻译,并按 locale 输出倒计时文本,便于后续继续做 RTL 语言验收。
|
||||
@ -181,11 +196,18 @@
|
||||
- `lib/shared/data_sources/models/enum/sc_gift_type.dart`
|
||||
- `lib/shared/tools/sc_network_image_utils.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/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_luck_gift_screen_widget.dart`
|
||||
- `lib/services/room/rc_room_manager.dart`
|
||||
- `lib/services/gift/gift_animation_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/user/edit/edit_user_info_page2.dart`
|
||||
- `lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user