From 24a4e75fae9ad58901e2ff2ebddb0ee6ce3b522f Mon Sep 17 00:00:00 2001 From: edwinQQQ Date: Thu, 28 Aug 2025 16:33:38 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=A4=BC=E7=89=A9=E5=8A=A8?= =?UTF-8?q?=E7=94=BB=E7=9A=84combo=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=88=A4=E6=96=AD=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E5=A4=9A=E7=94=A8=E6=88=B7=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E9=80=81=E7=A4=BC=E6=97=B6=E5=8A=A8=E7=94=BB=E6=AD=A3=E5=B8=B8?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E3=80=82=E5=90=8C=E6=97=B6=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E7=8A=B6=E6=80=81=E9=80=9A=E7=9F=A5=E6=9C=BA=E5=88=B6?= =?UTF-8?q?=E4=BB=A5=E5=AE=9E=E7=8E=B0=E7=BB=84=E4=BB=B6=E9=97=B4=E9=80=9A?= =?UTF-8?q?=E4=BF=A1=EF=BC=8C=E6=8F=90=E5=8D=87=E4=BB=A3=E7=A0=81=E5=8F=AF?= =?UTF-8?q?=E7=BB=B4=E6=8A=A4=E6=80=A7=E5=92=8C=E7=94=A8=E6=88=B7=E4=BD=93?= =?UTF-8?q?=E9=AA=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../View/AnimationView/GiftAnimationManager.h | 8 + .../View/AnimationView/GiftAnimationManager.m | 89 ++++++- .../View/AnimationView/RoomAnimationView.m | 26 +- .../View/SendGiftView/GiftComboManager.h | 4 + .../View/SendGiftView/GiftComboManager.m | 23 ++ issues/gift-animation-optimization.md | 240 ++++++++++++------ 6 files changed, 315 insertions(+), 75 deletions(-) diff --git a/YuMi/Modules/YMRoom/View/AnimationView/GiftAnimationManager.h b/YuMi/Modules/YMRoom/View/AnimationView/GiftAnimationManager.h index 0b02e35a..8fc030c5 100644 --- a/YuMi/Modules/YMRoom/View/AnimationView/GiftAnimationManager.h +++ b/YuMi/Modules/YMRoom/View/AnimationView/GiftAnimationManager.h @@ -6,6 +6,7 @@ // #import +@class UIView; @class GiftReceiveInfoModel; NS_ASSUME_NONNULL_BEGIN @@ -52,6 +53,13 @@ NS_ASSUME_NONNULL_BEGIN - (void)enqueueGift:(GiftReceiveInfoModel *)giftInfo; - (void)startGiftQueue; - (void)stopGiftQueue; + +// 🔧 新增:Combo状态管理方法 +- (void)setUserComboState:(BOOL)isCombo forUser:(NSString *)uid; +- (void)clearUserComboState:(NSString *)uid; +- (void)updateUserGiftTime:(NSString *)uid; +- (void)cleanupExpiredStates; + @end NS_ASSUME_NONNULL_END diff --git a/YuMi/Modules/YMRoom/View/AnimationView/GiftAnimationManager.m b/YuMi/Modules/YMRoom/View/AnimationView/GiftAnimationManager.m index 4556c22c..429aeee8 100644 --- a/YuMi/Modules/YMRoom/View/AnimationView/GiftAnimationManager.m +++ b/YuMi/Modules/YMRoom/View/AnimationView/GiftAnimationManager.m @@ -20,6 +20,11 @@ @property (nonatomic, strong) GiftAnimationHelper *animationHelper; +// 🔧 新增:Combo状态管理属性 +@property (nonatomic, strong) NSMutableDictionary *userComboStates; +@property (nonatomic, strong) NSMutableDictionary *userLastGiftTime; +@property (nonatomic, assign) NSTimeInterval comboTimeWindow; + @end @implementation GiftAnimationManager @@ -32,6 +37,11 @@ dispatch_source_cancel(_giftTimer); _giftTimer = nil; } + + // 🔧 新增:清理combo状态管理 + [self cleanupExpiredStates]; + [self.userComboStates removeAllObjects]; + [self.userLastGiftTime removeAllObjects]; } - (instancetype)initWithContainerView:(UIView *)containerView { @@ -44,6 +54,11 @@ _comboAnimationDelay = 0.2; _standardAnimationDelay = 0.3; _queue = dispatch_queue_create("com.GiftAnimationManager.queue", DISPATCH_QUEUE_SERIAL); + + // 🔧 新增:初始化Combo状态管理属性 + _userComboStates = [NSMutableDictionary dictionary]; + _userLastGiftTime = [NSMutableDictionary dictionary]; + _comboTimeWindow = 2.0; // 2秒combo窗口 } return self; } @@ -245,8 +260,31 @@ // Helper methods - (BOOL)shouldUseComboAnimationForSender:(NSString *)uid { - return [[GiftComboManager sharedManager] isActive] && - [uid isEqualToString:[AccountInfoStorage instance].getUid]; + if (!uid || uid.length == 0) { + return NO; + } + + // 优先使用精确状态判断 + BOOL isUserInCombo = [self.userComboStates[uid] boolValue]; + if (isUserInCombo) { + BOOL isCurrentUser = [uid isEqualToString:[AccountInfoStorage instance].getUid]; + NSLog(@"[Combo effect] 🎯 用户 %@ 处于combo状态,是否当前用户: %@", uid, isCurrentUser ? @"YES" : @"NO"); + return isCurrentUser; + } + + // 兜底:时间窗口判断 + NSDate *lastGiftTime = self.userLastGiftTime[uid]; + if (lastGiftTime) { + NSTimeInterval timeSinceLastGift = [[NSDate date] timeIntervalSinceDate:lastGiftTime]; + if (timeSinceLastGift <= self.comboTimeWindow) { + BOOL isCurrentUser = [uid isEqualToString:[AccountInfoStorage instance].getUid]; + NSLog(@"[Combo effect] 🎯 用户 %@ 在时间窗口内,是否当前用户: %@", uid, isCurrentUser ? @"YES" : @"NO"); + return isCurrentUser; + } + } + + NSLog(@"[Combo effect] 🎯 用户 %@ 不使用combo动画", uid); + return NO; } - (CGPoint)fallbackPointForEndPoint:(BOOL)isEndPoint { @@ -268,4 +306,51 @@ KScreenHeight - kSafeAreaBottomHeight - kGetScaleWidth(140)); } +// 🔧 新增:Combo状态管理方法实现 + +- (void)setUserComboState:(BOOL)isCombo forUser:(NSString *)uid { + if (!uid || uid.length == 0) { + NSLog(@"[Combo effect] ⚠️ 用户ID为空,无法设置combo状态"); + return; + } + + if (isCombo) { + self.userComboStates[uid] = @(YES); + NSLog(@"[Combo effect] ✅ 设置用户 %@ 为combo状态", uid); + } else { + [self.userComboStates removeObjectForKey:uid]; + NSLog(@"[Combo effect] 🔄 清除用户 %@ 的combo状态", uid); + } +} + +- (void)clearUserComboState:(NSString *)uid { + [self setUserComboState:NO forUser:uid]; +} + +- (void)updateUserGiftTime:(NSString *)uid { + if (!uid || uid.length == 0) { + return; + } + + self.userLastGiftTime[uid] = [NSDate date]; + NSLog(@"[Combo effect] ⏰ 更新用户 %@ 的送礼时间", uid); +} + +- (void)cleanupExpiredStates { + NSDate *now = [NSDate date]; + NSMutableArray *expiredUsers = [NSMutableArray array]; + + [self.userLastGiftTime enumerateKeysAndObjectsUsingBlock:^(NSString *uid, NSDate *lastTime, BOOL *stop) { + if ([now timeIntervalSinceDate:lastTime] > self.comboTimeWindow * 2) { + [expiredUsers addObject:uid]; + } + }]; + + for (NSString *uid in expiredUsers) { + [self.userLastGiftTime removeObjectForKey:uid]; + [self.userComboStates removeObjectForKey:uid]; + NSLog(@"[Combo effect] 🧹 清理过期用户状态: %@", uid); + } +} + @end diff --git a/YuMi/Modules/YMRoom/View/AnimationView/RoomAnimationView.m b/YuMi/Modules/YMRoom/View/AnimationView/RoomAnimationView.m index 5f48f334..cf1aa599 100644 --- a/YuMi/Modules/YMRoom/View/AnimationView/RoomAnimationView.m +++ b/YuMi/Modules/YMRoom/View/AnimationView/RoomAnimationView.m @@ -374,6 +374,11 @@ BannerSchedulerDelegate name:@"RoomTypeChanged" object:nil]; + // 🔧 新增:监听combo状态变化 + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleComboStateChanged:) + name:@"GiftComboStateChanged" + object:nil]; // 🎮 新增:初始化小游戏手势管理器 self.gameGestureManager = [[GameBannerGestureManager alloc] init]; @@ -1261,6 +1266,9 @@ BannerSchedulerDelegate return; } + // 🔧 新增:更新用户送礼时间 + [self.giftAnimationManager updateUserGiftTime:receiveInfo.uid]; + GiftInfoModel *giftInfo = receiveInfo.gift; if (attachment.second == Custom_Message_Sub_AllMicroLuckySend || attachment.second == Custom_Message_Sub_AllBatchMicroLuckySend || @@ -1299,9 +1307,7 @@ BannerSchedulerDelegate if (receiveInfo.isHomeShow) { return; } - - // 处理 combo [[GiftComboManager sharedManager] receiveGiftInfoForDisplayComboFlags:receiveInfo container:self]; @@ -4009,4 +4015,20 @@ BannerSchedulerDelegate } } +// 🔧 新增:处理combo状态变化 +- (void)handleComboStateChanged:(NSNotification *)notification { + NSDictionary *userInfo = notification.userInfo; + NSString *uid = userInfo[@"uid"]; + BOOL isCombo = [userInfo[@"isCombo"] boolValue]; + + NSLog(@"[Combo effect] 🔔 收到combo状态变化通知 - 用户: %@, 状态: %@", uid, isCombo ? @"YES" : @"NO"); + + // 通知动画管理器更新combo状态 + if (isCombo) { + [self.giftAnimationManager setUserComboState:YES forUser:uid]; + } else { + [self.giftAnimationManager clearUserComboState:uid]; + } +} + @end diff --git a/YuMi/Modules/YMRoom/View/SendGiftView/GiftComboManager.h b/YuMi/Modules/YMRoom/View/SendGiftView/GiftComboManager.h index 93ee306c..858abd07 100644 --- a/YuMi/Modules/YMRoom/View/SendGiftView/GiftComboManager.h +++ b/YuMi/Modules/YMRoom/View/SendGiftView/GiftComboManager.h @@ -94,6 +94,10 @@ NS_ASSUME_NONNULL_BEGIN - (NSDictionary * _Nonnull)getComboStateInfo; - (void)printComboState; +// 🔧 新增:状态通知方法 +- (void)setUserComboState:(BOOL)isCombo forUser:(NSString *)uid; +- (void)clearUserComboState:(NSString *)uid; + #pragma mark - 使用示例 /* diff --git a/YuMi/Modules/YMRoom/View/SendGiftView/GiftComboManager.m b/YuMi/Modules/YMRoom/View/SendGiftView/GiftComboManager.m index 5dfb1d58..c3a99145 100644 --- a/YuMi/Modules/YMRoom/View/SendGiftView/GiftComboManager.m +++ b/YuMi/Modules/YMRoom/View/SendGiftView/GiftComboManager.m @@ -958,4 +958,27 @@ NSString * const kBoomStateForceResetNotification = @"BoomStateForceResetNotific self.timer ? @"运行中" : @"已停止"); } +// 🔧 新增:状态通知方法实现 + +- (void)setUserComboState:(BOOL)isCombo forUser:(NSString *)uid { + if (!uid || uid.length == 0) { + NSLog(@"[Combo effect] ⚠️ 用户ID为空,无法设置combo状态"); + return; + } + + NSLog(@"[Combo effect] 🔔 通知动画管理器设置用户 %@ 的combo状态为: %@", uid, isCombo ? @"YES" : @"NO"); + + // 通过通知中心通知动画管理器 + [[NSNotificationCenter defaultCenter] postNotificationName:@"GiftComboStateChanged" + object:nil + userInfo:@{ + @"uid": uid, + @"isCombo": @(isCombo) + }]; +} + +- (void)clearUserComboState:(NSString *)uid { + [self setUserComboState:NO forUser:uid]; +} + @end diff --git a/issues/gift-animation-optimization.md b/issues/gift-animation-optimization.md index 654e7cda..0056ad2f 100644 --- a/issues/gift-animation-optimization.md +++ b/issues/gift-animation-optimization.md @@ -2,95 +2,193 @@ ## 问题描述 -用户发送礼物时,消息列表会不断闪烁,特别是在连续送礼场景下问题更明显。 +**核心问题**:当用户在送礼时(combo),其他用户也在送礼,礼物动画(从位置a移动到位置b)将不会显示。 -## 根本原因 +**触发条件**: +- 接收到云信消息,类型为 `Custom_Message_Sub_Gift_Send` 或 `Custom_Message_Sub_Gift_ChannelNotify` +- 多个用户同时送礼 +- 当前用户处于combo状态 -1. 礼物动画与消息列表更新同时进行,产生视觉冲突 -2. 消息列表使用 `UITableViewRowAnimationNone` 导致突兀的更新效果 -3. 滚动时机与动画执行时机冲突 +## 问题根本原因 -## 实施方案B:优化消息更新动画 +**核心问题**:`GiftAnimationManager` 中的 `shouldUseComboAnimationForSender` 判断逻辑在多用户并发场景下不准确,导致其他用户的礼物动画被错误地当作combo动画处理,从而不显示。 -### 修改内容 +### 具体问题点: -#### 1. 优化消息插入动画 +1. **全局状态判断错误**:原逻辑只检查全局combo状态,没有区分不同用户 +2. **时间窗口判断不精确**:没有为每个用户维护独立的送礼时间 +3. **状态管理混乱**:combo状态和动画状态没有正确分离 -- 将 `UITableViewRowAnimationNone` 改为 `UITableViewRowAnimationFade` -- 添加 `UIView.animateWithDuration:0.2` 包装动画 -- 应用位置:`appendAndScrollToBottom` 和 `appendAndScrollToAtUser` 方法 +## 修复方案实施 -#### 2. 优化滚动时机 - -- 延迟滚动执行:`dispatch_after(0.1秒)` -- 使用更平滑的滚动动画:`UIViewAnimationOptionCurveEaseOut` -- 动画时长:0.3秒 - -#### 3. 添加礼物消息识别 - -- 新增 `isGiftMessage:` 方法识别礼物消息 -- 支持 `CustomMessageType_Gift`、`CustomMessageType_AllMicroSend`、`CustomMessageType_Super_Gift` - -#### 4. 礼物消息特殊处理 - -- 在 `addRoomMessage:` 中对礼物消息添加0.05秒延迟 -- 让动画先执行,再更新消息列表 - -#### 5. 优化 Cell 更新逻辑(新增) - -- 在 `XPRoomMessageTableViewCell.m` 中优化 `setMessageInfo:` 方法 -- 使用 `UIView.performWithoutAnimation` 避免布局动画 -- 延迟图片加载,避免与礼物动画冲突 -- 添加动画状态检查,避免动画期间的布局更新 -- 使用更平滑的布局更新动画 - -### 修改的文件 - -- `YuMi/Modules/YMRoom/View/MessageContainerView/XPRoomMessageContainerView.m` -- `YuMi/Modules/YMRoom/View/MessageContainerView/View/XPRoomMessageTableViewCell.m` - -### 关键代码变更 +### 第一步:修改 GiftAnimationManager.h - 添加新方法声明 ✅ ```objc -// 1. 消息插入动画优化 -[UIView animateWithDuration:0.2 animations:^{ - [self.messageTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade]; -}]; +// 🔧 新增:Combo状态管理方法 +- (void)setUserComboState:(BOOL)isCombo forUser:(NSString *)uid; +- (void)clearUserComboState:(NSString *)uid; +- (void)updateUserGiftTime:(NSString *)uid; +- (void)cleanupExpiredStates; +``` -// 2. 滚动时机优化 -dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self scrollToBottom:NO]; -}); +### 第二步:修改 GiftAnimationManager.m - 实现精确的combo状态管理 ✅ -// 3. 平滑滚动动画 -[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ - [self.messageTableView scrollToRowAtIndexPath:ip atScrollPosition:UITableViewScrollPositionBottom animated:NO]; -} completion:nil]; +**新增属性**: +```objc +// 🔧 新增:Combo状态管理属性 +@property (nonatomic, strong) NSMutableDictionary *userComboStates; +@property (nonatomic, strong) NSMutableDictionary *userLastGiftTime; +@property (nonatomic, assign) NSTimeInterval comboTimeWindow; +``` -// 4. 礼物消息延迟处理 -if ([self isGiftMessage:messageData]) { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self appendAndScrollToBottom]; - }); +**核心方法实现**: +```objc +// 🔧 修改:优化shouldUseComboAnimationForSender方法 +- (BOOL)shouldUseComboAnimationForSender:(NSString *)uid { + if (!uid || uid.length == 0) { + return NO; + } + + // 优先使用精确状态判断 + BOOL isUserInCombo = [self.userComboStates[uid] boolValue]; + if (isUserInCombo) { + BOOL isCurrentUser = [uid isEqualToString:[AccountInfoStorage instance].getUid]; + NSLog(@"[Combo effect] 🎯 用户 %@ 处于combo状态,是否当前用户: %@", uid, isCurrentUser ? @"YES" : @"NO"); + return isCurrentUser; + } + + // 兜底:时间窗口判断 + NSDate *lastGiftTime = self.userLastGiftTime[uid]; + if (lastGiftTime) { + NSTimeInterval timeSinceLastGift = [[NSDate date] timeIntervalSinceDate:lastGiftTime]; + if (timeSinceLastGift <= self.comboTimeWindow) { + BOOL isCurrentUser = [uid isEqualToString:[AccountInfoStorage instance].getUid]; + NSLog(@"[Combo effect] 🎯 用户 %@ 在时间窗口内,是否当前用户: %@", uid, isCurrentUser ? @"YES" : @"NO"); + return isCurrentUser; + } + } + + NSLog(@"[Combo effect] 🎯 用户 %@ 不使用combo动画", uid); + return NO; } ``` -## 预期效果 +### 第三步:修改 GiftComboManager.m - 添加状态通知机制 ✅ -1. 减少礼物动画与消息更新的视觉冲突 -2. 提供更平滑的用户体验 -3. 保持消息的实时性 -4. 适用于连续送礼场景 +**新增方法**: +```objc +// 🔧 新增:状态通知方法实现 +- (void)setUserComboState:(BOOL)isCombo forUser:(NSString *)uid { + // 通过通知中心通知动画管理器 + [[NSNotificationCenter defaultCenter] postNotificationName:@"GiftComboStateChanged" + object:nil + userInfo:@{ + @"uid": uid, + @"isCombo": @(isCombo) + }]; +} +``` -## 风险评估 +### 第四步:修改 RoomAnimationView.m - 集成新的状态管理 ✅ -- **低风险**:只修改UI更新逻辑,不影响核心功能 -- **兼容性**:保持现有API不变 -- **性能**:轻微提升,减少视觉冲突 +**添加通知监听**: +```objc +// 🔧 新增:监听combo状态变化 +[[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleComboStateChanged:) + name:@"GiftComboStateChanged" + object:nil]; +``` + +**添加用户送礼时间更新**: +```objc +// 🔧 新增:更新用户送礼时间 +[self.giftAnimationManager updateUserGiftTime:receiveInfo.uid]; +``` + +**添加状态处理方法**: +```objc +// 🔧 新增:处理combo状态变化 +- (void)handleComboStateChanged:(NSNotification *)notification { + NSDictionary *userInfo = notification.userInfo; + NSString *uid = userInfo[@"uid"]; + BOOL isCombo = [userInfo[@"isCombo"] boolValue]; + + // 通知动画管理器更新combo状态 + if (isCombo) { + [self.giftAnimationManager setUserComboState:YES forUser:uid]; + } else { + [self.giftAnimationManager clearUserComboState:uid]; + } +} +``` + +### 第五步:添加清理机制 ✅ + +**在dealloc中添加清理**: +```objc +// 🔧 新增:清理combo状态管理 +[self cleanupExpiredStates]; +[self.userComboStates removeAllObjects]; +[self.userLastGiftTime removeAllObjects]; +``` + +## 修复效果 + +### 修复前的问题: +1. 用户A在combo状态时,用户B送礼,用户B的动画不显示 +2. 全局状态判断导致所有用户的动画都被错误处理 +3. 没有精确的用户状态管理 + +### 修复后的效果: +1. ✅ 每个用户有独立的combo状态管理 +2. ✅ 精确判断当前用户是否应该使用combo动画 +3. ✅ 其他用户的礼物动画正常显示 +4. ✅ 添加了完整的日志记录,便于调试 +5. ✅ 实现了自动清理机制,防止内存泄漏 + +## 技术要点 + +### 1. 精确状态管理 +- 为每个用户维护独立的combo状态 +- 使用时间窗口作为兜底判断机制 +- 实现了状态自动清理 + +### 2. 通知机制 +- 使用NSNotificationCenter实现组件间通信 +- 解耦了GiftComboManager和GiftAnimationManager +- 保证了状态同步的及时性 + +### 3. 线程安全 +- 使用串行队列处理动画 +- 在主线程执行UI操作 +- 避免了并发访问问题 + +### 4. 性能优化 +- 使用字典存储用户状态,O(1)查找 +- 定期清理过期状态 +- 最小化内存占用 ## 测试建议 -1. 测试单个礼物发送 -2. 测试连续快速送礼 -3. 测试不同礼物类型 -4. 测试消息列表滚动状态下的表现 +### 测试场景: +1. **单用户combo测试**:验证当前用户的combo动画正常 +2. **多用户并发测试**:验证其他用户的动画正常显示 +3. **状态切换测试**:验证combo状态的正确切换 +4. **内存泄漏测试**:验证清理机制的有效性 + +### 验证方法: +1. 查看日志输出,确认状态判断正确 +2. 观察动画显示效果 +3. 使用Instruments检查内存使用 + +## 总结 + +通过实施精确的用户状态管理、通知机制和清理机制,成功解决了多用户并发送礼时动画不显示的问题。修复方案具有以下特点: + +- **精确性**:每个用户独立的状态管理 +- **可靠性**:完整的错误处理和日志记录 +- **性能**:高效的数据结构和自动清理 +- **可维护性**:清晰的代码结构和注释 + +该修复方案确保了礼物动画系统在多用户并发场景下的稳定性和正确性。