From d4ac93adbbd531ca22d701bacea1c63eddd6d86a Mon Sep 17 00:00:00 2001 From: edwinQQQ Date: Thu, 28 Aug 2025 15:16:26 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=A4=BC=E7=89=A9=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E7=9A=84=E5=8A=A8=E7=94=BB=E6=95=88=E6=9E=9C=EF=BC=8C?= =?UTF-8?q?=E5=87=8F=E5=B0=91=E8=A7=86=E8=A7=89=E5=86=B2=E7=AA=81=E3=80=82?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=B6=88=E6=81=AF=E6=8F=92=E5=85=A5=E5=8A=A8?= =?UTF-8?q?=E7=94=BB=E4=B8=BA=E6=B7=A1=E5=85=A5=E6=95=88=E6=9E=9C=EF=BC=8C?= =?UTF-8?q?=E5=BB=B6=E8=BF=9F=E6=BB=9A=E5=8A=A8=E6=89=A7=E8=A1=8C=EF=BC=8C?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E5=8A=A8=E7=94=BB=E6=B5=81=E7=95=85=E6=80=A7?= =?UTF-8?q?=E3=80=82=E5=90=8C=E6=97=B6=E6=96=B0=E5=A2=9E=E7=A4=BC=E7=89=A9?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E8=AF=86=E5=88=AB=E6=96=B9=E6=B3=95=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=20Cell=20=E6=9B=B4=E6=96=B0=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E4=BB=A5=E9=81=BF=E5=85=8D=E5=8A=A8=E7=94=BB=E6=9C=9F?= =?UTF-8?q?=E9=97=B4=E7=9A=84=E5=B8=83=E5=B1=80=E6=9B=B4=E6=96=B0=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../View/XPRoomMessageTableViewCell.m | 62 ++++++++---- .../XPRoomMessageContainerView.m | 67 ++++++++++--- issues/gift-animation-optimization.md | 96 +++++++++++++++++++ 3 files changed, 193 insertions(+), 32 deletions(-) create mode 100644 issues/gift-animation-optimization.md diff --git a/YuMi/Modules/YMRoom/View/MessageContainerView/View/XPRoomMessageTableViewCell.m b/YuMi/Modules/YMRoom/View/MessageContainerView/View/XPRoomMessageTableViewCell.m index da59ed97..9d378e8c 100644 --- a/YuMi/Modules/YMRoom/View/MessageContainerView/View/XPRoomMessageTableViewCell.m +++ b/YuMi/Modules/YMRoom/View/MessageContainerView/View/XPRoomMessageTableViewCell.m @@ -29,6 +29,8 @@ @property (nonatomic,strong) UITapGestureRecognizer *tapEmptyRecognizer; @property (nonatomic, strong) UIView *bottomSpace; +/// 是否正在动画中,用于避免动画期间的布局更新 +@property (nonatomic, assign) BOOL isAnimating; @end @@ -153,26 +155,42 @@ #pragma mark - Getters And Setters - (void)setMessageInfo:(XPMessageInfoModel *)messageInfo { - // 更严格的比较,减少不必要的更新 + // 更严格的比较,避免不必要的更新 if (_messageInfo && [messageInfo.content isEqualToAttributedString:_messageInfo.content] && [messageInfo.bubbleImageUrl isEqualToString:_messageInfo.bubbleImageUrl] && - [messageInfo.boomImageUrl isEqualToString:_messageInfo.boomImageUrl]) { + [messageInfo.boomImageUrl isEqualToString:_messageInfo.boomImageUrl] && + messageInfo.first == _messageInfo.first && + messageInfo.second == _messageInfo.second) { return; } _messageInfo = messageInfo; if (messageInfo) { - // 确保在设置attributedText之前先设置hasBubble属性 - BOOL hasBubble = ![NSString isEmpty:messageInfo.bubbleImageUrl]; - if (self.contentLabel.hasBubble != hasBubble) { - self.contentLabel.hasBubble = hasBubble; + // 如果正在动画中,延迟更新以避免闪烁 + if (self.isAnimating) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self setMessageInfo:messageInfo]; + }); + return; } - self.contentLabel.attributedText = messageInfo.content; + // 批量更新,减少布局触发次数,使用 performWithoutAnimation 避免布局动画 + [UIView performWithoutAnimation:^{ + // 确保在设置attributedText之前先设置hasBubble属性 + BOOL hasBubble = ![NSString isEmpty:messageInfo.bubbleImageUrl]; + if (self.contentLabel.hasBubble != hasBubble) { + self.contentLabel.hasBubble = hasBubble; + } + + self.contentLabel.attributedText = messageInfo.content; + }]; + // 延迟图片加载,避免与礼物动画冲突 if (self.isLeftBigImage && messageInfo.boomImageUrl) { - self.leftBigImageView.imageUrl = messageInfo.boomImageUrl; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + self.leftBigImageView.imageUrl = messageInfo.boomImageUrl; + }); } } } @@ -190,9 +208,13 @@ return; } - [self.contentLabel mas_updateConstraints:^(MASConstraintMaker *make) { - make.width.mas_equalTo(size.width); - }]; + // 使用更平滑的布局更新,减少闪烁 + [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ + [self.contentLabel mas_updateConstraints:^(MASConstraintMaker *make) { + make.width.mas_equalTo(size.width); + }]; + [self layoutIfNeeded]; + } completion:nil]; // [self.bubbleImageView mas_updateConstraints:^(MASConstraintMaker *make) { // if (size.width + 32 >= kRoomMessageMaxWidth) { @@ -206,13 +228,17 @@ if (hasBubble) { @kWeakify(self); - [self.bubbleImageView loadImageWithUrl:self.messageInfo.bubbleImageUrl - completion:^(UIImage * _Nonnull image, NSURL * _Nonnull url) { - @kStrongify(self); - UIImage *image1 = [UIImage imageWithCGImage:image.CGImage scale:2.0 orientation:UIImageOrientationUp]; - UIImage *cutImage = [image1 cropRightAndBottomPixels:2]; - self.bubbleImageView.image = [self resizableImage:cutImage]; - }]; + // 延迟图片加载,避免与动画冲突 + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self.bubbleImageView loadImageWithUrl:self.messageInfo.bubbleImageUrl + completion:^(UIImage * _Nonnull image, NSURL * _Nonnull url) { + @kStrongify(self); + UIImage *image1 = [UIImage imageWithCGImage:image.CGImage scale:2.0 orientation:UIImageOrientationUp]; + UIImage *cutImage = [image1 cropRightAndBottomPixels:2]; + self.bubbleImageView.image = [self resizableImage:cutImage]; + }]; + }); + [self.bubbleImageView mas_updateConstraints:^(MASConstraintMaker *make) { make.edges.mas_equalTo(self.contentLabel).insets(UIEdgeInsetsMake(-10, -10, -10, -10)); }]; diff --git a/YuMi/Modules/YMRoom/View/MessageContainerView/XPRoomMessageContainerView.m b/YuMi/Modules/YMRoom/View/MessageContainerView/XPRoomMessageContainerView.m index 6a51bf07..bfbc0494 100644 --- a/YuMi/Modules/YMRoom/View/MessageContainerView/XPRoomMessageContainerView.m +++ b/YuMi/Modules/YMRoom/View/MessageContainerView/XPRoomMessageContainerView.m @@ -258,7 +258,11 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey"; for (NSInteger i = 0; i < tempNewDatas.count; i++) { [indexPaths addObject:[NSIndexPath indexPathForRow:startIndex + i inSection:0]]; } - [self.messageTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + + // 使用更平滑的动画效果,减少与礼物动画的视觉冲突 + [UIView animateWithDuration:0.2 animations:^{ + [self.messageTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade]; + }]; } else { [self.messageTableView reloadData]; } @@ -386,6 +390,23 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey"; } } +/// 检查是否为礼物消息 +- (BOOL)isGiftMessage:(id)messageData { + if ([messageData isKindOfClass:[NIMMessage class]]) { + NIMMessage *message = (NIMMessage *)messageData; + if ([message.messageObject isKindOfClass:[NIMCustomObject class]]) { + NIMCustomObject *obj = (NIMCustomObject *)message.messageObject; + if (obj.attachment && [obj.attachment isKindOfClass:[AttachmentModel class]]) { + AttachmentModel *attachment = (AttachmentModel *)obj.attachment; + return (attachment.first == CustomMessageType_Gift || + attachment.first == CustomMessageType_AllMicroSend || + attachment.first == CustomMessageType_Super_Gift); + } + } + } + return NO; +} + - (NSInteger)getCurrentDataSourceCount { NSInteger count = 0; switch (self.displayType) { @@ -572,7 +593,15 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey"; self.messageTipsBtn.hidden = NO; [self findAtMeNumber]; } else { - [self appendAndScrollToBottom]; + // 对于礼物消息,使用更平滑的更新策略 + if ([self isGiftMessage:messageData]) { + // 稍微延迟处理礼物消息,让动画先执行 + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self appendAndScrollToBottom]; + }); + } else { + [self appendAndScrollToBottom]; + } } } @@ -623,22 +652,29 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey"; 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]; + // 重新计算 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]]; } + + // 使用更平滑的动画效果,减少与礼物动画的视觉冲突 + [UIView animateWithDuration:0.2 animations:^{ + [self.messageTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade]; + }]; + } else { + [self.messageTableView reloadData]; + } } } //执行插入动画并滚动 - [self scrollToBottom:NO]; + // 延迟滚动执行,避免与礼物动画产生视觉冲突 + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self scrollToBottom:NO]; + }); } ///执行插入动画并滚动 @@ -669,7 +705,10 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey"; [self.messageTableView scrollToRowAtIndexPath:ip atScrollPosition:UITableViewScrollPositionBottom animated:YES]; }); } else { - [self.messageTableView scrollToRowAtIndexPath:ip atScrollPosition:UITableViewScrollPositionBottom animated:NO]; + // 使用更平滑的滚动动画,减少与礼物动画的视觉冲突 + [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ + [self.messageTableView scrollToRowAtIndexPath:ip atScrollPosition:UITableViewScrollPositionBottom animated:NO]; + } completion:nil]; } self.atCount = 0; diff --git a/issues/gift-animation-optimization.md b/issues/gift-animation-optimization.md new file mode 100644 index 00000000..654e7cda --- /dev/null +++ b/issues/gift-animation-optimization.md @@ -0,0 +1,96 @@ +# 礼物动画优化方案B实施总结 + +## 问题描述 + +用户发送礼物时,消息列表会不断闪烁,特别是在连续送礼场景下问题更明显。 + +## 根本原因 + +1. 礼物动画与消息列表更新同时进行,产生视觉冲突 +2. 消息列表使用 `UITableViewRowAnimationNone` 导致突兀的更新效果 +3. 滚动时机与动画执行时机冲突 + +## 实施方案B:优化消息更新动画 + +### 修改内容 + +#### 1. 优化消息插入动画 + +- 将 `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` + +### 关键代码变更 + +```objc +// 1. 消息插入动画优化 +[UIView animateWithDuration:0.2 animations:^{ + [self.messageTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade]; +}]; + +// 2. 滚动时机优化 +dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self scrollToBottom:NO]; +}); + +// 3. 平滑滚动动画 +[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ + [self.messageTableView scrollToRowAtIndexPath:ip atScrollPosition:UITableViewScrollPositionBottom animated:NO]; +} completion:nil]; + +// 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]; + }); +} +``` + +## 预期效果 + +1. 减少礼物动画与消息更新的视觉冲突 +2. 提供更平滑的用户体验 +3. 保持消息的实时性 +4. 适用于连续送礼场景 + +## 风险评估 + +- **低风险**:只修改UI更新逻辑,不影响核心功能 +- **兼容性**:保持现有API不变 +- **性能**:轻微提升,减少视觉冲突 + +## 测试建议 + +1. 测试单个礼物发送 +2. 测试连续快速送礼 +3. 测试不同礼物类型 +4. 测试消息列表滚动状态下的表现