优化礼物消息的动画效果,减少视觉冲突。修改消息插入动画为淡入效果,延迟滚动执行,确保动画流畅性。同时新增礼物消息识别方法,优化 Cell 更新逻辑以避免动画期间的布局更新。
This commit is contained in:
@@ -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));
|
||||
}];
|
||||
|
@@ -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;
|
||||
|
96
issues/gift-animation-optimization.md
Normal file
96
issues/gift-animation-optimization.md
Normal file
@@ -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. 测试消息列表滚动状态下的表现
|
Reference in New Issue
Block a user