礼物动画以及UI调整

This commit is contained in:
NIGGER SLAYER 2026-04-17 17:21:08 +08:00
parent 61c670b9ff
commit e63a99faaa
11 changed files with 933 additions and 494 deletions

View File

@ -137,6 +137,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
List<MicRes> acceptUsers, {
required SocialChatGiftRes gift,
required int quantity,
int animationCount = 1,
}) {
final rtmProvider = Provider.of<RtmProvider>(
navigatorKey.currentState!.context,
@ -176,6 +177,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
user: currentUser,
toUser: targetUser,
number: quantity,
customAnimationCount: animationCount,
),
);
}
@ -1142,6 +1144,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
acceptUsers: acceptUsers,
gift: selectedGift,
quantity: selectedNumber,
clickCount: 1,
roomId: roomId,
roomAccount: roomAccount,
isLuckyGiftRequest: isLuckyGiftRequest,
@ -1185,12 +1188,14 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
if (existingBatch != null) {
existingBatch.quantity += request.quantity;
existingBatch.clickCount += request.clickCount;
existingBatch.readyAt = now.add(_comboSendBatchWindow);
_giftFxLog(
'aggregate combo send update '
'batchKey=${existingBatch.batchKey} '
'giftId=${request.gift.id} '
'quantity=${existingBatch.quantity} '
'clickCount=${existingBatch.clickCount} '
'acceptUserIds=${request.acceptUserIds.join(",")}',
);
} else {
@ -1204,6 +1209,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
'batchKey=${batch.batchKey} '
'giftId=${request.gift.id} '
'quantity=${batch.quantity} '
'clickCount=${batch.clickCount} '
'acceptUserIds=${request.acceptUserIds.join(",")}',
);
}
@ -1294,6 +1300,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
'standardId=${request.gift.standardId} '
'giftCandy=${request.gift.giftCandy} '
'number=${request.quantity} '
'clickCount=${request.clickCount} '
'acceptCount=${request.acceptUserIds.length} '
'acceptUserIds=${request.acceptUserIds.join(",")} '
'roomId=${request.roomId} '
@ -1310,7 +1317,8 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
'giftId=${request.gift.id} '
'roomId=${request.roomId} '
'acceptUserIds=${request.acceptUserIds.join(",")} '
'quantity=${request.quantity}',
'quantity=${request.quantity} '
'clickCount=${request.clickCount}',
);
final result =
request.isLuckyGiftRequest
@ -1337,6 +1345,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
'giftTab=${request.gift.giftTab} '
'standardId=${request.gift.standardId} '
'number=${request.quantity} '
'clickCount=${request.clickCount} '
'acceptUserIds=${request.acceptUserIds.join(",")} '
'roomId=${request.roomId} '
'balance=$result '
@ -1347,17 +1356,20 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
request.acceptUsers,
gift: request.gift,
quantity: request.quantity,
animationCount: request.clickCount,
);
await sendLuckGiftAnimOtherMsg(
request.acceptUsers,
gift: request.gift,
quantity: request.quantity,
animationCount: request.clickCount,
);
} else {
sendGiftMsg(
request.acceptUsers,
gift: request.gift,
quantity: request.quantity,
animationCount: request.clickCount,
);
}
profileManager?.updateBalance(result);
@ -1383,6 +1395,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
List<MicRes> acceptUsers, {
required SocialChatGiftRes gift,
required int quantity,
int animationCount = 1,
}) {
///IM消息
for (var u in acceptUsers) {
@ -1417,6 +1430,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
user: AccountStorage().getCurrentUser()?.userProfile,
toUser: u.user,
number: quantity,
customAnimationCount: animationCount,
type: SCRoomMsgType.gift,
role:
Provider.of<RtcProvider>(
@ -1459,18 +1473,22 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
}
if (gift.giftSourceUrl != null && gift.special != null) {
if (scGiftHasFullScreenEffect(gift.special)) {
if (SCGlobalConfig.isGiftSpecialEffects) {
if (SCGlobalConfig.isGiftSpecialEffects &&
(rtcProvider?.shouldShowRoomVisualEffects ?? false)) {
_giftFxLog(
'local trigger player play '
'path=${gift.giftSourceUrl} '
'giftId=${gift.id} '
'giftName=${gift.giftName}',
'giftName=${gift.giftName} '
'animationCount=$animationCount',
);
SCGiftVapSvgaManager().play(gift.giftSourceUrl!);
} else {
_giftFxLog(
'skip local play because isGiftSpecialEffects=false '
'giftId=${gift.id}',
'skip local play because visual effects disabled '
'giftId=${gift.id} '
'isGiftSpecialEffects=${SCGlobalConfig.isGiftSpecialEffects} '
'roomVisible=${rtcProvider?.shouldShowRoomVisualEffects ?? false}',
);
}
} else {
@ -1584,6 +1602,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
List<MicRes> acceptUsers, {
required SocialChatGiftRes gift,
required int quantity,
int animationCount = 1,
}) async {
final targetUserIds =
acceptUsers
@ -1598,6 +1617,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
'giftName=${gift.giftName} '
'giftPhoto=${gift.giftPhoto} '
'quantity=$quantity '
'animationCount=$animationCount '
'firstTargetUserId=${firstTargetUser?.id} '
'groupId=${rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount} '
'roomId=${rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id} '
@ -1613,6 +1633,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
user: AccountStorage().getCurrentUser()?.userProfile,
toUser: firstTargetUser,
number: quantity,
customAnimationCount: animationCount,
type: SCRoomMsgType.luckGiftAnimOther,
msg: jsonEncode(acceptUsers.map((u) => u.user?.id).toList()),
),
@ -1701,6 +1722,7 @@ class _GiftSendRequest {
required this.acceptUsers,
required this.gift,
required this.quantity,
required this.clickCount,
required this.roomId,
required this.roomAccount,
required this.isLuckyGiftRequest,
@ -1710,6 +1732,7 @@ class _GiftSendRequest {
final List<MicRes> acceptUsers;
final SocialChatGiftRes gift;
final int quantity;
final int clickCount;
final String roomId;
final String roomAccount;
final bool isLuckyGiftRequest;
@ -1732,6 +1755,7 @@ class _PendingGiftSendBatch {
required this.acceptUsers,
required this.gift,
required this.quantity,
required this.clickCount,
required this.roomId,
required this.roomAccount,
required this.isLuckyGiftRequest,
@ -1748,6 +1772,7 @@ class _PendingGiftSendBatch {
acceptUsers: List<MicRes>.from(request.acceptUsers),
gift: request.gift,
quantity: request.quantity,
clickCount: request.clickCount,
roomId: request.roomId,
roomAccount: request.roomAccount,
isLuckyGiftRequest: request.isLuckyGiftRequest,
@ -1760,6 +1785,7 @@ class _PendingGiftSendBatch {
final List<MicRes> acceptUsers;
final SocialChatGiftRes gift;
int quantity;
int clickCount;
final String roomId;
final String roomAccount;
final bool isLuckyGiftRequest;
@ -1771,6 +1797,7 @@ class _PendingGiftSendBatch {
acceptUsers: List<MicRes>.from(acceptUsers),
gift: gift,
quantity: quantity,
clickCount: clickCount,
roomId: roomId,
roomAccount: roomAccount,
isLuckyGiftRequest: isLuckyGiftRequest,

View File

@ -1,3 +1,4 @@
import 'dart:math' as math;
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
@ -18,6 +19,7 @@ import 'package:yumi/shared/tools/sc_path_utils.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';
import 'package:yumi/ui_kit/widgets/room/anim/room_entrance_widget.dart';
import 'package:yumi/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart';
import 'package:yumi/ui_kit/widgets/room/room_head_widget.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
@ -42,6 +44,12 @@ class VoiceRoomPage extends StatefulWidget {
class _VoiceRoomPageState extends State<VoiceRoomPage>
with SingleTickerProviderStateMixin {
static const Duration _luckyGiftComboWindow = Duration(seconds: 3);
static const Duration _luckyGiftQueueDrainWindow = Duration(seconds: 3);
static const int _maxLuckyGiftTrackedAnimations = 5;
static const Duration _giftAnimationSessionWindow = _luckyGiftComboWindow;
static const Duration _giftAnimationQueueDrainWindow =
_luckyGiftQueueDrainWindow;
static const int _maxTrackedGiftAnimations = _maxLuckyGiftTrackedAnimations;
late TabController _tabController;
final List<Widget> _pages = [AllChatPage(), ChatPage(), GiftChatPage()];
@ -56,8 +64,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
void initState() {
super.initState();
_tabController = TabController(length: _pages.length, vsync: this);
Provider.of<RtmProvider>(context, listen: false).msgFloatingGiftListener =
_floatingGiftListener;
_enableRoomVisualEffects();
_tabController.addListener(() {}); //
_subscription = eventBus.on<SCGiveRoomLuckPageDisposeEvent>().listen((
@ -75,17 +82,51 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_ensureRoomVisualEffectsEnabled();
}
@override
void dispose() {
_suspendRoomVisualEffects();
_tabController.dispose(); //
_subscription.cancel();
super.dispose();
}
void _enableRoomVisualEffects() {
Provider.of<RtcProvider>(
context,
listen: false,
).setRoomVisualEffectsEnabled(true);
Provider.of<RtmProvider>(context, listen: false).msgFloatingGiftListener =
_floatingGiftListener;
}
void _ensureRoomVisualEffectsEnabled() {
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
if (rtcProvider.currenRoom == null ||
rtcProvider.roomVisualEffectsEnabled) {
return;
}
_enableRoomVisualEffects();
}
void _suspendRoomVisualEffects() {
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
rtcProvider.setRoomVisualEffectsEnabled(false);
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
if (rtmProvider.msgFloatingGiftListener == _floatingGiftListener) {
rtmProvider.msgFloatingGiftListener = null;
}
RoomEntranceHelper.clearQueue();
_clearLuckyGiftComboSessions();
_giftSeatFlightController.clear();
_tabController.dispose(); //
_subscription.cancel();
super.dispose();
SCGiftVapSvgaManager().stopPlayback();
}
bool roomThemeBackActi(JoinRoomRes? room) {
@ -111,6 +152,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
canPop: false,
onPopInvokedWithResult: (bool didPop, Object? result) {
if (!didPop) {
_suspendRoomVisualEffects();
SCFloatIchart().show();
SCNavigatorUtils.goBack(context);
}
@ -268,6 +310,12 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
///
_floatingGiftListener(Msg msg) {
if (!Provider.of<RtcProvider>(
context,
listen: false,
).shouldShowRoomVisualEffects) {
return;
}
if (Provider.of<GiftProvider>(context, listen: false).hideLGiftAnimal) {
return;
}
@ -288,21 +336,15 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
final giftPhoto = (msg.gift?.giftPhoto ?? "").trim();
final targetUserId = _resolveGiftTargetUserId(msg);
if (_supportsComboMilestoneEffects(msg)) {
_handleComboMilestoneVisuals(msg, targetUserId);
}
final isLuckyGift = _isLuckyGiftMessage(msg);
if (isLuckyGift) {
_handleLuckyGiftComboVisuals(msg, giftPhoto, targetUserId);
return;
}
if (_shouldPlaySeatFlightGiftAnimation(msg) && targetUserId != null) {
_giftSeatFlightController.enqueue(
RoomGiftSeatFlightRequest(
imagePath: giftPhoto,
targetUserId: targetUserId,
beginSize: 96.w,
endSize: 28.w,
),
);
}
_handleStandardGiftComboVisuals(msg, giftPhoto, targetUserId);
}
bool _isLuckyGiftMessage(Msg msg) {
@ -310,11 +352,18 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
return giftTab == "LUCK" || giftTab == SCGiftType.LUCKY_GIFT.name;
}
void _handleLuckyGiftComboVisuals(
Msg msg,
String giftPhoto,
String? targetUserId,
) {
bool _supportsComboMilestoneEffects(Msg msg) {
return SocialChatGiftSystemManager.supportsComboMilestoneEffects(msg.gift);
}
bool _shouldPlayComboMilestoneEffect(Msg msg) {
if (!_supportsComboMilestoneEffects(msg)) {
return false;
}
return SCGlobalConfig.isLuckGiftSpecialEffects;
}
void _handleComboMilestoneVisuals(Msg msg, String? targetUserId) {
final quantity = (msg.number ?? 0).floor();
if (quantity <= 0) {
return;
@ -325,17 +374,19 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
sessionKey,
() => _LuckyGiftComboSession(),
);
session.endTimer?.cancel();
session.clearQueueTimer?.cancel();
session.totalCount += quantity;
final highestMilestone =
SocialChatGiftSystemManager.resolveHighestReachedLuckGiftMilestone(
SocialChatGiftSystemManager.resolveHighestReachedComboMilestone(
session.totalCount,
);
if (highestMilestone != null &&
highestMilestone > session.highestPlayedMilestone &&
SCGlobalConfig.isLuckGiftSpecialEffects) {
_shouldPlayComboMilestoneEffect(msg)) {
final effectPath =
SocialChatGiftSystemManager.resolveLuckGiftComboEffectPath(
SocialChatGiftSystemManager.resolveComboMilestoneEffectPath(
highestMilestone,
);
if (effectPath != null && effectPath.isNotEmpty) {
@ -343,28 +394,122 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
session.highestPlayedMilestone = highestMilestone;
}
}
}
void _handleStandardGiftComboVisuals(
Msg msg,
String giftPhoto,
String? targetUserId,
) {
if ((msg.number ?? 0) <= 0) {
return;
}
if (!_shouldPlaySeatFlightGiftAnimation(msg) ||
targetUserId == null ||
giftPhoto.isEmpty) {
return;
}
final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetUserId);
final session = _luckyGiftComboSessions.putIfAbsent(
sessionKey,
() => _LuckyGiftComboSession(),
);
session.endTimer?.cancel();
session.clearQueueTimer?.cancel();
_enqueueTrackedSeatFlightAnimations(
sessionKey: sessionKey,
giftPhoto: giftPhoto,
targetUserId: targetUserId,
animationCount: _resolveTrackedAnimationCount(msg),
);
_scheduleGiftAnimationSessionEnd(sessionKey);
}
void _handleLuckyGiftComboVisuals(
Msg msg,
String giftPhoto,
String? targetUserId,
) {
if ((msg.number ?? 0) <= 0) {
return;
}
final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetUserId);
final session = _luckyGiftComboSessions[sessionKey];
if (session == null) {
return;
}
session.endTimer?.cancel();
session.clearQueueTimer?.cancel();
if (_shouldPlaySeatFlightGiftAnimation(msg) &&
targetUserId != null &&
giftPhoto.isNotEmpty) {
session.pendingFlightRequest = RoomGiftSeatFlightRequest(
_enqueueTrackedSeatFlightAnimations(
sessionKey: sessionKey,
giftPhoto: giftPhoto,
targetUserId: targetUserId,
animationCount: _resolveTrackedAnimationCount(msg),
);
}
_scheduleGiftAnimationSessionEnd(sessionKey);
}
int _resolveTrackedAnimationCount(Msg msg) {
return math.max(msg.customAnimationCount ?? 1, 1);
}
void _enqueueTrackedSeatFlightAnimations({
required String sessionKey,
required String giftPhoto,
required String targetUserId,
required int animationCount,
}) {
final normalizedAnimationCount = math.max(animationCount, 1);
final cappedAnimationCount = math.min(
normalizedAnimationCount,
_maxTrackedGiftAnimations,
);
for (var index = 0; index < cappedAnimationCount; index += 1) {
_giftSeatFlightController.enqueueLimited(
RoomGiftSeatFlightRequest(
imagePath: giftPhoto,
targetUserId: targetUserId,
beginSize: 96.w,
endSize: 28.w,
queueTag: sessionKey,
),
maxTrackedRequests: _maxTrackedGiftAnimations,
);
}
session.flushTimer?.cancel();
session.flushTimer = Timer(_luckyGiftComboWindow, () {
}
void _scheduleGiftAnimationSessionEnd(String sessionKey) {
final session = _luckyGiftComboSessions[sessionKey];
if (session == null) {
return;
}
session.endTimer?.cancel();
session.endTimer = Timer(_giftAnimationSessionWindow, () {
if (!mounted) {
return;
}
final activeSession = _luckyGiftComboSessions.remove(sessionKey);
final pendingFlightRequest = activeSession?.pendingFlightRequest;
activeSession?.dispose();
if (pendingFlightRequest != null) {
_giftSeatFlightController.enqueue(pendingFlightRequest);
final activeSession = _luckyGiftComboSessions[sessionKey];
if (activeSession == null) {
return;
}
if (!_giftSeatFlightController.hasTrackedRequests(sessionKey)) {
activeSession.dispose();
_luckyGiftComboSessions.remove(sessionKey);
return;
}
activeSession.clearQueueTimer?.cancel();
activeSession.clearQueueTimer = Timer(_giftAnimationQueueDrainWindow, () {
_giftSeatFlightController.clearQueuedRequests(sessionKey);
final expiredSession = _luckyGiftComboSessions.remove(sessionKey);
expiredSession?.dispose();
});
});
}
@ -445,12 +590,13 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
}
class _LuckyGiftComboSession {
Timer? flushTimer;
Timer? endTimer;
Timer? clearQueueTimer;
int totalCount = 0;
int highestPlayedMilestone = 0;
RoomGiftSeatFlightRequest? pendingFlightRequest;
void dispose() {
flushTimer?.cancel();
endTimer?.cancel();
clearQueueTimer?.cancel();
}
}

View File

@ -45,6 +45,7 @@ typedef RtcProvider = RealTimeCommunicationManager;
class RealTimeCommunicationManager extends ChangeNotifier {
bool needUpDataUserInfo = false;
bool _roomVisualEffectsEnabled = false;
///
JoinRoomRes? currenRoom;
@ -88,6 +89,19 @@ class RealTimeCommunicationManager extends ChangeNotifier {
this.context = context;
}
bool get roomVisualEffectsEnabled => _roomVisualEffectsEnabled;
bool get shouldShowRoomVisualEffects =>
currenRoom != null && _roomVisualEffectsEnabled;
void setRoomVisualEffectsEnabled(bool enabled) {
if (_roomVisualEffectsEnabled == enabled) {
return;
}
_roomVisualEffectsEnabled = enabled;
notifyListeners();
}
Future<void> joinAgoraVoiceChannel() async {
try {
engine = await _initAgoraRtcEngine();
@ -329,6 +343,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
listen: false,
).fetchUserProfileData();
retrieveMicrophoneList();
setRoomVisualEffectsEnabled(true);
SCFloatIchart().remove();
SCNavigatorUtils.push(
context,
@ -410,6 +425,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
notifyListeners();
});
}
setRoomVisualEffectsEnabled(true);
SCNavigatorUtils.push(context!, VoiceRoomRoute.voiceRoom, replace: false);
var joinResult = await rtmProvider?.joinRoomGroup(
currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "",
@ -455,7 +471,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
rtmProvider?.dispatchMessage(joinMsg, addLocal: true);
if (SCGlobalConfig.isEntryVehicleAnimation) {
if (AccountStorage().getCurrentUser()?.userProfile?.getMountains() !=
null) {
null &&
shouldShowRoomVisualEffects) {
Future.delayed(Duration(milliseconds: 550), () {
SCGiftVapSvgaManager().play(
AccountStorage()
@ -566,6 +583,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
rtmProvider?.msgGiftListener = null;
SCLoadingManager.show(context: context);
SCGiftVapSvgaManager().stopPlayback();
setRoomVisualEffectsEnabled(false);
SCHeartbeatUtils.scheduleHeartbeat(SCHeartbeatStatus.ONLINE.name, false);
SCHeartbeatUtils.cancelAnchorTimer();
rtmProvider?.cleanRoomData();
@ -630,6 +648,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
rtmProvider?.roomChatMsgList.clear();
redPacketList.clear();
currenRoom = null;
_roomVisualEffectsEnabled = false;
isMic = true;
isMusicPlaying = false;
DataPersistence.setLastTimeRoomId("");

View File

@ -1284,6 +1284,11 @@ class RealTimeMessagingManager extends ChangeNotifier {
}
} else {
if (msg.type == SCRoomMsgType.joinRoom) {
final shouldShowRoomVisualEffects =
Provider.of<RealTimeCommunicationManager>(
context!,
listen: false,
).shouldShowRoomVisualEffects;
if (msg.user != null) {
Provider.of<RealTimeCommunicationManager>(
context!,
@ -1296,7 +1301,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
///
if (msg.user?.getMountains() != null) {
if (SCGlobalConfig.isEntryVehicleAnimation) {
if (SCGlobalConfig.isEntryVehicleAnimation &&
shouldShowRoomVisualEffects) {
SCGiftVapSvgaManager().play(
msg.user?.getMountains()?.sourceUrl ?? "",
priority: 100,
@ -1330,7 +1336,11 @@ class RealTimeMessagingManager extends ChangeNotifier {
);
if (msg.gift!.giftSourceUrl != null && msg.gift!.special != null) {
if (scGiftHasFullScreenEffect(msg.gift!.special)) {
if (SCGlobalConfig.isGiftSpecialEffects) {
if (SCGlobalConfig.isGiftSpecialEffects &&
Provider.of<RealTimeCommunicationManager>(
context!,
listen: false,
).shouldShowRoomVisualEffects) {
_giftFxLog(
'trigger player play path=${msg.gift!.giftSourceUrl} '
'giftId=${msg.gift?.id} giftName=${msg.gift?.giftName}',
@ -1338,8 +1348,10 @@ class RealTimeMessagingManager extends ChangeNotifier {
SCGiftVapSvgaManager().play(msg.gift!.giftSourceUrl!);
} else {
_giftFxLog(
'skip player play because isGiftSpecialEffects=false '
'giftId=${msg.gift?.id}',
'skip player play because visual effects disabled '
'giftId=${msg.gift?.id} '
'isGiftSpecialEffects=${SCGlobalConfig.isGiftSpecialEffects} '
'roomVisible=${Provider.of<RealTimeCommunicationManager>(context!, listen: false).shouldShowRoomVisualEffects}',
);
}
} else {

View File

@ -4,6 +4,7 @@ import 'package:yumi/shared/business_logic/models/res/mic_res.dart';
import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
import 'package:yumi/shared/data_sources/models/enum/sc_gift_type.dart';
typedef GiftProvider = SocialChatGiftSystemManager;
@ -210,7 +211,7 @@ class SocialChatGiftSystemManager extends ChangeNotifier {
}
void startGiftAnimation() {
final milestone = resolveHighestReachedLuckGiftMilestone(number);
final milestone = resolveHighestReachedComboMilestone(number);
if (milestone == null || (isPlayed[milestone] ?? false)) {
return;
}
@ -221,7 +222,7 @@ class SocialChatGiftSystemManager extends ChangeNotifier {
void playVisualEffect(num n) {
if (!(isPlayed[n] ?? false)) {
if (SCGlobalConfig.isLuckGiftSpecialEffects) {
final comboEffectPath = resolveLuckGiftComboEffectPath(n);
final comboEffectPath = resolveComboMilestoneEffectPath(n);
if (comboEffectPath != null) {
SCGiftVapSvgaManager().play(comboEffectPath, priority: 200);
}
@ -239,7 +240,12 @@ class SocialChatGiftSystemManager extends ChangeNotifier {
}
}
static int? resolveHighestReachedLuckGiftMilestone(num count) {
static bool supportsComboMilestoneEffects(SocialChatGiftRes? gift) {
final giftTab = (gift?.giftTab ?? '').trim();
return giftTab == "LUCK" || giftTab == SCGiftType.LUCKY_GIFT.name;
}
static int? resolveHighestReachedComboMilestone(num count) {
final normalizedCount = count.floor();
int? highest;
for (final milestone in _luckGiftMilestones) {
@ -251,7 +257,11 @@ class SocialChatGiftSystemManager extends ChangeNotifier {
return highest;
}
static String? resolveLuckGiftComboEffectPath(num count) {
static int? resolveHighestReachedLuckGiftMilestone(num count) {
return resolveHighestReachedComboMilestone(count);
}
static String? resolveComboMilestoneEffectPath(num count) {
if (count % 1 != 0) {
return null;
}
@ -265,4 +275,8 @@ class SocialChatGiftSystemManager extends ChangeNotifier {
}
return "sc_images/room/anim/luck_gift_count_$normalizedCount.mp4";
}
static String? resolveLuckGiftComboEffectPath(num count) {
return resolveComboMilestoneEffectPath(count);
}
}

View File

@ -5,10 +5,10 @@ import 'package:yumi/app_localizations.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart';
import 'package:yumi/ui_kit/components/text/sc_text.dart';
import 'package:provider/provider.dart';
import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
class RoomAnimationQueueScreen extends StatefulWidget {
const RoomAnimationQueueScreen({super.key});
@ -31,11 +31,17 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
_msgUserJoinListener(Msg msg) {
Future.delayed(Duration(milliseconds: 550), () {
if (!_effectsEnabled) {
return;
}
_addToQueue(msg);
});
}
void _addToQueue(Msg msg) {
if (!_effectsEnabled || !mounted) {
return;
}
setState(() {
final taskId = DateTime.now().millisecondsSinceEpoch;
final animationKey = GlobalKey<_RoomEntranceAnimationState>();
@ -96,6 +102,12 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
}
void _clearQueue() {
if (!mounted) {
_animationQueue.clear();
_animationKeys.clear();
_isQueueProcessing = false;
return;
}
setState(() {
_animationQueue.clear();
_animationKeys.clear();
@ -103,8 +115,40 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
});
}
bool get _effectsEnabled =>
Provider.of<RtcProvider>(
context,
listen: false,
).shouldShowRoomVisualEffects;
@override
void dispose() {
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
if (rtmProvider.msgUserJoinListener == _msgUserJoinListener) {
rtmProvider.msgUserJoinListener = null;
}
_animationQueue.clear();
_animationKeys.clear();
_isQueueProcessing = false;
super.dispose();
}
@override
Widget build(BuildContext context) {
final effectsEnabled = context.select<RtcProvider, bool>(
(rtcProvider) => rtcProvider.shouldShowRoomVisualEffects,
);
if (!effectsEnabled) {
if (_animationQueue.isNotEmpty || _isQueueProcessing) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_clearQueue();
}
});
}
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
@ -218,9 +262,7 @@ class _RoomEntranceAnimationState extends State<RoomEntranceAnimation>
height: 37.w,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(
getEntranceBg(""),
),
image: AssetImage(getEntranceBg("")),
fit: BoxFit.fill,
),
),

View File

@ -17,6 +17,7 @@ class RoomGiftSeatFlightRequest {
this.flightDuration = const Duration(milliseconds: 920),
this.beginSize = 96,
this.endSize = 34,
this.queueTag,
});
final String imagePath;
@ -25,6 +26,7 @@ class RoomGiftSeatFlightRequest {
final Duration flightDuration;
final double beginSize;
final double endSize;
final String? queueTag;
}
class RoomGiftSeatFlightController {
@ -39,21 +41,11 @@ class RoomGiftSeatFlightController {
_RoomGiftSeatFlightOverlayState? _state;
void enqueue(RoomGiftSeatFlightRequest request) {
final imagePath = request.imagePath.trim();
final targetUserId = request.targetUserId.trim();
if (imagePath.isEmpty || targetUserId.isEmpty) {
final normalizedRequest = _normalizeRequest(request);
if (normalizedRequest == null) {
return;
}
final normalizedRequest = RoomGiftSeatFlightRequest(
imagePath: imagePath,
targetUserId: targetUserId,
holdDuration: request.holdDuration,
flightDuration: request.flightDuration,
beginSize: request.beginSize,
endSize: request.endSize,
);
if (_state == null) {
_pendingRequests.add(normalizedRequest);
return;
@ -61,11 +53,122 @@ class RoomGiftSeatFlightController {
_state!._enqueue(normalizedRequest);
}
void enqueueLimited(
RoomGiftSeatFlightRequest request, {
required int maxTrackedRequests,
}) {
final normalizedRequest = _normalizeRequest(request);
if (normalizedRequest == null) {
return;
}
final queueTag = (normalizedRequest.queueTag ?? '').trim();
if (queueTag.isEmpty || maxTrackedRequests <= 0) {
enqueue(normalizedRequest);
return;
}
if (_state == null) {
_trimPendingRequestsForTag(queueTag, maxTrackedRequests);
_pendingRequests.add(normalizedRequest);
return;
}
_state!._enqueueLimited(
normalizedRequest,
maxTrackedRequests: maxTrackedRequests,
);
}
void clear() {
_pendingRequests.clear();
_state?._clear();
}
bool hasTrackedRequests(String queueTag) {
final normalizedQueueTag = queueTag.trim();
if (normalizedQueueTag.isEmpty) {
return false;
}
if (_state == null) {
return _pendingRequests.any(
(request) => (request.queueTag ?? '').trim() == normalizedQueueTag,
);
}
return _state!._hasTrackedRequests(normalizedQueueTag);
}
void clearQueuedRequests(String queueTag) {
final normalizedQueueTag = queueTag.trim();
if (normalizedQueueTag.isEmpty) {
return;
}
if (_state == null) {
_clearPendingRequestsForTag(normalizedQueueTag);
return;
}
_state!._clearQueuedRequests(normalizedQueueTag);
}
RoomGiftSeatFlightRequest? _normalizeRequest(
RoomGiftSeatFlightRequest request,
) {
final imagePath = request.imagePath.trim();
final targetUserId = request.targetUserId.trim();
if (imagePath.isEmpty || targetUserId.isEmpty) {
return null;
}
final queueTag = request.queueTag?.trim();
return RoomGiftSeatFlightRequest(
imagePath: imagePath,
targetUserId: targetUserId,
holdDuration: request.holdDuration,
flightDuration: request.flightDuration,
beginSize: request.beginSize,
endSize: request.endSize,
queueTag: queueTag == null || queueTag.isEmpty ? null : queueTag,
);
}
void _trimPendingRequestsForTag(String queueTag, int maxTrackedRequests) {
while (_countPendingRequestsForTag(queueTag) >= maxTrackedRequests) {
final removed = _removeOldestPendingRequestForTag(queueTag);
if (!removed) {
break;
}
}
}
int _countPendingRequestsForTag(String queueTag) {
var count = 0;
for (final request in _pendingRequests) {
if ((request.queueTag ?? '').trim() == queueTag) {
count += 1;
}
}
return count;
}
bool _removeOldestPendingRequestForTag(String queueTag) {
for (final request in _pendingRequests.toList()) {
if ((request.queueTag ?? '').trim() == queueTag) {
_pendingRequests.remove(request);
return true;
}
}
return false;
}
void _clearPendingRequestsForTag(String queueTag) {
final requestsToRemove =
_pendingRequests
.where((request) => (request.queueTag ?? '').trim() == queueTag)
.toList();
for (final request in requestsToRemove) {
_pendingRequests.remove(request);
}
}
void _attach(_RoomGiftSeatFlightOverlayState state) {
_state = state;
while (_pendingRequests.isNotEmpty) {
@ -139,6 +242,26 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
_scheduleNextAnimation();
}
void _enqueueLimited(
RoomGiftSeatFlightRequest request, {
required int maxTrackedRequests,
}) {
final queueTag = (request.queueTag ?? '').trim();
if (queueTag.isEmpty) {
_enqueue(request);
return;
}
while (_countTrackedRequests(queueTag) >= maxTrackedRequests) {
final removed = _removeOldestQueuedRequest(queueTag);
if (!removed) {
return;
}
}
_enqueue(request);
}
void _clear() {
_queue.clear();
_controller.stop();
@ -158,6 +281,54 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
});
}
bool _hasTrackedRequests(String queueTag) {
if ((_activeRequest?.queueTag ?? '').trim() == queueTag) {
return true;
}
for (final queuedRequest in _queue) {
if ((queuedRequest.request.queueTag ?? '').trim() == queueTag) {
return true;
}
}
return false;
}
void _clearQueuedRequests(String queueTag) {
final requestsToRemove =
_queue
.where(
(queuedRequest) =>
(queuedRequest.request.queueTag ?? '').trim() == queueTag,
)
.toList();
for (final queuedRequest in requestsToRemove) {
_queue.remove(queuedRequest);
}
}
int _countTrackedRequests(String queueTag) {
var count = 0;
if ((_activeRequest?.queueTag ?? '').trim() == queueTag) {
count += 1;
}
for (final queuedRequest in _queue) {
if ((queuedRequest.request.queueTag ?? '').trim() == queueTag) {
count += 1;
}
}
return count;
}
bool _removeOldestQueuedRequest(String queueTag) {
for (final queuedRequest in _queue.toList()) {
if ((queuedRequest.request.queueTag ?? '').trim() == queueTag) {
_queue.remove(queuedRequest);
return true;
}
}
return false;
}
void _scheduleNextAnimation() {
if (_isPlaying || _queue.isEmpty || !mounted) {
return;

View File

@ -206,8 +206,8 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
child: RoomBottomCircleAction(
child: Image.asset(
"sc_images/room/${provider.isMic ? 'sc_icon_botton_mic_close' : 'sc_icon_botton_mic_open'}.png",
width: 24.w,
height: 24.w,
width: 30.w,
height: 30.w,
fit: BoxFit.contain,
gaplessPlayback: true,
),

View File

@ -1299,6 +1299,9 @@ class Msg {
//
num? number;
//
int? customAnimationCount;
num? awardAmount;
//
@ -1320,6 +1323,7 @@ class Msg {
this.role = "",
this.gift,
this.number,
this.customAnimationCount,
this.awardAmount,
this.hasPlayedAnimation,
this.needUpDataUserInfo,
@ -1334,6 +1338,7 @@ class Msg {
role = json['role'];
time = json['time'];
number = json['number'];
customAnimationCount = json['customAnimationCount'];
awardAmount = json['awardAmount'];
hasPlayedAnimation = json['hasPlayedAnimation'];
needUpDataUserInfo = json['needUpDataUserInfo'];
@ -1361,6 +1366,7 @@ class Msg {
map['time'] = time;
map['awardAmount'] = awardAmount;
map['number'] = number;
map['customAnimationCount'] = customAnimationCount;
map['hasPlayedAnimation'] = hasPlayedAnimation;
map['needUpDataUserInfo'] = needUpDataUserInfo;
if (userWheat != null) {

View File

@ -181,11 +181,12 @@ def build_android(args: argparse.Namespace, manifest: dict[str, object]) -> None
"flutter",
"build",
"apk",
"--split-per-abi",
"--target-platform",
"android-arm64",
]
append_common_flutter_args(apk_cmd, args, build_mode="debug")
with timed_stage(manifest, "android.localDebugApk", "Android 极速测试包Debug / ARM64"):
append_common_flutter_args(apk_cmd, args)
with timed_stage(manifest, "android.localReleaseApk", "Android 测试包Release / ARM64"):
run_command(apk_cmd)
google_play_dir = android_output_dir / "google-play"
@ -205,12 +206,12 @@ def build_android(args: argparse.Namespace, manifest: dict[str, object]) -> None
artifacts["localArm64Apk"] = copy_file(arm64_src, local_dir / f"{artifact_prefix}-arm64-v8a.apk")
elif profile == "local-arm64":
arm64_src = first_existing_path(
ROOT / "build" / "app" / "outputs" / "flutter-apk" / "app-arm64-v8a-debug.apk",
ROOT / "build" / "app" / "outputs" / "flutter-apk" / "app-debug.apk",
ROOT / "build" / "app" / "outputs" / "flutter-apk" / "app-arm64-v8a-release.apk",
ROOT / "build" / "app" / "outputs" / "flutter-apk" / "app-release.apk",
)
artifacts["localArm64Apk"] = copy_file(
arm64_src,
local_dir / f"{artifact_prefix}-arm64-v8a-debug.apk",
local_dir / f"{artifact_prefix}-arm64-v8a.apk",
include_sha256=False,
)

View File

@ -15,7 +15,8 @@
## 已完成模块
- 已继续优化幸运/CP 礼物连击体验:房间 `Gift/All` 消息面板现在会对短时间内同一发送者、同一目标、同一礼物的连续赠送做聚合更新,不再每点一次就追加一条新消息,而是复用同一条礼物播报并持续刷新成 `xN`同时礼物页底部发送按钮已补上本地连击反馈Lucky/CP/Magic 类礼物连续点击时会显示一条从右向左收缩的浅色倒计时渐变条,并同步累加当前连击数量,用户能更直观看到连击窗口是否还在持续。
- 已继续给发送端补齐连击请求聚合:礼物页当前会对 Lucky/CP/Magic 这类连击型礼物启用约 `200ms` 的本地批量窗口,用户在短时间内连续点击 `Send` 时,前端会先把同一礼物、同一目标集合、同一房间的点击数量累加到同一批次里,再统一发一次接口和一次房间 RTM 消息;这样高连击时不再按点击次数直冲 `/gift/batch``/gift/give/lucky-gift`,同时也避免本地回显和房间消息量被线性放大。
- 已继续收敛幸运礼物高连击时的播放策略:房间内幸运礼物现在按 3 秒连击会话聚合,连击期间只更新房内上飘与总次数,不再每次都立刻飞向麦位;同一轮连击结束后才会向目标麦位补播一次飞行动画,避免 `1000` 连击把静态礼物飞行动画排成超长队列;同时幸运礼物的档位特效已改为只命中当前会话累计数量对应的最高有效档位,不再把中间跨过的 `10/20/30/...` 全部补播一遍。
- 已按最新确认回调幸运礼物的“自制飞向麦位动画”策略:不再等整轮连击结束后只飞一次,而是改回按点击次数触发;为兼顾连击体感和队列安全,当前会通过 RTM 额外携带本批次对应的点击次数,让房间端按点击数补齐自制动画,同时同一轮幸运礼物连击的飞行动画队列最多只保留 `5` 个;当连击在 `3s` 内停止后,如果队列里还有待播动画,则只再给 `3s` 消化时间,超时后剩余待播动画会直接清掉。
- 已继续收敛幸运礼物高连击时的播放策略:房间内幸运礼物仍按 3 秒连击会话累计总次数,用来判断当前命中的最高有效档位特效;但自制飞向麦位动画已改回按点击次数触发,并通过“同会话最多保留 5 个待播/在播动画 + 连击结束后额外 3 秒清尾”的方式限制队列膨胀,不再让 `1000` 连击把静态礼物飞行动画排成超长队列;同时幸运礼物的档位特效仍保持只命中当前会话累计数量对应的最高有效档位,不再把中间跨过的 `10/20/30/...` 全部补播一遍。
- 已定位到幸运礼物“中奖通知”使用的是房间页顶层 `LuckGiftNomorAnimWidget`,此前背景一直是静态 `sc_icon_luck_gift_nomore.webp`;当前已改为优先播放本地 `sc_images/room/anim/luck_gift/luck_gift_reward_frame.svga`,并保留原 `webp` 作为失败兜底,这样幸运礼物中奖弹层会直接复用新的奖励边框动效资源。
- 已开始接入幸运礼物连击档位的新动效资源:桌面“幸运礼物相关”目录下的 SVGA 已统一导入到 `sc_images/room/anim/luck_gift/`,并按规范重命名为 `luck_gift_combo_count_10.svga / luck_gift_combo_count_666.svga / luck_gift_combo_count_10000.svga` 这一类统一英文命名;当前连击阈值触发仍复用 `GiftSystemManager.playVisualEffect()`,命中已提供素材的档位时优先播放新的本地 SVGA未提供素材的档位继续保留旧兜底逻辑避免影响现有幸运礼物连击链路。
- 已将语言房送礼链路接入新的“中心停留后飞向目标麦位”组件,但只对无自带特效的静态 PNG 礼物生效:当前带自身 `SVGA/MP4/VAP` 动画或被识别为全屏礼物特效的礼物保持原有播放逻辑不变;只有普通 PNG 礼物会额外触发“屏幕中央停留 -> 三连残影飞向被赠送麦位”的补充动效,避免和自带礼物特效重复叠播。