自制飞屏动画处理
安卓待验证
This commit is contained in:
parent
122bc5668d
commit
732e2bfcfb
180
lib/debug/lucky_gift_global_floating_preview.dart
Normal file
180
lib/debug/lucky_gift_global_floating_preview.dart
Normal file
@ -0,0 +1,180 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:pag/pag.dart';
|
||||
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/floating/floating_lucky_gift_global_screen_widget.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const LuckyGiftGlobalFloatingPreviewApp());
|
||||
}
|
||||
|
||||
class LuckyGiftGlobalFloatingPreviewApp extends StatelessWidget {
|
||||
const LuckyGiftGlobalFloatingPreviewApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScreenUtilInit(
|
||||
designSize: const Size(375, 812),
|
||||
minTextAdapt: true,
|
||||
builder: (_, child) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: const _LuckyGiftGlobalFloatingPreviewPage(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LuckyGiftGlobalFloatingPreviewPage extends StatelessWidget {
|
||||
const _LuckyGiftGlobalFloatingPreviewPage();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset(_backgroundAsset, fit: BoxFit.cover),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 116.w,
|
||||
child: FloatingLuckyGiftGlobalScreenWidget(
|
||||
debugDisableAutoSlide: true,
|
||||
message: SCFloatingMessage(
|
||||
type: FloatingLuckyGiftGlobalScreenWidget.floatingType,
|
||||
roomId: '1002900',
|
||||
userName: 'Bajul',
|
||||
userAvatarUrl: '',
|
||||
coins: 1000,
|
||||
multiple: 10,
|
||||
),
|
||||
onAnimationCompleted: () {},
|
||||
),
|
||||
),
|
||||
Positioned(left: 0, top: 250.w, child: const _PagLayerScanner()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const _backgroundAsset = 'sc_images/room/background_examples/bg_example_1.png';
|
||||
|
||||
class _PagLayerScanner extends StatefulWidget {
|
||||
const _PagLayerScanner();
|
||||
|
||||
@override
|
||||
State<_PagLayerScanner> createState() => _PagLayerScannerState();
|
||||
}
|
||||
|
||||
class _PagLayerScannerState extends State<_PagLayerScanner> {
|
||||
final _pagKey = GlobalKey<PAGViewState>();
|
||||
final _foundLayers = <String>{};
|
||||
String _summary = 'loading PAG...';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: FloatingLuckyGiftGlobalScreenWidget.bannerWidth.w,
|
||||
height: 160.w,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: FloatingLuckyGiftGlobalScreenWidget.bannerWidth.w,
|
||||
height: FloatingLuckyGiftGlobalScreenWidget.bannerHeight.w,
|
||||
child: PAGView.asset(
|
||||
FloatingLuckyGiftGlobalScreenWidget.backgroundPagAssetPath,
|
||||
key: _pagKey,
|
||||
width: FloatingLuckyGiftGlobalScreenWidget.bannerWidth.w,
|
||||
height: FloatingLuckyGiftGlobalScreenWidget.bannerHeight.w,
|
||||
autoPlay: false,
|
||||
initProgress: 0.35,
|
||||
repeatCount: PAGView.REPEAT_COUNT_LOOP,
|
||||
onInit: _scanLayers,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: FloatingLuckyGiftGlobalScreenWidget.bannerWidth.w,
|
||||
padding: EdgeInsets.all(6.w),
|
||||
color: Colors.black.withValues(alpha: 0.65),
|
||||
child: Text(
|
||||
_summary,
|
||||
maxLines: 4,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textScaler: TextScaler.noScaling,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 9.sp,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _scanLayers() async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
final state = _pagKey.currentState;
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
final rawWidth = state.rawWidth;
|
||||
final rawHeight = state.rawHeight;
|
||||
debugPrint('[LuckyGiftPAG] rawSize=${rawWidth}x$rawHeight');
|
||||
if (rawWidth <= 0 || rawHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
final sampleXs =
|
||||
<double>[
|
||||
40,
|
||||
80,
|
||||
120,
|
||||
170,
|
||||
220,
|
||||
280,
|
||||
340,
|
||||
410,
|
||||
480,
|
||||
560,
|
||||
640,
|
||||
710,
|
||||
].where((value) => value < rawWidth).toList();
|
||||
final sampleYs =
|
||||
<double>[
|
||||
32,
|
||||
58,
|
||||
82,
|
||||
104,
|
||||
128,
|
||||
158,
|
||||
].where((value) => value < rawHeight).toList();
|
||||
for (final y in sampleYs) {
|
||||
for (final x in sampleXs) {
|
||||
final layers = await state.getLayersUnderPoint(x, y);
|
||||
if (layers.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
_foundLayers.addAll(layers.where((name) => name.trim().isNotEmpty));
|
||||
debugPrint(
|
||||
'[LuckyGiftPAG] point=(${x.toStringAsFixed(0)},'
|
||||
'${y.toStringAsFixed(0)}) layers=$layers',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_summary =
|
||||
'raw ${rawWidth.toStringAsFixed(0)}x'
|
||||
'${rawHeight.toStringAsFixed(0)} layers: '
|
||||
'${_foundLayers.join(', ')}';
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -137,60 +137,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
return 'type=${error.runtimeType} error=$error';
|
||||
}
|
||||
|
||||
void _showLocalLuckyGiftFeedback(
|
||||
List<MicRes> acceptUsers, {
|
||||
required SocialChatGiftRes gift,
|
||||
required int quantity,
|
||||
required String giftBatchId,
|
||||
int animationCount = 1,
|
||||
}) {
|
||||
final targetUserIds = _resolveAcceptUserIds(acceptUsers);
|
||||
final rtmProvider = Provider.of<RtmProvider>(
|
||||
navigatorKey.currentState!.context,
|
||||
listen: false,
|
||||
);
|
||||
final currentUser = AccountStorage().getCurrentUser()?.userProfile;
|
||||
if (currentUser == null) {
|
||||
_giftFxLog(
|
||||
'local lucky feedback skipped reason=no_current_user giftId=${gift.id}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for (final acceptUser in acceptUsers) {
|
||||
final targetUser = acceptUser.user;
|
||||
if (targetUser == null) {
|
||||
_giftFxLog(
|
||||
'local lucky feedback skipped reason=no_target_user giftId=${gift.id}',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
_giftFxLog(
|
||||
'local lucky feedback trigger '
|
||||
'giftId=${gift.id} '
|
||||
'giftName=${gift.giftName} '
|
||||
'toUserId=${targetUser.id} '
|
||||
'toUserName=${targetUser.userNickname} '
|
||||
'quantity=$quantity',
|
||||
);
|
||||
rtmProvider.msgFloatingGiftListener?.call(
|
||||
Msg(
|
||||
groupId:
|
||||
rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount,
|
||||
msg: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
type: SCRoomMsgType.luckGiftAnimOther,
|
||||
gift: gift,
|
||||
user: currentUser,
|
||||
toUser: targetUser,
|
||||
targetUserIds: targetUserIds,
|
||||
giftBatchId: giftBatchId,
|
||||
number: quantity,
|
||||
customAnimationCount: animationCount,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _applyGiftSelection(SocialChatGiftRes? gift, {bool notify = true}) {
|
||||
checkedGift = gift;
|
||||
if (gift != null &&
|
||||
@ -1602,13 +1548,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
_showLocalCpInviteWaitingAfterGift(request, request.acceptUsers);
|
||||
if (request.isLuckyGiftRequest) {
|
||||
final giftBatchId = _createGiftBatchId(request);
|
||||
_showLocalLuckyGiftFeedback(
|
||||
request.acceptUsers,
|
||||
gift: request.gift,
|
||||
quantity: request.quantity,
|
||||
giftBatchId: giftBatchId,
|
||||
animationCount: request.clickCount,
|
||||
);
|
||||
await sendLuckGiftAnimOtherMsg(
|
||||
request.acceptUsers,
|
||||
gift: request.gift,
|
||||
@ -2127,7 +2066,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
required String giftBatchId,
|
||||
int animationCount = 1,
|
||||
}) async {
|
||||
final targetUserIds = _resolveAcceptUserIds(acceptUsers);
|
||||
final targetUserIds = _resolveAcceptUserIdentityKeys(acceptUsers);
|
||||
final firstTargetUser =
|
||||
acceptUsers.isNotEmpty ? acceptUsers.first.user : null;
|
||||
_giftFxLog(
|
||||
@ -2156,7 +2095,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
number: quantity,
|
||||
customAnimationCount: animationCount,
|
||||
type: SCRoomMsgType.luckGiftAnimOther,
|
||||
msg: jsonEncode(acceptUsers.map((u) => u.user?.id).toList()),
|
||||
msg: jsonEncode(targetUserIds),
|
||||
),
|
||||
addLocal: true,
|
||||
);
|
||||
@ -2179,6 +2118,26 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
return targetUserIds;
|
||||
}
|
||||
|
||||
List<String> _resolveAcceptUserIdentityKeys(List<MicRes> acceptUsers) {
|
||||
final targetUserIds = <String>[];
|
||||
void addTargetUserId(String? userId) {
|
||||
final normalizedUserId = (userId ?? "").trim();
|
||||
if (normalizedUserId.isEmpty ||
|
||||
targetUserIds.contains(normalizedUserId)) {
|
||||
return;
|
||||
}
|
||||
targetUserIds.add(normalizedUserId);
|
||||
}
|
||||
|
||||
for (final acceptUser in acceptUsers) {
|
||||
final user = acceptUser.user;
|
||||
addTargetUserId(user?.id);
|
||||
addTargetUserId(user?.account);
|
||||
addTargetUserId(user?.getID());
|
||||
}
|
||||
return targetUserIds;
|
||||
}
|
||||
|
||||
String _createGiftBatchId(_GiftSendRequest request) {
|
||||
final sortedAcceptUserIds = List<String>.from(request.acceptUserIds)
|
||||
..sort();
|
||||
|
||||
@ -175,6 +175,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
void _handleRoomRouteVisible() {
|
||||
_releaseCoveredRouteGuard();
|
||||
_setRoomRouteVisible(true);
|
||||
_scheduleEnsureRoomVisualEffectsEnabled();
|
||||
}
|
||||
|
||||
void _scheduleRoomRouteVisible() {
|
||||
@ -249,13 +250,20 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
rtmProvider.msgFloatingGiftListener = _floatingGiftListener;
|
||||
rtmProvider.msgLuckyGiftRewardTickerListener =
|
||||
_luckyGiftRewardTickerListener;
|
||||
debugPrint('[GiftFx][voice] bind room visual effect listeners');
|
||||
}
|
||||
|
||||
void _ensureRoomVisualEffectsEnabled() {
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
if (rtcProvider.currenRoom == null ||
|
||||
!_roomRouteVisible ||
|
||||
rtcProvider.roomVisualEffectsEnabled) {
|
||||
if (rtcProvider.currenRoom == null || !_roomRouteVisible) {
|
||||
return;
|
||||
}
|
||||
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
|
||||
final needsListenerRebind =
|
||||
rtmProvider.msgFloatingGiftListener != _floatingGiftListener ||
|
||||
rtmProvider.msgLuckyGiftRewardTickerListener !=
|
||||
_luckyGiftRewardTickerListener;
|
||||
if (rtcProvider.roomVisualEffectsEnabled && !needsListenerRebind) {
|
||||
return;
|
||||
}
|
||||
_enableRoomVisualEffects();
|
||||
@ -684,13 +692,36 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
_floatingGiftListener(Msg msg) {
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
if (!rtcProvider.shouldShowRoomVisualEffects) {
|
||||
debugPrint(
|
||||
'[GiftFx][voice] skip floating listener visual effects hidden '
|
||||
'type=${msg.type} '
|
||||
'giftId=${msg.gift?.id} '
|
||||
'enabled=${rtcProvider.roomVisualEffectsEnabled} '
|
||||
'visible=${rtcProvider.isVoiceRoomRouteVisible} '
|
||||
'hasRoom=${rtcProvider.currenRoom != null}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (Provider.of<GiftProvider>(context, listen: false).hideLGiftAnimal) {
|
||||
debugPrint(
|
||||
'[GiftFx][voice] skip floating listener hidden by gift preference '
|
||||
'type=${msg.type} giftId=${msg.gift?.id}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final targetUserIds = _resolveGiftTargetUserIds(msg);
|
||||
final targetGroupKey = _buildGiftTargetGroupKey(targetUserIds);
|
||||
debugPrint(
|
||||
'[GiftFx][voice] floating listener recv '
|
||||
'type=${msg.type} '
|
||||
'giftId=${msg.gift?.id} '
|
||||
'giftTab=${msg.gift?.giftTab} '
|
||||
'quantity=${msg.number} '
|
||||
'animationCount=${msg.customAnimationCount} '
|
||||
'giftPhoto=${msg.gift?.giftPhoto} '
|
||||
'targetUserIds=${targetUserIds.join(",")} '
|
||||
'targetGroupKey=$targetGroupKey',
|
||||
);
|
||||
final shouldPlayBatchVisuals =
|
||||
targetUserIds.length <= 1 ||
|
||||
targetGroupKey == null ||
|
||||
@ -726,7 +757,10 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
listen: false,
|
||||
).enqueueGiftAnimation(giftModel);
|
||||
} else {
|
||||
return;
|
||||
debugPrint(
|
||||
'[GiftFx][voice] skip gift ticker by visual batch dedupe '
|
||||
'type=${msg.type} giftId=${msg.gift?.id} targetGroupKey=$targetGroupKey',
|
||||
);
|
||||
}
|
||||
|
||||
final giftPhoto = (msg.gift?.giftPhoto ?? "").trim();
|
||||
@ -861,7 +895,9 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
|
||||
bool _isLuckyGiftMessage(Msg msg) {
|
||||
final giftTab = (msg.gift?.giftTab ?? '').trim();
|
||||
return giftTab == "LUCK" || giftTab == SCGiftType.LUCKY_GIFT.name;
|
||||
return giftTab == "LUCK" ||
|
||||
giftTab == SCGiftType.LUCKY_GIFT.name ||
|
||||
giftTab == SCGiftType.MAGIC.name;
|
||||
}
|
||||
|
||||
bool _supportsComboMilestoneEffects(Msg msg) {
|
||||
@ -956,26 +992,46 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
List<String> targetUserIds,
|
||||
) {
|
||||
if ((msg.number ?? 0) <= 0) {
|
||||
debugPrint(
|
||||
'[GiftFx][voice] skip lucky seat flight invalid quantity '
|
||||
'giftId=${msg.gift?.id} quantity=${msg.number}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (targetGroupKey == null || targetUserIds.isEmpty) {
|
||||
debugPrint(
|
||||
'[GiftFx][voice] skip lucky seat flight empty targets '
|
||||
'giftId=${msg.gift?.id} targetGroupKey=$targetGroupKey',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetGroupKey);
|
||||
final session = _luckyGiftComboSessions[sessionKey];
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
final session = _luckyGiftComboSessions.putIfAbsent(
|
||||
sessionKey,
|
||||
() => _LuckyGiftComboSession(),
|
||||
);
|
||||
session.endTimer?.cancel();
|
||||
session.clearQueueTimer?.cancel();
|
||||
|
||||
if (_shouldPlaySeatFlightGiftAnimation(msg) && giftPhoto.isNotEmpty) {
|
||||
if (_shouldPlayLuckyGiftSeatFlightAnimation(msg) && giftPhoto.isNotEmpty) {
|
||||
final flightBatchKey = _buildGiftFlightBatchKey(msg, targetUserIds);
|
||||
if (targetUserIds.length > 1 &&
|
||||
!_markGiftFlightBatchForPlayback(flightBatchKey)) {
|
||||
debugPrint(
|
||||
'[GiftFx][voice] skip lucky seat flight by flight batch dedupe '
|
||||
'giftId=${msg.gift?.id} batchKey=$flightBatchKey',
|
||||
);
|
||||
_scheduleGiftAnimationSessionEnd(sessionKey);
|
||||
return;
|
||||
}
|
||||
debugPrint(
|
||||
'[GiftFx][voice] enqueue lucky seat flight '
|
||||
'giftId=${msg.gift?.id} '
|
||||
'giftPhoto=$giftPhoto '
|
||||
'targetUserIds=${targetUserIds.join(",")} '
|
||||
'animationCount=${_resolveTrackedAnimationCount(msg)} '
|
||||
'batchKey=$flightBatchKey',
|
||||
);
|
||||
_enqueueTrackedSeatFlightAnimations(
|
||||
sessionKey: sessionKey,
|
||||
giftPhoto: giftPhoto,
|
||||
@ -983,10 +1039,29 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
animationCount: _resolveTrackedAnimationCount(msg),
|
||||
batchKey: flightBatchKey,
|
||||
);
|
||||
} else {
|
||||
debugPrint(
|
||||
'[GiftFx][voice] skip lucky seat flight unsupported gift photo '
|
||||
'giftId=${msg.gift?.id} giftPhoto=$giftPhoto',
|
||||
);
|
||||
}
|
||||
_scheduleGiftAnimationSessionEnd(sessionKey);
|
||||
}
|
||||
|
||||
bool _shouldPlayLuckyGiftSeatFlightAnimation(Msg msg) {
|
||||
final gift = msg.gift;
|
||||
if (gift == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final giftPhoto = (gift.giftPhoto ?? "").trim();
|
||||
if (giftPhoto.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final giftPhotoExt = _normalizedGiftResourceExtension(giftPhoto);
|
||||
return !_isAnimatedGiftResource(giftPhotoExt);
|
||||
}
|
||||
|
||||
int _resolveTrackedAnimationCount(Msg msg) {
|
||||
return math.max(msg.customAnimationCount ?? 1, 1);
|
||||
}
|
||||
@ -1016,21 +1091,51 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
);
|
||||
final normalizedTargetUserIds = _normalizeGiftTargetUserIds(targetUserIds);
|
||||
if (normalizedTargetUserIds.isEmpty) {
|
||||
debugPrint(
|
||||
'[GiftFx][voice] skip seat flight enqueue empty normalized targets '
|
||||
'sessionKey=$sessionKey',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
final seenSeatKeys = <GlobalKey<State<StatefulWidget>>>{};
|
||||
final resolvedTargetUserIds = <String>[];
|
||||
final unresolvedTargetUserIds = <String>[];
|
||||
for (final targetUserId in normalizedTargetUserIds) {
|
||||
final targetKey = rtcProvider.getSeatGlobalKeyByIndex(targetUserId);
|
||||
if (targetKey == null) {
|
||||
unresolvedTargetUserIds.add(targetUserId);
|
||||
continue;
|
||||
}
|
||||
if (seenSeatKeys.add(targetKey)) {
|
||||
resolvedTargetUserIds.add(targetUserId);
|
||||
}
|
||||
}
|
||||
final flightTargetUserIds =
|
||||
resolvedTargetUserIds.isNotEmpty
|
||||
? resolvedTargetUserIds
|
||||
: normalizedTargetUserIds;
|
||||
debugPrint(
|
||||
'[GiftFx][voice] seat flight targets '
|
||||
'sessionKey=$sessionKey '
|
||||
'requested=${normalizedTargetUserIds.join(",")} '
|
||||
'resolved=${resolvedTargetUserIds.join(",")} '
|
||||
'unresolved=${unresolvedTargetUserIds.join(",")} '
|
||||
'used=${flightTargetUserIds.join(",")}',
|
||||
);
|
||||
final normalizedAnimationCount = math.max(animationCount, 1);
|
||||
final cappedAnimationCount = math.min(
|
||||
normalizedAnimationCount,
|
||||
_maxTrackedGiftAnimations,
|
||||
);
|
||||
final maxTrackedRequests = math.max(
|
||||
_maxTrackedGiftAnimations * normalizedTargetUserIds.length,
|
||||
_maxTrackedGiftAnimations * flightTargetUserIds.length,
|
||||
_maxTrackedGiftAnimations,
|
||||
);
|
||||
for (var index = 0; index < cappedAnimationCount; index += 1) {
|
||||
final roundBatchTag =
|
||||
normalizedTargetUserIds.length > 1 ? '$batchKey|round:$index' : null;
|
||||
for (final targetUserId in normalizedTargetUserIds) {
|
||||
flightTargetUserIds.length > 1 ? '$batchKey|round:$index' : null;
|
||||
for (final targetUserId in flightTargetUserIds) {
|
||||
_giftSeatFlightController.enqueueLimited(
|
||||
RoomGiftSeatFlightRequest(
|
||||
imagePath: giftPhoto,
|
||||
|
||||
@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:webview_flutter_android/webview_flutter_android.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/modules/room_game/bridge/baishun_js_bridge.dart';
|
||||
import 'package:yumi/modules/room_game/data/models/room_game_models.dart';
|
||||
@ -50,6 +51,7 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
bool _didReceiveBridgeMessage = false;
|
||||
bool _didFinishPageLoad = false;
|
||||
bool _hasDeliveredLaunchConfig = false;
|
||||
bool _shouldMountWebView = defaultTargetPlatform != TargetPlatform.android;
|
||||
int _bridgeInjectCount = 0;
|
||||
String? _errorMessage;
|
||||
|
||||
@ -105,6 +107,7 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
onPageFinished: (String url) async {
|
||||
_didFinishPageLoad = true;
|
||||
_log('page_finished url=${_clip(url, 240)}');
|
||||
_showWebView(reason: 'page_finished');
|
||||
await _injectBridge(reason: 'page_finished');
|
||||
},
|
||||
onWebResourceError: (WebResourceError error) {
|
||||
@ -142,6 +145,7 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
_didReceiveBridgeMessage = false;
|
||||
_didFinishPageLoad = false;
|
||||
_hasDeliveredLaunchConfig = false;
|
||||
_shouldMountWebView = defaultTargetPlatform != TargetPlatform.android;
|
||||
_bridgeInjectCount = 0;
|
||||
_stopBridgeBootstrap(reason: 'prepare_page_load');
|
||||
_bridgeBootstrapTimer = Timer.periodic(const Duration(milliseconds: 250), (
|
||||
@ -173,6 +177,7 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
return;
|
||||
}
|
||||
_log('loading_fallback_fire isLoading=$_isLoading');
|
||||
_showWebView(reason: 'loading_fallback');
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
@ -326,6 +331,7 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
return;
|
||||
}
|
||||
_log('game_loaded received');
|
||||
_showWebView(reason: 'game_loaded');
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = null;
|
||||
@ -357,6 +363,16 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
}
|
||||
}
|
||||
|
||||
void _showWebView({required String reason}) {
|
||||
if (_shouldMountWebView || !mounted) {
|
||||
return;
|
||||
}
|
||||
_log('mount_webview reason=$reason');
|
||||
setState(() {
|
||||
_shouldMountWebView = true;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _reload() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
@ -412,7 +428,11 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
return '';
|
||||
}
|
||||
|
||||
void _log(String message) {}
|
||||
void _log(String message) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('$_logPrefix $message');
|
||||
}
|
||||
}
|
||||
|
||||
String _clip(String value, [int limit = 600]) {
|
||||
final trimmed = value.trim();
|
||||
@ -631,6 +651,26 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
return screenSize.width / ratio;
|
||||
}
|
||||
|
||||
Widget _buildWebView() {
|
||||
final params = PlatformWebViewWidgetCreationParams(
|
||||
controller: _controller.platform,
|
||||
gestureRecognizers: _webGestureRecognizers,
|
||||
);
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
return WebViewWidget.fromPlatformCreationParams(
|
||||
params:
|
||||
AndroidWebViewWidgetCreationParams.fromPlatformWebViewWidgetCreationParams(
|
||||
params,
|
||||
displayWithHybridComposition: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
return WebViewWidget(
|
||||
controller: _controller,
|
||||
gestureRecognizers: _webGestureRecognizers,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final topCrop = 28.w;
|
||||
@ -660,16 +700,14 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
color: const Color(0xFF081915),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: -topCrop,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: bottomFrameHeight,
|
||||
child: WebViewWidget(
|
||||
controller: _controller,
|
||||
gestureRecognizers: _webGestureRecognizers,
|
||||
if (_shouldMountWebView)
|
||||
Positioned(
|
||||
top: -topCrop,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: bottomFrameHeight,
|
||||
child: _buildWebView(),
|
||||
),
|
||||
),
|
||||
if (bottomFrameHeight > 0)
|
||||
Positioned(
|
||||
left: 0,
|
||||
@ -682,10 +720,12 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
),
|
||||
if (_errorMessage != null) _buildErrorState(),
|
||||
if (_isLoading && _errorMessage == null)
|
||||
const IgnorePointer(
|
||||
ignoring: true,
|
||||
child: BaishunLoadingView(
|
||||
message: 'Waiting for gameLoaded...',
|
||||
const Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
ignoring: true,
|
||||
child: BaishunLoadingView(
|
||||
message: 'Waiting for gameLoaded...',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@ -14,7 +14,7 @@ class BaishunLoadingView extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned.fill(
|
||||
return SizedBox.expand(
|
||||
child: Container(
|
||||
color: backgroundColor,
|
||||
alignment: Alignment.center,
|
||||
|
||||
@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:webview_flutter_android/webview_flutter_android.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/modules/room_game/bridge/leader_js_bridge.dart';
|
||||
import 'package:yumi/modules/room_game/data/models/room_game_models.dart';
|
||||
@ -50,6 +51,7 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
||||
bool _isClosing = false;
|
||||
bool _didReceiveBridgeMessage = false;
|
||||
bool _didFinishPageLoad = false;
|
||||
bool _shouldMountWebView = defaultTargetPlatform != TargetPlatform.android;
|
||||
int _bridgeInjectCount = 0;
|
||||
String? _errorMessage;
|
||||
|
||||
@ -105,6 +107,7 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
||||
onPageFinished: (String url) async {
|
||||
_didFinishPageLoad = true;
|
||||
_log('page_finished url=${_clip(url, 240)}');
|
||||
_showWebView(reason: 'page_finished');
|
||||
await _injectBridge(reason: 'page_finished');
|
||||
},
|
||||
onWebResourceError: (WebResourceError error) {
|
||||
@ -136,6 +139,7 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
||||
void _prepareForPageLoad() {
|
||||
_didReceiveBridgeMessage = false;
|
||||
_didFinishPageLoad = false;
|
||||
_shouldMountWebView = defaultTargetPlatform != TargetPlatform.android;
|
||||
_bridgeInjectCount = 0;
|
||||
_stopBridgeBootstrap(reason: 'prepare_page_load');
|
||||
_bridgeBootstrapTimer = Timer.periodic(const Duration(milliseconds: 250), (
|
||||
@ -155,6 +159,7 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
||||
return;
|
||||
}
|
||||
_log('loading_fallback_fire');
|
||||
_showWebView(reason: 'loading_fallback');
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
@ -274,6 +279,7 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
_showWebView(reason: 'load_complete');
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = null;
|
||||
@ -294,6 +300,16 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
||||
}
|
||||
}
|
||||
|
||||
void _showWebView({required String reason}) {
|
||||
if (_shouldMountWebView || !mounted) {
|
||||
return;
|
||||
}
|
||||
_log('mount_webview reason=$reason');
|
||||
setState(() {
|
||||
_shouldMountWebView = true;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _reload() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
@ -385,7 +401,31 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
void _log(String message) {}
|
||||
Widget _buildWebView() {
|
||||
final params = PlatformWebViewWidgetCreationParams(
|
||||
controller: _controller.platform,
|
||||
gestureRecognizers: _webGestureRecognizers,
|
||||
);
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
return WebViewWidget.fromPlatformCreationParams(
|
||||
params:
|
||||
AndroidWebViewWidgetCreationParams.fromPlatformWebViewWidgetCreationParams(
|
||||
params,
|
||||
displayWithHybridComposition: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
return WebViewWidget(
|
||||
controller: _controller,
|
||||
gestureRecognizers: _webGestureRecognizers,
|
||||
);
|
||||
}
|
||||
|
||||
void _log(String message) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('$_logPrefix $message');
|
||||
}
|
||||
}
|
||||
|
||||
String _maskValue(String value, {int keepStart = 6, int keepEnd = 4}) {
|
||||
final trimmed = value.trim();
|
||||
@ -566,16 +606,14 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
||||
color: const Color(0xFF081915),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: bottomFrameHeight,
|
||||
child: WebViewWidget(
|
||||
controller: _controller,
|
||||
gestureRecognizers: _webGestureRecognizers,
|
||||
if (_shouldMountWebView)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: bottomFrameHeight,
|
||||
child: _buildWebView(),
|
||||
),
|
||||
),
|
||||
if (bottomFrameHeight > 0)
|
||||
Positioned(
|
||||
left: 0,
|
||||
@ -588,10 +626,12 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
||||
),
|
||||
if (_errorMessage != null) _buildErrorState(),
|
||||
if (_isLoading && _errorMessage == null)
|
||||
const IgnorePointer(
|
||||
ignoring: true,
|
||||
child: BaishunLoadingView(
|
||||
message: 'Waiting for Leader game...',
|
||||
const Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
ignoring: true,
|
||||
child: BaishunLoadingView(
|
||||
message: 'Waiting for Leader game...',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -192,7 +193,11 @@ class _RoomGameListSheetState extends State<RoomGameListSheet> {
|
||||
}
|
||||
}
|
||||
|
||||
void _log(String message) {}
|
||||
void _log(String message) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('$_logPrefix $message');
|
||||
}
|
||||
}
|
||||
|
||||
String _clip(String value, [int limit = 600]) {
|
||||
final trimmed = value.trim();
|
||||
|
||||
@ -51,6 +51,8 @@ import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart';
|
||||
import '../../shared/data_sources/models/enum/sc_room_info_event_type.dart';
|
||||
import '../../shared/data_sources/models/enum/sc_room_roles_type.dart';
|
||||
import '../../shared/business_logic/models/res/sc_is_follow_room_res.dart';
|
||||
import '../../shared/business_logic/models/res/sc_mic_go_up_res.dart'
|
||||
hide PropsResources;
|
||||
import '../../shared/business_logic/models/res/sc_room_rocket_api_res.dart';
|
||||
import '../../shared/business_logic/models/res/sc_room_red_packet_config_res.dart';
|
||||
import '../../shared/business_logic/models/res/sc_room_red_packet_list_res.dart';
|
||||
@ -523,13 +525,19 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
Set<String> _currentUserIdentityKeys() {
|
||||
final currentUser = AccountStorage().getCurrentUser()?.userProfile;
|
||||
return {currentUser?.id?.trim() ?? "", currentUser?.account?.trim() ?? ""}
|
||||
..removeWhere((item) => item.isEmpty);
|
||||
return {
|
||||
currentUser?.id?.trim() ?? "",
|
||||
currentUser?.account?.trim() ?? "",
|
||||
currentUser?.getID().trim() ?? "",
|
||||
}..removeWhere((item) => item.isEmpty);
|
||||
}
|
||||
|
||||
Set<String> _userProfileIdentityKeys(SocialChatUserProfile? user) {
|
||||
return {user?.id?.trim() ?? "", user?.account?.trim() ?? ""}
|
||||
..removeWhere((item) => item.isEmpty);
|
||||
return {
|
||||
user?.id?.trim() ?? "",
|
||||
user?.account?.trim() ?? "",
|
||||
user?.getID().trim() ?? "",
|
||||
}..removeWhere((item) => item.isEmpty);
|
||||
}
|
||||
|
||||
bool _userProfileMatchesAnyIdentity(
|
||||
@ -1564,8 +1572,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _syncSelfMicRuntimeState() {
|
||||
final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id;
|
||||
if ((currentUserId ?? "").isEmpty) {
|
||||
final currentUserKeys = _currentUserIdentityKeys();
|
||||
if (currentUserKeys.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||
@ -2417,6 +2425,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
bool keepAudioPublishing = false,
|
||||
}) async {
|
||||
if (!_roomRtcJoined || _roomRtcEngineAdapter == null) {
|
||||
debugPrint(
|
||||
'[RoomRtc] switchToBroadcaster skipped joined=$_roomRtcJoined '
|
||||
'hasAdapter=${_roomRtcEngineAdapter != null}',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
@ -2425,6 +2437,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
RoomRtcAnchorConfig(muted: muted, publishMuted: localAudioMuted),
|
||||
);
|
||||
if (!switched) {
|
||||
debugPrint('[RoomRtc] switchToBroadcaster failed adapter=false');
|
||||
_roomRtcRoleIsBroadcaster = false;
|
||||
_resyncHeartbeatFromRoomRtcState();
|
||||
return false;
|
||||
@ -2437,6 +2450,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_resyncHeartbeatFromRoomRtcState();
|
||||
return true;
|
||||
} catch (error) {
|
||||
debugPrint('[RoomRtc] switchToBroadcaster error=$error');
|
||||
_roomRtcRoleIsBroadcaster = false;
|
||||
_resyncHeartbeatFromRoomRtcState();
|
||||
return false;
|
||||
@ -5198,14 +5212,63 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
if (normalizedUserId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final lookupKeys = <String>{normalizedUserId};
|
||||
final currentUserKeys = _currentUserIdentityKeys();
|
||||
if (currentUserKeys.contains(normalizedUserId)) {
|
||||
lookupKeys.addAll(currentUserKeys);
|
||||
}
|
||||
roomWheatMap.forEach((seatIndex, seat) {
|
||||
if (seat.user?.id != normalizedUserId || seatIndex == exceptIndex) {
|
||||
final seatUserKeys = _userProfileIdentityKeys(seat.user);
|
||||
if (!seatUserKeys.any(lookupKeys.contains) || seatIndex == exceptIndex) {
|
||||
return;
|
||||
}
|
||||
roomWheatMap[seatIndex] = seat.copyWith(clearUser: true);
|
||||
});
|
||||
}
|
||||
|
||||
void _applySelfMicGoUpState({
|
||||
required SocialChatUserProfile myUser,
|
||||
required num targetIndex,
|
||||
required SCMicGoUpRes micGoUpRes,
|
||||
required bool selfMicMuted,
|
||||
}) {
|
||||
final previousSelfSeatIndex = userOnMaiInIndex(myUser.id ?? "");
|
||||
final isSeatSwitching =
|
||||
previousSelfSeatIndex > -1 && previousSelfSeatIndex != targetIndex;
|
||||
|
||||
_clearSelfMicReleaseGuard();
|
||||
if (isSeatSwitching) {
|
||||
_startSelfMicSwitchGuard(
|
||||
sourceIndex: previousSelfSeatIndex,
|
||||
targetIndex: targetIndex,
|
||||
);
|
||||
} else {
|
||||
_clearSelfMicSwitchGuard();
|
||||
_startSelfMicGoUpGuard(targetIndex: targetIndex);
|
||||
}
|
||||
_clearUserFromSeats(myUser.id, exceptIndex: targetIndex);
|
||||
|
||||
final currentSeat = roomWheatMap[targetIndex];
|
||||
final currentUser = myUser.copyWith(roles: currenRoom?.entrants?.roles);
|
||||
roomWheatMap[targetIndex] =
|
||||
currentSeat?.copyWith(
|
||||
user: currentUser,
|
||||
micMute: micGoUpRes.micMute,
|
||||
micLock: micGoUpRes.micLock,
|
||||
roomToken: micGoUpRes.roomToken,
|
||||
) ??
|
||||
MicRes(
|
||||
roomId: currenRoom?.roomProfile?.roomProfile?.id,
|
||||
micIndex: targetIndex,
|
||||
micLock: micGoUpRes.micLock,
|
||||
micMute: micGoUpRes.micMute,
|
||||
user: currentUser,
|
||||
roomToken: micGoUpRes.roomToken,
|
||||
);
|
||||
isMic = selfMicMuted;
|
||||
_syncSelfMicRuntimeState();
|
||||
}
|
||||
|
||||
void _openRoomUserInfoCard(String? userId) {
|
||||
final normalizedUserId = (userId ?? '').trim();
|
||||
if (normalizedUserId.isEmpty) {
|
||||
@ -5269,66 +5332,29 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
final targetIndex = micGoUpRes.micIndex ?? index;
|
||||
final selfMicMuted = micGoUpRes.micMute ?? false;
|
||||
if (myUser != null) {
|
||||
_applySelfMicGoUpState(
|
||||
myUser: myUser,
|
||||
targetIndex: targetIndex,
|
||||
micGoUpRes: micGoUpRes,
|
||||
selfMicMuted: selfMicMuted,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
final switched = await _switchRoomRtcToBroadcaster(
|
||||
muted: selfMicMuted,
|
||||
keepAudioPublishing: selfMicMuted && _currentUserPublishingRoomMusic,
|
||||
);
|
||||
if (!switched) {
|
||||
await _switchRoomRtcToAudience();
|
||||
try {
|
||||
await SCChatRoomRepository().micGoDown(
|
||||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
targetIndex,
|
||||
);
|
||||
} catch (downError) {}
|
||||
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
||||
_clearSelfMicGoUpGuard(clearPreferredIndex: true);
|
||||
_clearUserFromSeats(myUser?.id);
|
||||
_syncSelfMicRuntimeState();
|
||||
notifyListeners();
|
||||
_refreshMicListSilently(notifyIfUnchanged: true);
|
||||
_handleSelfMicGoUpFailure(
|
||||
'Failed to put on the microphone, TRTC switch failed',
|
||||
_resetSelfMicGoUpFailureState();
|
||||
SCTts.show(
|
||||
'Microphone audio failed to start, please try switching microphone again',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final previousSelfSeatIndex =
|
||||
myUser != null ? userOnMaiInIndex(myUser.id ?? "") : -1;
|
||||
final isSeatSwitching =
|
||||
previousSelfSeatIndex > -1 && previousSelfSeatIndex != targetIndex;
|
||||
if (myUser != null) {
|
||||
_clearSelfMicReleaseGuard();
|
||||
if (isSeatSwitching) {
|
||||
_startSelfMicSwitchGuard(
|
||||
sourceIndex: previousSelfSeatIndex,
|
||||
targetIndex: targetIndex,
|
||||
);
|
||||
} else {
|
||||
_clearSelfMicSwitchGuard();
|
||||
_startSelfMicGoUpGuard(targetIndex: targetIndex);
|
||||
}
|
||||
_clearUserFromSeats(myUser.id, exceptIndex: targetIndex);
|
||||
final currentSeat = roomWheatMap[targetIndex];
|
||||
final currentUser = myUser.copyWith(roles: currenRoom?.entrants?.roles);
|
||||
roomWheatMap[targetIndex] =
|
||||
currentSeat?.copyWith(
|
||||
user: currentUser,
|
||||
micMute: micGoUpRes.micMute,
|
||||
micLock: micGoUpRes.micLock,
|
||||
roomToken: micGoUpRes.roomToken,
|
||||
) ??
|
||||
MicRes(
|
||||
roomId: currenRoom?.roomProfile?.roomProfile?.id,
|
||||
micIndex: targetIndex,
|
||||
micLock: micGoUpRes.micLock,
|
||||
micMute: micGoUpRes.micMute,
|
||||
user: currentUser,
|
||||
roomToken: micGoUpRes.roomToken,
|
||||
);
|
||||
}
|
||||
isMic = selfMicMuted;
|
||||
_syncSelfMicRuntimeState();
|
||||
if (roomWheatMap[targetIndex]?.micMute ?? false) {
|
||||
///房主上麦自动解禁麦位
|
||||
if (isFz()) {
|
||||
@ -5477,6 +5503,13 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
///点击的用户在哪个麦上
|
||||
num userOnMaiInIndex(String userId) {
|
||||
return _findUserOnMaiInIndex(userId, useDisplaySeatForCurrentUser: true);
|
||||
}
|
||||
|
||||
num _findUserOnMaiInIndex(
|
||||
String userId, {
|
||||
required bool useDisplaySeatForCurrentUser,
|
||||
}) {
|
||||
final normalizedUserId = userId.trim();
|
||||
if (normalizedUserId.isEmpty) {
|
||||
return -1;
|
||||
@ -5484,12 +5517,17 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
num index = -1;
|
||||
final currentUserKeys = _currentUserIdentityKeys();
|
||||
final isCurrentUserQuery = currentUserKeys.contains(normalizedUserId);
|
||||
final lookupKeys =
|
||||
isCurrentUserQuery ? currentUserKeys : <String>{normalizedUserId}
|
||||
..removeWhere((item) => item.isEmpty);
|
||||
final lookupKeys = <String>{normalizedUserId};
|
||||
if (isCurrentUserQuery) {
|
||||
lookupKeys.addAll(currentUserKeys);
|
||||
}
|
||||
lookupKeys.removeWhere((item) => item.isEmpty);
|
||||
roomWheatMap.forEach((k, value) {
|
||||
final visibleSeat = isCurrentUserQuery ? micAtIndexForDisplay(k) : value;
|
||||
if (_userProfileMatchesAnyIdentity(visibleSeat?.user, lookupKeys)) {
|
||||
final candidateSeat =
|
||||
useDisplaySeatForCurrentUser && isCurrentUserQuery
|
||||
? micAtIndexForDisplay(k)
|
||||
: value;
|
||||
if (_userProfileMatchesAnyIdentity(candidateSeat?.user, lookupKeys)) {
|
||||
index = k;
|
||||
}
|
||||
});
|
||||
@ -5780,7 +5818,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
GlobalKey<State<StatefulWidget>>? getSeatGlobalKeyByIndex(String userId) {
|
||||
///需要有人的座位
|
||||
num index = userOnMaiInIndex(userId);
|
||||
num index = _findUserOnMaiInIndex(
|
||||
userId,
|
||||
useDisplaySeatForCurrentUser: false,
|
||||
);
|
||||
if (index > -1) {
|
||||
return seatGlobalKeyMap[index];
|
||||
}
|
||||
|
||||
@ -276,6 +276,33 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
].join("|");
|
||||
}
|
||||
|
||||
bool _shouldNotifyFloatingGiftListener(Msg msg) {
|
||||
if (msg.type == SCRoomMsgType.gift) {
|
||||
return true;
|
||||
}
|
||||
if (msg.type == SCRoomMsgType.luckGiftAnimOther) {
|
||||
return msg.user != null && msg.gift != null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _notifyFloatingGiftListenerFromAddMsg(Msg msg) {
|
||||
if (!_shouldNotifyFloatingGiftListener(msg)) {
|
||||
return;
|
||||
}
|
||||
_giftFxLog(
|
||||
'trigger floating gift listener from addMsg '
|
||||
'type=${msg.type} '
|
||||
'giftId=${msg.gift?.id} '
|
||||
'sendUserId=${msg.user?.id} '
|
||||
'toUserId=${msg.toUser?.id} '
|
||||
'quantity=${msg.number} '
|
||||
'targetUserIds=${(msg.targetUserIds ?? const <String>[]).join(",")} '
|
||||
'hasListener=${msgFloatingGiftListener != null}',
|
||||
);
|
||||
msgFloatingGiftListener?.call(msg);
|
||||
}
|
||||
|
||||
String _firstNonBlank(Iterable<String?> values) {
|
||||
for (final value in values) {
|
||||
final text = value?.trim();
|
||||
@ -2977,9 +3004,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
if (mergedGiftMsg != null) {
|
||||
msgAllListener?.call(mergedGiftMsg);
|
||||
msgGiftListener?.call(mergedGiftMsg);
|
||||
if (msg.type == SCRoomMsgType.gift) {
|
||||
msgFloatingGiftListener?.call(msg);
|
||||
}
|
||||
_notifyFloatingGiftListenerFromAddMsg(msg);
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
@ -3071,12 +3096,41 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
roomGiftMsgList.removeAt(roomGiftMsgList.length - 1);
|
||||
}
|
||||
msgGiftListener?.call(msg);
|
||||
if (msg.type == SCRoomMsgType.gift) {
|
||||
msgFloatingGiftListener?.call(msg);
|
||||
}
|
||||
_notifyFloatingGiftListenerFromAddMsg(msg);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> _luckGiftTargetUserIdsFromMsg(Msg msg) {
|
||||
final targetUserIds = <String>[];
|
||||
void addTargetUserId(dynamic userId) {
|
||||
final normalizedUserId = userId?.toString().trim() ?? '';
|
||||
if (normalizedUserId.isEmpty ||
|
||||
targetUserIds.contains(normalizedUserId)) {
|
||||
return;
|
||||
}
|
||||
targetUserIds.add(normalizedUserId);
|
||||
}
|
||||
|
||||
for (final userId in msg.targetUserIds ?? const <String>[]) {
|
||||
addTargetUserId(userId);
|
||||
}
|
||||
|
||||
final rawTargetUserIds = (msg.msg ?? '').trim();
|
||||
if (rawTargetUserIds.startsWith('[')) {
|
||||
try {
|
||||
final decoded = jsonDecode(rawTargetUserIds);
|
||||
if (decoded is List) {
|
||||
for (final userId in decoded) {
|
||||
addTargetUserId(userId);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
addTargetUserId(msg.toUser?.id);
|
||||
return targetUserIds;
|
||||
}
|
||||
|
||||
void _enqueueCpRelationBroadcastFloatingIfNeeded(
|
||||
Msg msg, {
|
||||
bool isRegionBroadcast = false,
|
||||
@ -6490,10 +6544,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
'giftPhoto=${msg.gift?.giftPhoto}',
|
||||
);
|
||||
} else {
|
||||
final targetUserIds =
|
||||
(jsonDecode(msg.msg ?? "") as List)
|
||||
.map((e) => e as String)
|
||||
.toList();
|
||||
final targetUserIds = _luckGiftTargetUserIdsFromMsg(msg);
|
||||
_giftFxLog(
|
||||
'recv LUCK_GIFT_ANIM_OTHER '
|
||||
'giftPhoto=${msg.gift?.giftPhoto} '
|
||||
@ -6508,16 +6559,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
targetUserIds,
|
||||
),
|
||||
);
|
||||
if (msg.user != null && msg.toUser != null && msg.gift != null) {
|
||||
_giftFxLog(
|
||||
'trigger floating gift listener from LUCK_GIFT_ANIM_OTHER '
|
||||
'sendUserId=${msg.user?.id} '
|
||||
'toUserId=${msg.toUser?.id} '
|
||||
'quantity=${msg.number} '
|
||||
'giftId=${msg.gift?.id}',
|
||||
);
|
||||
msgFloatingGiftListener?.call(msg);
|
||||
} else {
|
||||
if (!_shouldNotifyFloatingGiftListener(msg)) {
|
||||
_giftFxLog(
|
||||
'skip floating gift listener from LUCK_GIFT_ANIM_OTHER '
|
||||
'reason=incomplete_msg '
|
||||
|
||||
@ -5,6 +5,10 @@ import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||
|
||||
void _seatFlightLog(String message) {
|
||||
debugPrint('[GiftFx][seatFlight] $message');
|
||||
}
|
||||
|
||||
typedef RoomGiftSeatTargetResolver =
|
||||
GlobalKey<State<StatefulWidget>>? Function(String targetUserId);
|
||||
|
||||
@ -52,8 +56,19 @@ class RoomGiftSeatFlightController {
|
||||
if (_state == null) {
|
||||
_trimPendingRequestsOverflow();
|
||||
_pendingRequests.add(normalizedRequest);
|
||||
_seatFlightLog(
|
||||
'buffer request no state '
|
||||
'target=${normalizedRequest.targetUserId} '
|
||||
'queueTag=${normalizedRequest.queueTag} '
|
||||
'pending=${_pendingRequests.length}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
_seatFlightLog(
|
||||
'enqueue via attached state '
|
||||
'target=${normalizedRequest.targetUserId} '
|
||||
'queueTag=${normalizedRequest.queueTag}',
|
||||
);
|
||||
_state!._enqueue(normalizedRequest);
|
||||
}
|
||||
|
||||
@ -76,8 +91,20 @@ class RoomGiftSeatFlightController {
|
||||
_trimPendingRequestsForTag(queueTag, maxTrackedRequests);
|
||||
_trimPendingRequestsOverflow();
|
||||
_pendingRequests.add(normalizedRequest);
|
||||
_seatFlightLog(
|
||||
'buffer limited request no state '
|
||||
'target=${normalizedRequest.targetUserId} '
|
||||
'queueTag=$queueTag '
|
||||
'pending=${_pendingRequests.length}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
_seatFlightLog(
|
||||
'enqueue limited via attached state '
|
||||
'target=${normalizedRequest.targetUserId} '
|
||||
'queueTag=$queueTag '
|
||||
'max=$maxTrackedRequests',
|
||||
);
|
||||
_state!._enqueueLimited(
|
||||
normalizedRequest,
|
||||
maxTrackedRequests: maxTrackedRequests,
|
||||
@ -184,6 +211,7 @@ class RoomGiftSeatFlightController {
|
||||
|
||||
void _attach(_RoomGiftSeatFlightOverlayState state) {
|
||||
_state = state;
|
||||
_seatFlightLog('attach overlay pending=${_pendingRequests.length}');
|
||||
while (_pendingRequests.isNotEmpty) {
|
||||
state._enqueue(_pendingRequests.removeFirst());
|
||||
}
|
||||
@ -191,6 +219,7 @@ class RoomGiftSeatFlightController {
|
||||
|
||||
void _detach(_RoomGiftSeatFlightOverlayState state) {
|
||||
if (_state == state) {
|
||||
_seatFlightLog('detach overlay');
|
||||
_state = null;
|
||||
}
|
||||
}
|
||||
@ -262,6 +291,13 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
_ensureCenterVisual(request);
|
||||
_trimQueuedRequestsOverflow();
|
||||
_queue.add(_QueuedRoomGiftSeatFlightRequest(request: request));
|
||||
_seatFlightLog(
|
||||
'state enqueue '
|
||||
'target=${request.targetUserId} '
|
||||
'queue=${_queue.length} '
|
||||
'queueTag=${request.queueTag} '
|
||||
'batchTag=${request.batchTag}',
|
||||
);
|
||||
_syncIdleCenterVisual();
|
||||
_scheduleNextAnimation();
|
||||
}
|
||||
@ -411,6 +447,10 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
_queue.isEmpty ||
|
||||
(expectedSessionToken != null &&
|
||||
expectedSessionToken != _sessionToken)) {
|
||||
_seatFlightLog(
|
||||
'skip start mounted=$mounted playing=$_isPlaying queue=${_queue.length} '
|
||||
'expected=$expectedSessionToken session=$_sessionToken',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -441,9 +481,16 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
);
|
||||
}
|
||||
|
||||
final shouldRetryBatch = unresolvedBatch.any(
|
||||
(queuedRequest) => queuedRequest.retryCount < 6,
|
||||
_seatFlightLog(
|
||||
'start batch size=${queuedBatch.length} '
|
||||
'active=${activeFlights.length} '
|
||||
'unresolved=${unresolvedBatch.map((item) => item.request.targetUserId).join(",")} '
|
||||
'queueRemaining=${_queue.length}',
|
||||
);
|
||||
|
||||
final shouldRetryBatch =
|
||||
activeFlights.isEmpty &&
|
||||
unresolvedBatch.any((queuedRequest) => queuedRequest.retryCount < 6);
|
||||
if (shouldRetryBatch) {
|
||||
final retryBatch =
|
||||
queuedBatch
|
||||
@ -466,6 +513,7 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
}
|
||||
|
||||
if (activeFlights.isEmpty) {
|
||||
_seatFlightLog('drop batch no resolved targets');
|
||||
_syncIdleCenterVisual();
|
||||
_scheduleNextAnimation();
|
||||
return;
|
||||
@ -594,6 +642,13 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
final targetKey = widget.resolveTargetKey(targetUserId);
|
||||
final targetContext = targetKey?.currentContext;
|
||||
if (overlayContext == null || targetContext == null) {
|
||||
_seatFlightLog(
|
||||
'resolve target failed context '
|
||||
'target=$targetUserId '
|
||||
'hasOverlayContext=${overlayContext != null} '
|
||||
'hasTargetKey=${targetKey != null} '
|
||||
'hasTargetContext=${targetContext != null}',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -603,6 +658,12 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
targetBox is! RenderBox ||
|
||||
!overlayBox.hasSize ||
|
||||
!targetBox.hasSize) {
|
||||
_seatFlightLog(
|
||||
'resolve target failed render '
|
||||
'target=$targetUserId '
|
||||
'overlayBox=${overlayBox.runtimeType} '
|
||||
'targetBox=${targetBox.runtimeType}',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ class FloatingLuckyGiftGlobalScreenWidget extends StatefulWidget {
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.onAnimationCompleted,
|
||||
this.debugDisableAutoSlide = false,
|
||||
});
|
||||
|
||||
static const int floatingType = 7;
|
||||
@ -24,6 +25,7 @@ class FloatingLuckyGiftGlobalScreenWidget extends StatefulWidget {
|
||||
|
||||
final SCFloatingMessage message;
|
||||
final VoidCallback onAnimationCompleted;
|
||||
final bool debugDisableAutoSlide;
|
||||
|
||||
@override
|
||||
State<FloatingLuckyGiftGlobalScreenWidget> createState() =>
|
||||
@ -73,7 +75,7 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
|
||||
});
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
if (mounted && !widget.debugDisableAutoSlide) {
|
||||
_controller.forward();
|
||||
}
|
||||
});
|
||||
@ -94,29 +96,19 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
|
||||
onTap: _handleTap,
|
||||
onHorizontalDragEnd: _handleHorizontalDragEnd,
|
||||
child: SlideTransition(
|
||||
position: _isSwipeAnimating ? _swipeAnimation : _offsetAnimation,
|
||||
position:
|
||||
widget.debugDisableAutoSlide
|
||||
? const AlwaysStoppedAnimation(Offset.zero)
|
||||
: (_isSwipeAnimating ? _swipeAnimation : _offsetAnimation),
|
||||
child: SizedBox(
|
||||
width: FloatingLuckyGiftGlobalScreenWidget.bannerWidth.w,
|
||||
height: FloatingLuckyGiftGlobalScreenWidget.bannerHeight.w,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned(left: 70.w, top: 34.w, child: _buildAvatar()),
|
||||
Positioned.fill(child: _buildBackground()),
|
||||
Positioned(left: 30.w, top: 22.w, child: _buildAvatar()),
|
||||
Positioned(
|
||||
left: 88.w,
|
||||
top: 26.w,
|
||||
width: 160.w,
|
||||
height: 45.w,
|
||||
child: _buildTextBlock(),
|
||||
),
|
||||
Positioned(
|
||||
right: 23.w,
|
||||
top: 24.w,
|
||||
width: 61.w,
|
||||
height: 45.w,
|
||||
child: _buildMultiple(),
|
||||
),
|
||||
Positioned.fill(child: _buildContentOverlay()),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -150,6 +142,28 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContentOverlay() {
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned(
|
||||
left: 110.w,
|
||||
top: 24.w,
|
||||
width: 120.w,
|
||||
height: 46.w,
|
||||
child: _buildTextBlock(),
|
||||
),
|
||||
Positioned(
|
||||
left: 220.w,
|
||||
top: 24.w,
|
||||
width: 70.w,
|
||||
height: 46.w,
|
||||
child: _buildMultiple(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAvatar() {
|
||||
const fallbackAsset = "sc_images/general/sc_icon_avar_defalt.png";
|
||||
final avatarUrl = (widget.message.userAvatarUrl ?? "").trim();
|
||||
@ -157,37 +171,28 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
|
||||
avatarUrl.isEmpty
|
||||
? Image.asset(
|
||||
fallbackAsset,
|
||||
width: 48.w,
|
||||
height: 48.w,
|
||||
width: 26.w,
|
||||
height: 26.w,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: Image(
|
||||
image: buildCachedImageProvider(
|
||||
avatarUrl,
|
||||
logicalWidth: 48.w,
|
||||
logicalHeight: 48.w,
|
||||
logicalWidth: 38.w,
|
||||
logicalHeight: 38.w,
|
||||
),
|
||||
width: 48.w,
|
||||
height: 48.w,
|
||||
width: 38.w,
|
||||
height: 38.w,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(_, __, ___) => Image.asset(
|
||||
fallbackAsset,
|
||||
width: 48.w,
|
||||
height: 48.w,
|
||||
width: 38.w,
|
||||
height: 38.w,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
return Container(
|
||||
width: 48.w,
|
||||
height: 48.w,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: const Color(0xFFFFF4C2), width: 1.5.w),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: avatar,
|
||||
);
|
||||
return ClipOval(child: avatar);
|
||||
}
|
||||
|
||||
Widget _buildTextBlock() {
|
||||
@ -201,20 +206,20 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
|
||||
title,
|
||||
TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 19.sp,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
shadows: _textShadows(),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 7.w),
|
||||
SizedBox(height: 1.w),
|
||||
_singleLineText(
|
||||
subtitle,
|
||||
TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 10.5.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
shadows: _textShadows(),
|
||||
@ -234,7 +239,7 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
|
||||
textScaler: TextScaler.noScaling,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 32.sp,
|
||||
fontSize: 23.sp,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
|
||||
@ -377,41 +377,42 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
||||
}
|
||||
final imageReplacementsByName =
|
||||
_imageReplacementsByName ?? const <String, Uint8List>{};
|
||||
final useNativeAssetView =
|
||||
assetName != null && Platform.isIOS && widget.loop;
|
||||
final useNativeView = _shouldUseNativeRoomRocketPagView(
|
||||
isIOS: Platform.isIOS,
|
||||
hasAsset: assetName != null,
|
||||
hasBytes: bytes != null,
|
||||
);
|
||||
final pagView =
|
||||
assetName != null
|
||||
? PAGView.asset(
|
||||
assetName,
|
||||
key: _pagKey,
|
||||
width: useNativeAssetView ? widget.width : null,
|
||||
height: useNativeAssetView ? widget.height : null,
|
||||
autoPlay: useNativeAssetView,
|
||||
usePlatformView: useNativeAssetView,
|
||||
width: useNativeView ? widget.width : null,
|
||||
height: useNativeView ? widget.height : null,
|
||||
autoPlay: useNativeView,
|
||||
usePlatformView: useNativeView,
|
||||
repeatCount: repeatCount,
|
||||
onInit: useNativeAssetView ? null : _handleInit,
|
||||
onAnimationStart:
|
||||
useNativeAssetView ? null : _handleAnimationStart,
|
||||
onAnimationEnd: useNativeAssetView ? null : _handleAnimationEnd,
|
||||
onAnimationCancel:
|
||||
useNativeAssetView ? null : _handleAnimationCancel,
|
||||
onAnimationRepeat:
|
||||
useNativeAssetView ? null : _handleAnimationRepeat,
|
||||
onInit: useNativeView ? null : _handleInit,
|
||||
onAnimationStart: useNativeView ? null : _handleAnimationStart,
|
||||
onAnimationEnd: useNativeView ? null : _handleAnimationEnd,
|
||||
onAnimationCancel: useNativeView ? null : _handleAnimationCancel,
|
||||
onAnimationRepeat: useNativeView ? null : _handleAnimationRepeat,
|
||||
defaultBuilder: (_) => const SizedBox(width: 1, height: 1),
|
||||
imageReplacementsByName: imageReplacementsByName,
|
||||
)
|
||||
: PAGView.bytes(
|
||||
bytes,
|
||||
key: _pagKey,
|
||||
width: null,
|
||||
height: null,
|
||||
autoPlay: false,
|
||||
width: useNativeView ? widget.width : null,
|
||||
height: useNativeView ? widget.height : null,
|
||||
autoPlay: useNativeView,
|
||||
usePlatformView: useNativeView,
|
||||
repeatCount: repeatCount,
|
||||
onInit: _handleInit,
|
||||
onAnimationStart: _handleAnimationStart,
|
||||
onAnimationEnd: _handleAnimationEnd,
|
||||
onAnimationCancel: _handleAnimationCancel,
|
||||
onAnimationRepeat: _handleAnimationRepeat,
|
||||
onInit: useNativeView ? null : _handleInit,
|
||||
onAnimationStart: useNativeView ? null : _handleAnimationStart,
|
||||
onAnimationEnd: useNativeView ? null : _handleAnimationEnd,
|
||||
onAnimationCancel: useNativeView ? null : _handleAnimationCancel,
|
||||
onAnimationRepeat: useNativeView ? null : _handleAnimationRepeat,
|
||||
defaultBuilder: (_) => const SizedBox(width: 1, height: 1),
|
||||
imageReplacementsByName: imageReplacementsByName,
|
||||
);
|
||||
@ -422,7 +423,7 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
||||
widget.clipBehavior == Clip.none ? Clip.none : Clip.hardEdge,
|
||||
children: [
|
||||
if (visibleUnderlay != null) visibleUnderlay,
|
||||
if (useNativeAssetView)
|
||||
if (useNativeView)
|
||||
Positioned.fill(child: pagView)
|
||||
else
|
||||
_scaledPagView(pagView),
|
||||
@ -475,7 +476,11 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
||||
urlsByName: imageReplacementUrls,
|
||||
),
|
||||
);
|
||||
if (widget.loop) {
|
||||
if (_shouldUseNativeRoomRocketPagView(
|
||||
isIOS: Platform.isIOS,
|
||||
hasAsset: true,
|
||||
hasBytes: false,
|
||||
)) {
|
||||
_startWatchdog?.cancel();
|
||||
_initialized = true;
|
||||
}
|
||||
@ -530,13 +535,25 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
||||
'head=${_pagByteHead(bytes)} '
|
||||
'elapsedMs=${stopwatch.elapsedMilliseconds}',
|
||||
);
|
||||
final useNativeView = _shouldUseNativeRoomRocketPagView(
|
||||
isIOS: Platform.isIOS,
|
||||
hasAsset: false,
|
||||
hasBytes: true,
|
||||
);
|
||||
setState(() {
|
||||
_pagBytes = bytes;
|
||||
_imageReplacementsByName = imageReplacements;
|
||||
_releasedFromMemory = false;
|
||||
if (useNativeView) {
|
||||
_initialized = true;
|
||||
}
|
||||
});
|
||||
_scheduleMemoryRelease(path);
|
||||
_restartWatchdog();
|
||||
if (useNativeView) {
|
||||
_startWatchdog?.cancel();
|
||||
} else {
|
||||
_restartWatchdog();
|
||||
}
|
||||
} catch (error) {
|
||||
if (!_isCurrentResource(path, signature)) {
|
||||
return;
|
||||
@ -907,6 +924,27 @@ String debugRoomRocketPagSourceKind(String resource) {
|
||||
return _pagSourceKind(resource);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
bool debugShouldUseNativeRoomRocketPagView({
|
||||
required bool isIOS,
|
||||
required bool hasAsset,
|
||||
required bool hasBytes,
|
||||
}) {
|
||||
return _shouldUseNativeRoomRocketPagView(
|
||||
isIOS: isIOS,
|
||||
hasAsset: hasAsset,
|
||||
hasBytes: hasBytes,
|
||||
);
|
||||
}
|
||||
|
||||
bool _shouldUseNativeRoomRocketPagView({
|
||||
required bool isIOS,
|
||||
required bool hasAsset,
|
||||
required bool hasBytes,
|
||||
}) {
|
||||
return isIOS && (hasAsset || hasBytes);
|
||||
}
|
||||
|
||||
Future<Uint8List> _readPagBytes(String value) async {
|
||||
final normalized = _normalizePagResource(value);
|
||||
final uri = _pagNetworkUri(normalized);
|
||||
|
||||
@ -1,14 +1,32 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
|
||||
import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart';
|
||||
|
||||
const Duration _roomEmojiGifRestartInterval = Duration(milliseconds: 3500);
|
||||
|
||||
String _roomEmojiCacheKey(String resource, Object token) {
|
||||
return 'room_emoji_${base64UrlEncode(utf8.encode('$resource|$token'))}';
|
||||
}
|
||||
|
||||
int? _resolveRoomEmojiImageCacheDimension(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();
|
||||
}
|
||||
|
||||
class RoomEmojiAssetImage extends StatefulWidget {
|
||||
const RoomEmojiAssetImage({
|
||||
super.key,
|
||||
@ -71,7 +89,7 @@ class _RoomEmojiAssetImageState extends State<RoomEmojiAssetImage>
|
||||
height: widget.height,
|
||||
loop: true,
|
||||
fit: widget.fit,
|
||||
fallback: _buildNetworkFallback(resource, 'svga fallback'),
|
||||
fallback: _buildNetworkFailure(resource, 'svga failed'),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -135,6 +153,9 @@ class _RoomEmojiAssetImageState extends State<RoomEmojiAssetImage>
|
||||
headers: buildNetworkImageHeaders(normalizedUrl),
|
||||
cache: true,
|
||||
clearMemoryCacheWhenDispose: false,
|
||||
clearMemoryCacheIfFailed: true,
|
||||
cacheWidth: _resolveRoomEmojiImageCacheDimension(widget.width),
|
||||
cacheHeight: _resolveRoomEmojiImageCacheDimension(widget.height),
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
fit: widget.fit,
|
||||
@ -166,23 +187,19 @@ class _RoomEmojiAssetImageState extends State<RoomEmojiAssetImage>
|
||||
'failed error=$error',
|
||||
normalizedUrl,
|
||||
);
|
||||
return _buildNetworkFallback(normalizedUrl, 'image failed');
|
||||
return _buildNetworkFailure(normalizedUrl, 'image failed');
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNetworkFallback(String resource, String reason) {
|
||||
Widget _buildNetworkFailure(String resource, String reason) {
|
||||
_logNetworkOnce(
|
||||
_loggedNetworkError,
|
||||
reason,
|
||||
normalizeImageResourceUrl(resource),
|
||||
);
|
||||
return SizedBox(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: const Center(child: SCRotatingDotsLoading()),
|
||||
);
|
||||
return SizedBox(width: widget.width, height: widget.height);
|
||||
}
|
||||
|
||||
void _logNetworkOnce(Set<String> bucket, String message, String url) {
|
||||
@ -291,8 +308,11 @@ class _RestartingGifImageState extends State<_RestartingGifImage> {
|
||||
key: ValueKey('room_emoji_gif_placeholder_${normalizedUrl}_$token'),
|
||||
headers: buildNetworkImageHeaders(normalizedUrl),
|
||||
cache: true,
|
||||
cacheKey: '$normalizedUrl#room_emoji_gif_${token % 2}',
|
||||
cacheKey: _roomEmojiCacheKey(normalizedUrl, token % 2),
|
||||
clearMemoryCacheWhenDispose: false,
|
||||
clearMemoryCacheIfFailed: true,
|
||||
cacheWidth: _resolveRoomEmojiImageCacheDimension(widget.width),
|
||||
cacheHeight: _resolveRoomEmojiImageCacheDimension(widget.height),
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
fit: widget.fit,
|
||||
@ -317,8 +337,11 @@ class _RestartingGifImageState extends State<_RestartingGifImage> {
|
||||
),
|
||||
headers: buildNetworkImageHeaders(normalizedUrl),
|
||||
cache: true,
|
||||
cacheKey: '$normalizedUrl#room_emoji_gif_${_restartToken % 2}',
|
||||
cacheKey: _roomEmojiCacheKey(normalizedUrl, _restartToken % 2),
|
||||
clearMemoryCacheWhenDispose: true,
|
||||
clearMemoryCacheIfFailed: true,
|
||||
cacheWidth: _resolveRoomEmojiImageCacheDimension(widget.width),
|
||||
cacheHeight: _resolveRoomEmojiImageCacheDimension(widget.height),
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
fit: widget.fit,
|
||||
@ -365,11 +388,7 @@ class _RestartingGifImageState extends State<_RestartingGifImage> {
|
||||
case LoadState.failed:
|
||||
final error = state.lastException ?? 'unknown';
|
||||
_debugGifOnce(_loggedError, 'failed error=$error', normalizedUrl);
|
||||
return SizedBox(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: const Center(child: SCRotatingDotsLoading()),
|
||||
);
|
||||
return SizedBox(width: widget.width, height: widget.height);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@ -1781,7 +1781,7 @@ packages:
|
||||
source: hosted
|
||||
version: "4.4.2"
|
||||
webview_flutter_android:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: webview_flutter_android
|
||||
sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558"
|
||||
|
||||
@ -56,10 +56,11 @@ dependencies:
|
||||
#路由
|
||||
fluro: 2.0.5
|
||||
#权限请求
|
||||
permission_handler: ^12.0.0+1
|
||||
webview_flutter: 4.4.2
|
||||
#跳转外链
|
||||
url_launcher: ^6.3.1
|
||||
permission_handler: ^12.0.0+1
|
||||
webview_flutter: 4.4.2
|
||||
webview_flutter_android: 3.16.9
|
||||
#跳转外链
|
||||
url_launcher: ^6.3.1
|
||||
#键值对存储
|
||||
# mmkv: ^2.2.2
|
||||
shared_preferences: ^2.5.3
|
||||
|
||||
32
test/room_game_loading_view_test.dart
Normal file
32
test/room_game_loading_view_test.dart
Normal file
@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:yumi/modules/room_game/views/baishun_loading_view.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('room game loading overlay can be placed inside stack', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
ScreenUtilInit(
|
||||
designSize: const Size(375, 812),
|
||||
builder:
|
||||
(_, __) => const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
ColoredBox(color: Colors.black),
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(child: BaishunLoadingView()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.byType(BaishunLoadingView), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@ -44,6 +44,33 @@ void main() {
|
||||
expect(debugRoomRocketPagSourceKind('assets/rocket/rocket.pag'), 'asset');
|
||||
});
|
||||
|
||||
test('uses native iOS pag view for loaded bytes and assets', () {
|
||||
expect(
|
||||
debugShouldUseNativeRoomRocketPagView(
|
||||
isIOS: true,
|
||||
hasAsset: false,
|
||||
hasBytes: true,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
debugShouldUseNativeRoomRocketPagView(
|
||||
isIOS: true,
|
||||
hasAsset: true,
|
||||
hasBytes: false,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
debugShouldUseNativeRoomRocketPagView(
|
||||
isIOS: false,
|
||||
hasAsset: true,
|
||||
hasBytes: true,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps pag bytes in memory for five minutes by default', () {
|
||||
expect(debugRoomRocketPagMemoryTtl(), const Duration(minutes: 5));
|
||||
});
|
||||
|
||||
1
需求进度.md
1
需求进度.md
@ -1055,3 +1055,4 @@
|
||||
- [x] 2026-06-02 继续优化公屏 GIF 循环闪 loading:网络 GIF 重播时不再销毁整个 widget,加载下一轮期间复用上一帧画面,避免循环间隙出现转圈。
|
||||
- [x] 2026-06-02 复核发现不销毁 key 会导致 GIF 解码器不重启并停在最后一帧;改为前景层用新 key 重播、背景层保留上一轮画面,兼顾循环播放和不显示 loading。
|
||||
- [x] 2026-06-02 按接口文档恢复 `have` 权限判断:`/material/emoji/all` 返回的表情包仅 `have == true` 且非空时展示,`have=false/null` 计入 `filteredNoAccess` 日志。
|
||||
- [x] 2026-06-02 排查 Android 表情包一直转圈:初步判断不是接口解析,表情组已进入渲染后媒体加载失败态被转圈 fallback 掩盖;表情图片/GIF 按项目通用 `netImage` 补解码尺寸、失败清缓存,并把失败 fallback 改为静态占位便于区分 loading 与 failed。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user