diff --git a/.bevel/do_not_share/port b/.bevel/do_not_share/port index c2d09309..cff32fde 100644 --- a/.bevel/do_not_share/port +++ b/.bevel/do_not_share/port @@ -1 +1 @@ -57817 \ No newline at end of file +56756 \ No newline at end of file diff --git a/YuMi/Modules/YMRoom/Manager/PublicRoomManager.m b/YuMi/Modules/YMRoom/Manager/PublicRoomManager.m index 2847974b..a75f9d1d 100644 --- a/YuMi/Modules/YMRoom/Manager/PublicRoomManager.m +++ b/YuMi/Modules/YMRoom/Manager/PublicRoomManager.m @@ -376,24 +376,27 @@ if (message.session.sessionType == NIMSessionTypeChatroom) { NSString *sessionId = message.session.sessionId; if ([sessionId isEqualToString:self.currentPublicRoomId]) { - NIMMessageChatroomExtension *messageExt = (NIMMessageChatroomExtension *)message.messageExt; + AttachmentModel *attachment; if (message.messageType == NIMMessageTypeCustom) { NIMCustomObject *obj = (NIMCustomObject *) message.messageObject; attachment = (AttachmentModel *) obj.attachment; - if (attachment) { - switch (attachment.first) { - case CustomMessageType_Super_Gift: - [self handleFirst_106:attachment - message:message]; - break; - - default: - break; - } + if (attachment.first > 0 && attachment.second >0) { + [self handleMessageWithAttachmentAndFirstSecond:message]; } +// if (attachment) { +// switch (attachment.first) { +// case CustomMessageType_Super_Gift: +// [self handleFirst_106:attachment +// message:message]; +// break; +// default: +// break; +// } +// } } +// NIMMessageChatroomExtension *messageExt = (NIMMessageChatroomExtension *)message.messageExt; // NSLog(@"PublicRoomManager: 收到公共房间消息: %@\n%@", // message.rawAttachContent, // messageExt.roomExt); @@ -402,8 +405,15 @@ } } +- (void)handleMessageWithAttachmentAndFirstSecond:(NIMMessage *)message { + +} + - (void)handleFirst_106:(AttachmentModel *)attachment message:(NIMMessage *)message { + + // allRoomMsg + // 只有用户在房间时,才会转发 if (![XPSkillCardPlayerManager shareInstance].isInRoom) { NSLog(@"PublicRoomManager: 用户未在房间中,跳过消息转发"); diff --git a/YuMi/Modules/YMRoom/View/AnimationView/RoomAnimationView.m b/YuMi/Modules/YMRoom/View/AnimationView/RoomAnimationView.m index 5875d963..fa5b20fb 100644 --- a/YuMi/Modules/YMRoom/View/AnimationView/RoomAnimationView.m +++ b/YuMi/Modules/YMRoom/View/AnimationView/RoomAnimationView.m @@ -799,6 +799,9 @@ BannerSchedulerDelegate NSLog(@"🔄 BravoGiftBannerView complete 回调被调用"); self.isRoomBannerV2Displaying = NO; [self.bannerScheduler markBannerFinished]; + + // 🔧 新增:确保手势容器状态正确 + [self ensureBannerGestureContainersEnabled]; } exitCurrentRoom:^{ @kStrongify(self); if (!self || !self.superview) { @@ -826,6 +829,9 @@ BannerSchedulerDelegate NSLog(@"🔄 LuckyPackageBannerView complete 回调被调用"); self.isRoomBannerV2Displaying = NO; [self.bannerScheduler markBannerFinished]; + + // 🔧 新增:确保手势容器状态正确 + [self ensureBannerGestureContainersEnabled]; } exitCurrentRoom:^{ @kStrongify(self); [self.hostDelegate exitRoom]; @@ -851,6 +857,9 @@ BannerSchedulerDelegate NSLog(@"🔄 RoomHighValueGiftBannerAnimation complete 回调被调用"); self.isRoomBannerV2Displaying = NO; [self.bannerScheduler markBannerFinished]; + + // 🔧 新增:确保手势容器状态正确 + [self ensureBannerGestureContainersEnabled]; }]; } @@ -866,6 +875,9 @@ BannerSchedulerDelegate @kStrongify(self); self.isRoomBannerV2Displaying = NO; [self.bannerScheduler markBannerFinished]; + + // 🔧 新增:确保手势容器状态正确 + [self ensureBannerGestureContainersEnabled]; }]; } @@ -878,6 +890,9 @@ BannerSchedulerDelegate NSLog(@"🔄 CPGiftBanner complete 回调被调用"); self.isRoomBannerV2Displaying = NO; [self.bannerScheduler markBannerFinished]; + + // 🔧 新增:确保手势容器状态正确 + [self ensureBannerGestureContainersEnabled]; }]; } @@ -889,6 +904,9 @@ BannerSchedulerDelegate @kStrongify(self); self.isRoomBannerV2Displaying = NO; [self.bannerScheduler markBannerFinished]; + + // 🔧 新增:确保手势容器状态正确 + [self ensureBannerGestureContainersEnabled]; }]; } @@ -1052,6 +1070,9 @@ BannerSchedulerDelegate NSLog(@"🔄 LuckyGiftWinningBannerView complete 回调被调用"); self.isRoomBannerV2Displaying = NO; [self.bannerScheduler markBannerFinished]; + + // 🔧 新增:确保手势容器状态正确 + [self ensureBannerGestureContainersEnabled]; } exitCurrentRoom:^{ @kStrongify(self); [self.hostDelegate exitRoom]; @@ -1091,6 +1112,9 @@ BannerSchedulerDelegate NSLog(@"🔄 GameUniversalBannerView complete 回调被调用"); self.isRoomBannerV2Displaying = NO; [self.bannerScheduler markBannerFinished]; + + // 🔧 新增:确保手势容器状态正确 + [self ensureBannerGestureContainersEnabled]; } goToGame:^(NSInteger gameID) { @kStrongify(self); NSArray *baishunList = [self.hostDelegate getPlayList]; @@ -2426,7 +2450,28 @@ BannerSchedulerDelegate - (void)handleBannerTap:(UITapGestureRecognizer *)tapGesture { CGPoint tapPoint = [tapGesture locationInView:self.bannerContainer]; - // 检查当前显示的 banner 是否在 tap 位置可以响应事件 + // 🔧 新增:检查是否有可见的 banner + BOOL hasVisibleBanner = NO; + for (UIView *subview in self.bannerContainer.subviews) { + if (!subview.hidden && subview.alpha > 0.01) { + hasVisibleBanner = YES; + break; + } + } + + // 如果没有可见的 banner,直接转发点击事件到下层 + if (!hasVisibleBanner) { + NSLog(@"🎯 没有可见 banner,直接转发点击事件到下层"); + self.savedTapPoint = tapPoint; + self.hasSavedTapPoint = YES; + CGPoint screenPoint = [self.bannerContainer convertPoint:tapPoint toView:nil]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"BannerTapToFunctionContainer" + object:nil + userInfo:@{@"point": [NSValue valueWithCGPoint:screenPoint]}]; + return; + } + + // 有可见 banner 时的原有逻辑 if ([self isPointInBannerInteractiveArea:tapPoint]) { // banner 可以响应,不处理,让 banner 继续原有逻辑 NSLog(@"🎯 Banner tap 位置在可交互区域,banner 将处理此事件"); @@ -2440,7 +2485,6 @@ BannerSchedulerDelegate NSLog(@"💾 Banner tap 位置不在可交互区域,已保存位置: %@", NSStringFromCGPoint(tapPoint)); // 将 bannerContainer 中的点转换为屏幕坐标系 CGPoint screenPoint = [self.bannerContainer convertPoint:tapPoint toView:nil]; -// UIView *tappedView = tapGesture.view; [[NSNotificationCenter defaultCenter] postNotificationName:@"BannerTapToFunctionContainer" object:nil userInfo:@{@"point": [NSValue valueWithCGPoint:screenPoint]}]; @@ -3786,6 +3830,9 @@ BannerSchedulerDelegate - (void)bannerSchedulerDidFinishPlaying:(BannerScheduler *)scheduler { // Banner 播放完成,可以在这里进行清理工作 NSLog(@"🔄 BannerScheduler: Banner 播放完成"); + + // 🔧 新增:确保手势容器状态正确 + [self ensureBannerGestureContainersEnabled]; } - (void)bannerScheduler:(BannerScheduler *)scheduler didStartPlayingBanner:(id)banner { @@ -3904,6 +3951,29 @@ BannerSchedulerDelegate NSLog(@"🎯 RoomAnimationView: Banner 手势容器已恢复显示(非小游戏模式)"); } +// 🔧 新增:确保手势容器状态正确 +- (void)ensureBannerGestureContainersEnabled { + // 确保手势容器可见且可交互 + if (self.bannerSwipeGestureContainer.hidden || + self.bannerLeftTapGestureContainer.hidden || + self.bannerRightTapGestureContainer.hidden) { + + NSLog(@"🔧 检测到手势容器被隐藏,重新激活"); + [self restoreBannerGestureNormalMode]; + } + + // 确保用户交互启用 + if (!self.bannerSwipeGestureContainer.userInteractionEnabled || + !self.bannerLeftTapGestureContainer.userInteractionEnabled || + !self.bannerRightTapGestureContainer.userInteractionEnabled) { + + NSLog(@"🔧 检测到手势容器用户交互被禁用,重新启用"); + self.bannerSwipeGestureContainer.userInteractionEnabled = YES; + self.bannerLeftTapGestureContainer.userInteractionEnabled = YES; + self.bannerRightTapGestureContainer.userInteractionEnabled = YES; + } +} + @end diff --git a/YuMi/Modules/YMRoom/View/MessageContainerView/XPRoomMessageContainerView.m b/YuMi/Modules/YMRoom/View/MessageContainerView/XPRoomMessageContainerView.m index d6763b27..6a51bf07 100644 --- a/YuMi/Modules/YMRoom/View/MessageContainerView/XPRoomMessageContainerView.m +++ b/YuMi/Modules/YMRoom/View/MessageContainerView/XPRoomMessageContainerView.m @@ -133,8 +133,10 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey"; /// 更新其他 tag 的数据源,若传入空数组,则初始化并从 datasource 中获取数据 - (void)updateAllDataSource:(NSArray *)datas { if (!datas || datas.count == 0) { - self.datasource_chat = @[].mutableCopy; - self.datasource_gift = @[].mutableCopy; + // 清空分类数据源 + [self.datasource_chat removeAllObjects]; + [self.datasource_gift removeAllObjects]; + // 从主数据源重新构建分类数据源 datas = self.datasource; } @@ -152,6 +154,8 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey"; case CustomMessageType_Treasure_Fairy: [self.datasource_gift addObject:model]; break; + default: + break; } } } @@ -178,8 +182,6 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey"; - (void)appendAndScrollToAtUser { // 1. 检查 incomingMessages 是否为空 if (self.incomingMessages.count < 1) { - NSInteger rows = self.datasource.count; - // 2. 安全检查 locationArray 是否为空 if (self.locationArray.count == 0) { [self scrollToBottomWithTipsHidden:YES]; @@ -193,15 +195,22 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey"; return; } - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0]; - if (rows > indexPath.row) { - [self.messageTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES]; - if (rows == indexPath.row + 1) { - self.messageTipsBtn.hidden = YES; - self.isPending = NO; - } - } else { + // 将 datasource 的索引转换为当前显示数据源的索引 + NSInteger convertedIndex = [self convertDataSourceIndexToCurrentDisplayIndex:index]; + if (convertedIndex == NSNotFound) { [self scrollToBottomWithTipsHidden:YES]; + } else { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:convertedIndex inSection:0]; + NSInteger currentRows = [self getCurrentDataSourceCount]; + if (currentRows > indexPath.row) { + [self.messageTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES]; + if (currentRows == indexPath.row + 1) { + self.messageTipsBtn.hidden = YES; + self.isPending = NO; + } + } else { + [self scrollToBottomWithTipsHidden:YES]; + } } [self safelyRemoveLocationAtIndex:0]; @@ -217,8 +226,10 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey"; needReloadData = YES; // 标记需要重新加载数据 } - // 5. 插入新消息 - NSMutableArray *indexPaths = @[].mutableCopy; + // 5. 在更新数据源之前获取当前行数 + NSInteger currentRows = [self getCurrentDataSourceCount]; + + // 6. 插入新消息 NSMutableArray *tempNewDatas = @[].mutableCopy; for (id item in self.incomingMessages) { XPMessageInfoModel *model = [self parseMessage:item]; @@ -226,18 +237,32 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey"; [tempNewDatas addObject:model]; [self.datasource addObject:model]; - [indexPaths addObject:[NSIndexPath indexPathForRow:self.datasource.count - 1 inSection:0]]; - [self processAtMentionsForMessage:item]; } [self updateAllDataSource:tempNewDatas]; [self.incomingMessages removeAllObjects]; - // 如果有删除操作,使用 reloadData;否则使用增量更新 + // 7. 更新 UITableView if (needReloadData) { [self.messageTableView reloadData]; - } else if (indexPaths.count > 0) { - [self.messageTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + } else if (tempNewDatas.count > 0) { + // 安全检查:确保数据源一致性 + NSInteger expectedRows = [self getCurrentDataSourceCount]; + if (expectedRows != [self.messageTableView numberOfRowsInSection:0]) { + [self.messageTableView reloadData]; + } else { + // 重新计算 indexPath,使用更新前的行数作为起始索引 + NSMutableArray *indexPaths = @[].mutableCopy; + NSInteger startIndex = currentRows; + if (startIndex >= 0 && startIndex <= [self.messageTableView numberOfRowsInSection:0]) { + for (NSInteger i = 0; i < tempNewDatas.count; i++) { + [indexPaths addObject:[NSIndexPath indexPathForRow:startIndex + i inSection:0]]; + } + [self.messageTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + } else { + [self.messageTableView reloadData]; + } + } } // 6. 滚动到指定位置或底部 @@ -245,9 +270,11 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey"; } - (void)scrollToBottomWithTipsHidden:(BOOL)hidden { - NSInteger rows = self.datasource.count; - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:(rows - 1) inSection:0]; - [self.messageTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES]; + NSInteger rows = [self getCurrentDataSourceCount]; + if (rows > 0) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:(rows - 1) inSection:0]; + [self.messageTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES]; + } self.messageTipsBtn.hidden = hidden; self.isPending = NO; self.atCount = 0; @@ -296,9 +323,32 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey"; - (void)safelyRemoveMessages:(NSInteger)count { if (self.datasource.count >= count) { + // 获取要删除的消息 NSIndexSet *set = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, count)]; + NSArray *removedMessages = [self.datasource objectsAtIndexes:set]; + + // 从主数据源删除 [self.datasource removeObjectsAtIndexes:set]; - [self updateAllDataSource:nil]; + + // 从分类数据源中删除对应的消息 + for (XPMessageInfoModel *removedModel in removedMessages) { + switch (removedModel.first) { + case NIMMessageTypeText: + case CustomMessageType_Face: + [self.datasource_chat removeObject:removedModel]; + break; + case CustomMessageType_Gift: + case CustomMessageType_RoomBoom: + case CustomMessageType_Candy_Tree: + case CustomMessageType_Super_Gift: + case CustomMessageType_AllMicroSend: + case CustomMessageType_Treasure_Fairy: + [self.datasource_gift removeObject:removedModel]; + break; + default: + break; + } + } // 更新 locationArray NSMutableIndexSet *indexSet = [NSMutableIndexSet indexSet]; @@ -336,18 +386,81 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey"; } } +- (NSInteger)getCurrentDataSourceCount { + NSInteger count = 0; + switch (self.displayType) { + case 1: + count = self.datasource.count; + break; + case 2: + count = self.datasource_chat.count; + break; + case 3: + count = self.datasource_gift.count; + break; + default: + count = self.datasource.count; + break; + } + // 确保返回非负数 + return MAX(0, count); +} + +- (NSInteger)convertDataSourceIndexToCurrentDisplayIndex:(NSInteger)dataSourceIndex { + if (self.displayType == 1) { + return dataSourceIndex; + } + + if (dataSourceIndex >= self.datasource.count) { + return NSNotFound; + } + + XPMessageInfoModel *targetModel = [self.datasource objectAtIndex:dataSourceIndex]; + if (!targetModel) { + return NSNotFound; + } + + NSArray *currentDataSource = nil; + switch (self.displayType) { + case 2: + currentDataSource = self.datasource_chat; + break; + case 3: + currentDataSource = self.datasource_gift; + break; + default: + return dataSourceIndex; + } + + // 在当前数据源中查找对应的模型 + for (NSInteger i = 0; i < currentDataSource.count; i++) { + XPMessageInfoModel *model = [currentDataSource objectAtIndex:i]; + if ([model isEqual:targetModel]) { + return i; + } + } + + return NSNotFound; +} + - (void)scrollToFirstLocationOrBottom { - NSInteger rows = self.datasource.count; if (self.locationArray.count == 0) { [self scrollToBottomWithTipsHidden:YES]; return; } NSInteger index = [self safeGetIndexFromLocationArrayAt:0]; - if (index == NSNotFound || index >= rows) { + if (index == NSNotFound) { + [self scrollToBottomWithTipsHidden:YES]; + return; + } + + // 将 datasource 的索引转换为当前显示数据源的索引 + NSInteger convertedIndex = [self convertDataSourceIndexToCurrentDisplayIndex:index]; + if (convertedIndex == NSNotFound) { [self scrollToBottomWithTipsHidden:YES]; } else { - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0]; + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:convertedIndex inSection:0]; [self.messageTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES]; [self safelyRemoveLocationAtIndex:0]; } @@ -473,14 +586,15 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey"; BOOL needReloadData = NO; if (self.datasource.count > kRoomMessageMaxLength) { - NSIndexSet *set = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, kRoomMessageMaxLength/2)]; - NSArray *needRemoveMsgArray = [self.datasource objectsAtIndexes:set]; - [self.datasource removeObjectsInArray:needRemoveMsgArray]; + NSInteger removedCount = kRoomMessageMaxLength / 2; + [self safelyRemoveMessages:removedCount]; needReloadData = YES; // 标记需要重新加载数据 } + // 在更新数据源之前获取当前行数 + NSInteger currentRows = [self getCurrentDataSourceCount]; + NSMutableArray *tempArray = @[].mutableCopy; - NSMutableArray *indexPaths = @[].mutableCopy; @kWeakify(self); [self.incomingMessages enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { @kStrongify(self); @@ -493,7 +607,6 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey"; if (model) { [tempArray addObject:model]; [self.datasource addObject:model]; - [indexPaths addObject:[NSIndexPath indexPathForRow:self.datasource.count - 1 inSection:0]]; } }]; @@ -504,8 +617,24 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey"; // 如果有删除操作,使用 reloadData;否则使用增量更新 if (needReloadData) { [self.messageTableView reloadData]; - } else if (indexPaths.count > 0) { - [self.messageTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + } else if (tempArray.count > 0) { + // 安全检查:确保数据源一致性 + NSInteger expectedRows = [self getCurrentDataSourceCount]; + if (expectedRows != [self.messageTableView numberOfRowsInSection:0]) { + [self.messageTableView reloadData]; + } else { + // 重新计算 indexPath,使用更新前的行数作为起始索引 + NSMutableArray *indexPaths = @[].mutableCopy; + NSInteger startIndex = currentRows; + if (startIndex >= 0 && startIndex <= [self.messageTableView numberOfRowsInSection:0]) { + for (NSInteger i = 0; i < tempArray.count; i++) { + [indexPaths addObject:[NSIndexPath indexPathForRow:startIndex + i inSection:0]]; + } + [self.messageTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + } else { + [self.messageTableView reloadData]; + } + } } //执行插入动画并滚动 @@ -1340,21 +1469,23 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey"; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + NSInteger count = 0; switch (self.displayType) { case 1: - return self.datasource.count; + count = self.datasource.count; break; case 2: - return self.datasource_chat.count; + count = self.datasource_chat.count; break; case 3: - return self.datasource_gift.count; + count = self.datasource_gift.count; break; - default: - return self.datasource.count; + count = self.datasource.count; break; } + // 确保返回非负数 + return MAX(0, count); } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { diff --git a/YuMi/Modules/YMRoom/View/SendGiftView/View/PIGiftBravoGiftBroadcastView.m b/YuMi/Modules/YMRoom/View/SendGiftView/View/PIGiftBravoGiftBroadcastView.m index 7b3a97bb..c8849cbb 100644 --- a/YuMi/Modules/YMRoom/View/SendGiftView/View/PIGiftBravoGiftBroadcastView.m +++ b/YuMi/Modules/YMRoom/View/SendGiftView/View/PIGiftBravoGiftBroadcastView.m @@ -161,6 +161,13 @@ self.isAnimating = NO; self.shouldStopAnimation = NO; + // 检查数据源 + if (self.source.count < 2) { + NSLog(@"⚠️ PIGiftBravoGiftBroadcastView: 数据源不足 (%lu个),动画可能过快", (unsigned long)self.source.count); + } else { + NSLog(@"✅ PIGiftBravoGiftBroadcastView: 数据源正常 (%lu个)", (unsigned long)self.source.count); + } + @kWeakify(self); [self.source enumerateObjectsUsingBlock:^(BravoGiftTabInfomationModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { @kStrongify(self); @@ -176,10 +183,13 @@ - (void)startLoop { if (self.isAnimating || self.labels.count == 0 || self.shouldStopAnimation) { + NSLog(@"🚫 PIGiftBravoGiftBroadcastView: 动画启动被阻止 - isAnimating:%d, labelsCount:%lu, shouldStop:%d", + self.isAnimating, (unsigned long)self.labels.count, self.shouldStopAnimation); return; } self.isAnimating = YES; + NSLog(@"🎬 PIGiftBravoGiftBroadcastView: 开始动画循环"); [self playCurrentAnimation]; } @@ -187,21 +197,33 @@ - (void)startAnimation { if (self.shouldStopAnimation) { self.shouldStopAnimation = NO; + NSLog(@"🔄 PIGiftBravoGiftBroadcastView: 重置停止标志,重新开始动画"); } if (self.labels.count > 0) { [self startLoop]; + } else { + NSLog(@"⚠️ PIGiftBravoGiftBroadcastView: 无法开始动画,labels为空"); } } // 公共方法:停止动画循环 - (void)stopAnimation { + NSLog(@"⏹️ PIGiftBravoGiftBroadcastView: 停止动画"); [self endloop]; } - (void)playCurrentAnimation { + // 更严格的状态检查 if (self.shouldStopAnimation) { self.isAnimating = NO; + NSLog(@"🚫 PIGiftBravoGiftBroadcastView: 动画被停止标志阻止"); + return; + } + + // 确保只有一个动画在运行 + if (self.container.subviews.count > 0) { + NSLog(@"⚠️ PIGiftBravoGiftBroadcastView: 容器中已有视图,跳过当前动画"); return; } @@ -209,9 +231,12 @@ PIGiftBravoGiftBroadcastItemView *item = [self.labels xpSafeObjectAtIndex:self.index]; if (!item) { self.isAnimating = NO; + NSLog(@"❌ PIGiftBravoGiftBroadcastView: 无法获取当前索引(%ld)的视图", (long)self.index); return; } + NSLog(@"🎭 PIGiftBravoGiftBroadcastView: 播放第%ld个动画项", (long)self.index); + // 设置初始位置(屏幕右侧) item.frame = CGRectMake(KScreenWidth, 2, KScreenWidth - 25 - 42, 26); // 添加到容器视图 @@ -226,11 +251,12 @@ if (!finished || self.shouldStopAnimation) { [item removeFromSuperview]; self.isAnimating = NO; + NSLog(@"❌ PIGiftBravoGiftBroadcastView: 入场动画被中断"); return; } - // 停留时间:2秒延迟后执行出场动画 - [UIView animateWithDuration:0.5 delay:2.0 options:UIViewAnimationOptionCurveEaseIn animations:^{ + // 停留时间:从2秒增加到3秒 + [UIView animateWithDuration:0.5 delay:3.0 options:UIViewAnimationOptionCurveEaseIn animations:^{ item.frame = CGRectMake(-KScreenWidth, 2, KScreenWidth - 25 - 42, 26); } completion:^(BOOL finished) { @kStrongify(self); @@ -239,6 +265,7 @@ if (self.shouldStopAnimation) { self.isAnimating = NO; + NSLog(@"⏹️ PIGiftBravoGiftBroadcastView: 出场动画被停止"); return; } @@ -246,10 +273,12 @@ self.index += 1; if (self.index >= self.labels.count) { self.index = 0; + NSLog(@"🔄 PIGiftBravoGiftBroadcastView: 动画循环重置到开始"); } - // 使用定时器延迟下一个动画,避免递归调用 - self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(scheduleNextAnimation) userInfo:nil repeats:NO]; + // 使用定时器延迟下一个动画,从0.1秒增加到0.5秒 + self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(scheduleNextAnimation) userInfo:nil repeats:NO]; + NSLog(@"⏰ PIGiftBravoGiftBroadcastView: 安排下一个动画,延迟0.5秒"); }]; }]; } @@ -260,9 +289,11 @@ if (self.shouldStopAnimation) { self.isAnimating = NO; + NSLog(@"🚫 PIGiftBravoGiftBroadcastView: 定时器回调被停止标志阻止"); return; } + NSLog(@"▶️ PIGiftBravoGiftBroadcastView: 定时器触发下一个动画"); [self playCurrentAnimation]; } @@ -271,6 +302,8 @@ self.shouldStopAnimation = YES; self.isAnimating = NO; + NSLog(@"🛑 PIGiftBravoGiftBroadcastView: 结束动画循环"); + // 停止并清理定时器 if (self.animationTimer) { [self.animationTimer invalidate]; diff --git a/YuMi/Modules/YMRoom/View/XPRoomViewController.m b/YuMi/Modules/YMRoom/View/XPRoomViewController.m index 62fc5e37..2f7adfef 100644 --- a/YuMi/Modules/YMRoom/View/XPRoomViewController.m +++ b/YuMi/Modules/YMRoom/View/XPRoomViewController.m @@ -195,6 +195,9 @@ XPCandyTreeInsufficientBalanceViewDelegate> /// 上麦请求弹窗定时器,用于10秒后自动移除弹窗 @property(nonatomic,strong) NSTimer *upMicAskTimer; +/// 🔧 修复:保存 block 形式的通知观察者,防止内存泄漏 +@property(nonatomic,strong) id exchangeRoomAnimationViewObserver; + @end @implementation XPRoomViewController @@ -310,6 +313,12 @@ XPCandyTreeInsufficientBalanceViewDelegate> [[RoomBoomManager sharedManager] removeEventListenerForTarget:self]; + // 🔧 修复:移除 block 形式的通知观察者 + if (self.exchangeRoomAnimationViewObserver) { + [[NSNotificationCenter defaultCenter] removeObserver:self.exchangeRoomAnimationViewObserver]; + self.exchangeRoomAnimationViewObserver = nil; + } + [[NSNotificationCenter defaultCenter] removeObserver:self]; // 🔧 修复:清理 RoomAnimationView @@ -451,7 +460,8 @@ XPCandyTreeInsufficientBalanceViewDelegate> object:nil]; @kWeakify(self); - [[NSNotificationCenter defaultCenter] addObserverForName:@"kExchangeRoomAnimationViewAndGameViewIndex" + // 🔧 修复:保存 block 观察者的返回值,防止内存泄漏 + self.exchangeRoomAnimationViewObserver = [[NSNotificationCenter defaultCenter] addObserverForName:@"kExchangeRoomAnimationViewAndGameViewIndex" object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification * _Nonnull notification) { @@ -1929,6 +1939,11 @@ XPCandyTreeInsufficientBalanceViewDelegate> NSLog(@"[Recv] --- Message Raw Attach Content: %@, %@, %ld", @(message.senderClientType), message.rawAttachContent, (long)message.messageType); + if ([message.rawAttachContent containsString:@"\"allRoomMsg\":1"]) { + NSLog(@"[Recv] --- 拦截旧的全房间消息"); + continue; + } + if (message.messageType == NIMMessageTypeNotification) { [self handleNIMNotificationTypeMessage:message]; } else if (message.messageType == NIMMessageTypeCustom) { diff --git a/docs/PIGiftBravoGiftBroadcastView_Animation_Fix.md b/docs/PIGiftBravoGiftBroadcastView_Animation_Fix.md new file mode 100644 index 00000000..ac61e34d --- /dev/null +++ b/docs/PIGiftBravoGiftBroadcastView_Animation_Fix.md @@ -0,0 +1,130 @@ +# PIGiftBravoGiftBroadcastView 动画速度修复 + +## 问题描述 +`PIGiftBravoGiftBroadcastView` 中内容切换的动画变得非常快,影响用户体验。 + +## 问题分析 + +### 1. 动画时序问题 +- **原有时序**: + - 入场动画:0.5秒 + - 停留时间:2.0秒延迟 + - 出场动画:0.5秒 + - 下一个动画间隔:0.1秒 +- **问题**:间隔时间过短,导致动画切换过快 + +### 2. 状态管理问题 +- 缺少严格的状态检查 +- 可能存在多个动画同时运行的情况 +- 状态同步不够完善 + +### 3. 数据源问题 +- 没有对数据源进行验证 +- 数据源不足时可能导致动画异常 + +## 解决方案 + +### 1. 调整动画时序 +- **停留时间**:从2秒增加到3秒 +- **间隔时间**:从0.1秒增加到0.5秒 +- **总周期**:从3.1秒增加到4.5秒 + +### 2. 优化状态管理 +- 添加更严格的状态检查 +- 确保只有一个动画在运行 +- 防止容器中同时存在多个视图 + +### 3. 添加数据源检查 +- 在setupUI中验证数据源数量 +- 添加详细的日志输出便于调试 + +## 修改内容 + +### 文件:`PIGiftBravoGiftBroadcastView.m` + +#### 1. setupUI方法 +```objc +// 检查数据源 +if (self.source.count < 2) { + NSLog(@"⚠️ PIGiftBravoGiftBroadcastView: 数据源不足 (%lu个),动画可能过快", (unsigned long)self.source.count); +} else { + NSLog(@"✅ PIGiftBravoGiftBroadcastView: 数据源正常 (%lu个)", (unsigned long)self.source.count); +} +``` + +#### 2. playCurrentAnimation方法 +```objc +// 更严格的状态检查 +if (self.shouldStopAnimation) { + self.isAnimating = NO; + NSLog(@"🚫 PIGiftBravoGiftBroadcastView: 动画被停止标志阻止"); + return; +} + +// 确保只有一个动画在运行 +if (self.container.subviews.count > 0) { + NSLog(@"⚠️ PIGiftBravoGiftBroadcastView: 容器中已有视图,跳过当前动画"); + return; +} + +// 停留时间:从2秒增加到3秒 +[UIView animateWithDuration:0.5 delay:3.0 options:UIViewAnimationOptionCurveEaseIn animations:^{ + item.frame = CGRectMake(-KScreenWidth, 2, KScreenWidth - 25 - 42, 26); +} completion:^(BOOL finished) { + // 使用定时器延迟下一个动画,从0.1秒增加到0.5秒 + self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(scheduleNextAnimation) userInfo:nil repeats:NO]; + NSLog(@"⏰ PIGiftBravoGiftBroadcastView: 安排下一个动画,延迟0.5秒"); +}]; +``` + +#### 3. 添加详细日志 +- 动画启动/停止状态 +- 数据源验证结果 +- 动画播放进度 +- 状态变化追踪 + +## 预期效果 + +### 1. 动画速度改善 +- 动画切换更加平滑自然 +- 用户有足够时间阅读内容 +- 减少视觉疲劳 + +### 2. 稳定性提升 +- 防止多个动画同时运行 +- 更好的状态管理 +- 减少异常情况 + +### 3. 调试便利性 +- 详细的日志输出 +- 便于问题定位 +- 性能监控 + +## 测试建议 + +### 1. 功能测试 +- 验证动画时序是否正确 +- 检查状态管理是否正常 +- 确认数据源检查是否有效 + +### 2. 性能测试 +- 监控内存使用情况 +- 检查CPU占用率 +- 验证动画流畅度 + +### 3. 边界测试 +- 数据源为空的情况 +- 快速切换场景的情况 +- 内存压力下的表现 + +## 注意事项 + +1. **向后兼容**:修改保持了原有的API接口不变 +2. **性能影响**:增加了日志输出,在Release版本中可以考虑移除 +3. **配置灵活**:动画时间可以通过常量定义,便于后续调整 + +## 修改时间 +2025年1月27日 + +## 修改人员 +AI Assistant