修改bug
This commit is contained in:
@@ -73,6 +73,7 @@
|
|||||||
self.messageLabel.numberOfLines = 0;
|
self.messageLabel.numberOfLines = 0;
|
||||||
self.messageLabel.font = [UIFont systemFontOfSize:16];
|
self.messageLabel.font = [UIFont systemFontOfSize:16];
|
||||||
self.messageLabel.textColor = [UIColor whiteColor];
|
self.messageLabel.textColor = [UIColor whiteColor];
|
||||||
|
self.messageLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||||
// 设置 preferredMaxLayoutWidth 让 AutoLayout 能正确计算多行高度
|
// 设置 preferredMaxLayoutWidth 让 AutoLayout 能正确计算多行高度
|
||||||
CGFloat maxWidth = [UIScreen mainScreen].bounds.size.width * 0.75 - 16 - 24;
|
CGFloat maxWidth = [UIScreen mainScreen].bounds.size.width * 0.75 - 16 - 24;
|
||||||
self.messageLabel.preferredMaxLayoutWidth = maxWidth;
|
self.messageLabel.preferredMaxLayoutWidth = maxWidth;
|
||||||
@@ -94,20 +95,22 @@
|
|||||||
make.center.equalTo(self.voiceButton);
|
make.center.equalTo(self.voiceButton);
|
||||||
}];
|
}];
|
||||||
|
|
||||||
// bubbleView 约束
|
// 关键修复:bubbleView 必须有明确的高度约束链
|
||||||
[self.bubbleView mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.bubbleView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.top.equalTo(self.voiceButton.mas_bottom).offset(4);
|
make.top.equalTo(self.voiceButton.mas_bottom).offset(4);
|
||||||
make.bottom.equalTo(self.contentView).offset(-4);
|
make.bottom.equalTo(self.contentView).offset(-4).priority(999); // 降低优先级避免冲突
|
||||||
make.left.equalTo(self.contentView).offset(16);
|
make.left.equalTo(self.contentView).offset(16);
|
||||||
make.width.lessThanOrEqualTo(self.contentView).multipliedBy(0.75);
|
make.width.lessThanOrEqualTo(self.contentView).multipliedBy(0.75);
|
||||||
}];
|
}];
|
||||||
|
|
||||||
// messageLabel 约束
|
// 关键修复:messageLabel 约束必须完整,让 AutoLayout 能推导出 bubbleView 的高度
|
||||||
[self.messageLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.messageLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.top.equalTo(self.bubbleView).offset(10);
|
make.top.equalTo(self.bubbleView).offset(10);
|
||||||
make.bottom.equalTo(self.bubbleView).offset(-10);
|
make.bottom.equalTo(self.bubbleView).offset(-10).priority(999); // 降低优先级
|
||||||
make.left.equalTo(self.bubbleView).offset(12);
|
make.left.equalTo(self.bubbleView).offset(12);
|
||||||
make.right.equalTo(self.bubbleView).offset(-12);
|
make.right.equalTo(self.bubbleView).offset(-12);
|
||||||
|
// 关键修复:给 messageLabel 一个最小高度,防止高度为 0
|
||||||
|
make.height.greaterThanOrEqualTo(@20);
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,10 +130,18 @@
|
|||||||
NSLog(@"[KBChatAssistantMessageCell] 启动新的打字机效果");
|
NSLog(@"[KBChatAssistantMessageCell] 启动新的打字机效果");
|
||||||
[self startTypewriterEffectWithText:message.text];
|
[self startTypewriterEffectWithText:message.text];
|
||||||
} else {
|
} else {
|
||||||
// 直接显示完整文本
|
// 关键修复:直接显示完整文本时,清除 attributedText,使用普通 text
|
||||||
NSLog(@"[KBChatAssistantMessageCell] 直接显示完整文本(needsTypewriter: %d, isComplete: %d)",
|
NSLog(@"[KBChatAssistantMessageCell] 直接显示完整文本(needsTypewriter: %d, isComplete: %d)",
|
||||||
message.needsTypewriterEffect, message.isComplete);
|
message.needsTypewriterEffect, message.isComplete);
|
||||||
self.messageLabel.text = message.text;
|
self.messageLabel.attributedText = nil; // 清除 attributedText
|
||||||
|
self.messageLabel.text = message.text; // 设置普通文本
|
||||||
|
|
||||||
|
// 强制布局更新
|
||||||
|
[self.contentView setNeedsLayout];
|
||||||
|
[self.contentView layoutIfNeeded];
|
||||||
|
|
||||||
|
NSLog(@"[KBChatAssistantMessageCell] 直接显示后 Label frame: %@",
|
||||||
|
NSStringFromCGRect(self.messageLabel.frame));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化语音时长(如果时长为 0,不显示)
|
// 格式化语音时长(如果时长为 0,不显示)
|
||||||
@@ -191,30 +202,33 @@
|
|||||||
self.fullText = text;
|
self.fullText = text;
|
||||||
self.currentCharIndex = 0;
|
self.currentCharIndex = 0;
|
||||||
|
|
||||||
// 先设置完整文本,让布局系统计算出正确的高度
|
// 关键修复:先设置普通文本,让布局系统计算出正确的高度
|
||||||
self.messageLabel.text = text;
|
self.messageLabel.text = text;
|
||||||
|
|
||||||
// 强制布局更新
|
// 强制布局更新,确保 Cell 有正确的高度
|
||||||
[self.messageLabel setNeedsLayout];
|
|
||||||
[self.bubbleView setNeedsLayout];
|
|
||||||
[self.contentView setNeedsLayout];
|
[self.contentView setNeedsLayout];
|
||||||
[self layoutIfNeeded];
|
[self.contentView layoutIfNeeded];
|
||||||
|
|
||||||
NSLog(@"[KBChatAssistantMessageCell] 布局后 Label frame: %@", NSStringFromCGRect(self.messageLabel.frame));
|
NSLog(@"[KBChatAssistantMessageCell] 布局后 Label frame: %@, bubbleView frame: %@",
|
||||||
|
NSStringFromCGRect(self.messageLabel.frame),
|
||||||
|
NSStringFromCGRect(self.bubbleView.frame));
|
||||||
|
|
||||||
// 使用 NSAttributedString 实现打字机效果
|
// 关键修复:布局完成后再应用打字机效果的 attributedText
|
||||||
// 先把所有文字设置为透明,然后逐个显示
|
|
||||||
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:text];
|
|
||||||
[attributedText addAttribute:NSForegroundColorAttributeName
|
|
||||||
value:[UIColor clearColor]
|
|
||||||
range:NSMakeRange(0, text.length)];
|
|
||||||
[attributedText addAttribute:NSFontAttributeName
|
|
||||||
value:self.messageLabel.font
|
|
||||||
range:NSMakeRange(0, text.length)];
|
|
||||||
self.messageLabel.attributedText = attributedText;
|
|
||||||
|
|
||||||
// 确保在主线程创建定时器
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
// 使用 NSAttributedString 实现打字机效果
|
||||||
|
// 先把所有文字设置为透明,然后逐个显示
|
||||||
|
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:text];
|
||||||
|
[attributedText addAttribute:NSForegroundColorAttributeName
|
||||||
|
value:[UIColor clearColor]
|
||||||
|
range:NSMakeRange(0, text.length)];
|
||||||
|
[attributedText addAttribute:NSFontAttributeName
|
||||||
|
value:self.messageLabel.font
|
||||||
|
range:NSMakeRange(0, text.length)];
|
||||||
|
self.messageLabel.attributedText = attributedText;
|
||||||
|
|
||||||
|
// 再次强制布局,确保 attributedText 不会改变高度
|
||||||
|
[self.contentView layoutIfNeeded];
|
||||||
|
|
||||||
// 每 0.03 秒显示一个字符(更快的打字机效果)
|
// 每 0.03 秒显示一个字符(更快的打字机效果)
|
||||||
self.typewriterTimer = [NSTimer scheduledTimerWithTimeInterval:0.03
|
self.typewriterTimer = [NSTimer scheduledTimerWithTimeInterval:0.03
|
||||||
target:self
|
target:self
|
||||||
|
|||||||
@@ -64,9 +64,16 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
|||||||
self.tableView.delegate = self;
|
self.tableView.delegate = self;
|
||||||
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||||
self.tableView.backgroundColor = [UIColor clearColor];
|
self.tableView.backgroundColor = [UIColor clearColor];
|
||||||
self.tableView.estimatedRowHeight = 60;
|
// 关键修复:使用合理的估算高度(避免抖动,但不能为0)
|
||||||
|
self.tableView.estimatedRowHeight = 80;
|
||||||
|
self.tableView.estimatedSectionHeaderHeight = 0;
|
||||||
|
self.tableView.estimatedSectionFooterHeight = 0;
|
||||||
self.tableView.rowHeight = UITableViewAutomaticDimension;
|
self.tableView.rowHeight = UITableViewAutomaticDimension;
|
||||||
self.tableView.showsVerticalScrollIndicator = YES;
|
self.tableView.showsVerticalScrollIndicator = YES;
|
||||||
|
// 关键修复:禁用内容自动调整,防止与外层 CollectionView 冲突
|
||||||
|
if (@available(iOS 11.0, *)) {
|
||||||
|
self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
||||||
|
}
|
||||||
[self addSubview:self.tableView];
|
[self addSubview:self.tableView];
|
||||||
|
|
||||||
// 注册 Cell
|
// 注册 Cell
|
||||||
@@ -182,13 +189,22 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)scrollToBottom {
|
- (void)scrollToBottom {
|
||||||
|
[self scrollToBottomAnimated:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)scrollToBottomAnimated:(BOOL)animated {
|
||||||
if (self.messages.count == 0) return;
|
if (self.messages.count == 0) return;
|
||||||
|
|
||||||
|
// 关键修复:使用 layoutIfNeeded 确保布局完成后再滚动
|
||||||
|
[self.tableView layoutIfNeeded];
|
||||||
|
|
||||||
NSIndexPath *lastIndexPath = [NSIndexPath indexPathForRow:self.messages.count - 1
|
NSIndexPath *lastIndexPath = [NSIndexPath indexPathForRow:self.messages.count - 1
|
||||||
inSection:0];
|
inSection:0];
|
||||||
|
|
||||||
|
// 直接滚动到最后一条消息,不检查是否可见(确保新消息能被看到)
|
||||||
[self.tableView scrollToRowAtIndexPath:lastIndexPath
|
[self.tableView scrollToRowAtIndexPath:lastIndexPath
|
||||||
atScrollPosition:UITableViewScrollPositionBottom
|
atScrollPosition:UITableViewScrollPositionBottom
|
||||||
animated:YES];
|
animated:animated];
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Public Helpers
|
#pragma mark - Public Helpers
|
||||||
@@ -240,6 +256,10 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
|||||||
for (NSInteger i = oldCount; i < newCount; i++) {
|
for (NSInteger i = oldCount; i < newCount; i++) {
|
||||||
[indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
|
[indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关键修复:批量插入前先布局,避免高度计算不准确
|
||||||
|
[self.tableView layoutIfNeeded];
|
||||||
|
|
||||||
[self.tableView insertRowsAtIndexPaths:indexPaths
|
[self.tableView insertRowsAtIndexPaths:indexPaths
|
||||||
withRowAnimation:UITableViewRowAnimationNone];
|
withRowAnimation:UITableViewRowAnimationNone];
|
||||||
}
|
}
|
||||||
@@ -247,8 +267,10 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
|||||||
[self updateFooterVisibility];
|
[self updateFooterVisibility];
|
||||||
|
|
||||||
if (autoScroll) {
|
if (autoScroll) {
|
||||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
// 关键修复:插入完成后立即滚动,使用 dispatch_async 确保插入动画完成
|
||||||
[self scrollToBottom];
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[self.tableView layoutIfNeeded]; // 再次确保布局完成
|
||||||
|
[self scrollToBottomAnimated:YES];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,14 +296,14 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
|||||||
CGFloat newContentHeight = self.tableView.contentSize.height;
|
CGFloat newContentHeight = self.tableView.contentSize.height;
|
||||||
CGFloat delta = newContentHeight - oldContentHeight;
|
CGFloat delta = newContentHeight - oldContentHeight;
|
||||||
CGFloat offsetY = oldOffsetY + delta;
|
CGFloat offsetY = oldOffsetY + delta;
|
||||||
|
// 关键修复:使用非动画方式设置 offset,避免抖动
|
||||||
[self.tableView setContentOffset:CGPointMake(0, offsetY) animated:NO];
|
[self.tableView setContentOffset:CGPointMake(0, offsetY) animated:NO];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scrollToBottom) {
|
if (scrollToBottom) {
|
||||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
// 关键修复:直接滚动,不使用延迟
|
||||||
[self scrollToBottom];
|
[self scrollToBottomAnimated:NO];
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,6 +452,31 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 关键修复:优化嵌套滚动体验,减少边界弹簧效果导致的抖动
|
||||||
|
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
|
||||||
|
withVelocity:(CGPoint)velocity
|
||||||
|
targetContentOffset:(inout CGPoint *)targetContentOffset {
|
||||||
|
|
||||||
|
CGFloat offsetY = scrollView.contentOffset.y;
|
||||||
|
CGFloat contentHeight = scrollView.contentSize.height;
|
||||||
|
CGFloat scrollViewHeight = scrollView.bounds.size.height;
|
||||||
|
|
||||||
|
// 如果内容不够长,禁用弹簧效果
|
||||||
|
if (contentHeight <= scrollViewHeight) {
|
||||||
|
scrollView.bounces = NO;
|
||||||
|
} else {
|
||||||
|
scrollView.bounces = YES;
|
||||||
|
|
||||||
|
// 在快速滑动到底部时,避免过度弹簧导致抖动
|
||||||
|
if (velocity.y < 0) { // 向上滑动(到底部)
|
||||||
|
CGFloat maxOffset = contentHeight - scrollViewHeight + scrollView.contentInset.bottom;
|
||||||
|
if (targetContentOffset->y > maxOffset) {
|
||||||
|
targetContentOffset->y = maxOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - KBChatAssistantMessageCellDelegate
|
#pragma mark - KBChatAssistantMessageCellDelegate
|
||||||
|
|
||||||
- (void)assistantMessageCell:(KBChatAssistantMessageCell *)cell
|
- (void)assistantMessageCell:(KBChatAssistantMessageCell *)cell
|
||||||
|
|||||||
@@ -61,6 +61,18 @@
|
|||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 关键修复:Cell 复用时重置状态
|
||||||
|
- (void)prepareForReuse {
|
||||||
|
[super prepareForReuse];
|
||||||
|
|
||||||
|
// 停止音频播放
|
||||||
|
[self.chatView stopPlayingAudio];
|
||||||
|
|
||||||
|
// 重置加载状态
|
||||||
|
self.isLoading = NO;
|
||||||
|
self.hasLoadedData = NO;
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - 1:控件初始化
|
#pragma mark - 1:控件初始化
|
||||||
|
|
||||||
- (void)setupUI {
|
- (void)setupUI {
|
||||||
@@ -130,6 +142,8 @@
|
|||||||
self.nameLabel.text = persona.name;
|
self.nameLabel.text = persona.name;
|
||||||
self.openingLabel.text = persona.shortDesc.length > 0 ? persona.shortDesc : persona.introText;
|
self.openingLabel.text = persona.shortDesc.length > 0 ? persona.shortDesc : persona.introText;
|
||||||
|
|
||||||
|
// 关键修复:清空消息时停止音频播放,避免状态混乱
|
||||||
|
[self.chatView stopPlayingAudio];
|
||||||
[self.chatView clearMessages];
|
[self.chatView clearMessages];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,9 @@
|
|||||||
/// AiVM 实例
|
/// AiVM 实例
|
||||||
@property (nonatomic, strong) AiVM *aiVM;
|
@property (nonatomic, strong) AiVM *aiVM;
|
||||||
|
|
||||||
|
/// 是否正在等待 AI 回复(用于禁止滚动)
|
||||||
|
@property (nonatomic, assign) BOOL isWaitingForAIResponse;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation KBAIHomeVC
|
@implementation KBAIHomeVC
|
||||||
@@ -83,6 +86,7 @@
|
|||||||
self.currentIndex = 0;
|
self.currentIndex = 0;
|
||||||
self.preloadedIndexes = [NSMutableSet set];
|
self.preloadedIndexes = [NSMutableSet set];
|
||||||
self.aiVM = [[AiVM alloc] init];
|
self.aiVM = [[AiVM alloc] init];
|
||||||
|
self.isWaitingForAIResponse = NO; // 初始化状态
|
||||||
|
|
||||||
[self setupUI];
|
[self setupUI];
|
||||||
[self setupVoiceToTextManager];
|
[self setupVoiceToTextManager];
|
||||||
@@ -259,6 +263,11 @@
|
|||||||
#pragma mark - UIScrollViewDelegate
|
#pragma mark - UIScrollViewDelegate
|
||||||
|
|
||||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
||||||
|
// 关键修复:如果正在等待 AI 回复,不进行预加载等操作
|
||||||
|
if (self.isWaitingForAIResponse) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
CGFloat pageHeight = scrollView.bounds.size.height;
|
CGFloat pageHeight = scrollView.bounds.size.height;
|
||||||
CGFloat offsetY = scrollView.contentOffset.y;
|
CGFloat offsetY = scrollView.contentOffset.y;
|
||||||
NSInteger currentPage = offsetY / pageHeight;
|
NSInteger currentPage = offsetY / pageHeight;
|
||||||
@@ -288,6 +297,16 @@
|
|||||||
[self updateChatViewBottomInset];
|
[self updateChatViewBottomInset];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 关键修复:禁止在等待 AI 回复时开始拖拽
|
||||||
|
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
|
||||||
|
if (self.isWaitingForAIResponse) {
|
||||||
|
NSLog(@"[KBAIHomeVC] 正在等待 AI 回复,禁止滚动");
|
||||||
|
// 强制停止滚动
|
||||||
|
scrollView.scrollEnabled = NO;
|
||||||
|
scrollView.scrollEnabled = YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - 4:语音转写
|
#pragma mark - 4:语音转写
|
||||||
|
|
||||||
- (void)setupVoiceToTextManager {
|
- (void)setupVoiceToTextManager {
|
||||||
@@ -607,6 +626,11 @@
|
|||||||
[currentCell appendUserMessage:text];
|
[currentCell appendUserMessage:text];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关键修复:发送消息前禁止 CollectionView 滚动
|
||||||
|
self.isWaitingForAIResponse = YES;
|
||||||
|
self.collectionView.scrollEnabled = NO;
|
||||||
|
NSLog(@"[KBAIHomeVC] 开始等待 AI 回复,禁止 CollectionView 滚动");
|
||||||
|
|
||||||
__weak typeof(self) weakSelf = self;
|
__weak typeof(self) weakSelf = self;
|
||||||
[self.aiVM requestChatMessageWithContent:text
|
[self.aiVM requestChatMessageWithContent:text
|
||||||
companionId:companionId
|
companionId:companionId
|
||||||
@@ -617,10 +641,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
// if (error) {
|
// 关键修复:收到响应后(无论成功或失败)重新启用 CollectionView 滚动
|
||||||
// NSLog(@"[KBAIHomeVC] 请求聊天失败:%@", error.localizedDescription);
|
strongSelf.isWaitingForAIResponse = NO;
|
||||||
// return;
|
strongSelf.collectionView.scrollEnabled = YES;
|
||||||
// }
|
NSLog(@"[KBAIHomeVC] AI 回复完成,恢复 CollectionView 滚动");
|
||||||
|
|
||||||
if (response.code == 50030) {
|
if (response.code == 50030) {
|
||||||
NSString *message = response.message ?: @"";
|
NSString *message = response.message ?: @"";
|
||||||
|
|||||||
Reference in New Issue
Block a user