diff --git a/keyBoard/Class/AiTalk/V/KBChatAssistantMessageCell.m b/keyBoard/Class/AiTalk/V/KBChatAssistantMessageCell.m index 0af109b..965c688 100644 --- a/keyBoard/Class/AiTalk/V/KBChatAssistantMessageCell.m +++ b/keyBoard/Class/AiTalk/V/KBChatAssistantMessageCell.m @@ -73,6 +73,7 @@ self.messageLabel.numberOfLines = 0; self.messageLabel.font = [UIFont systemFontOfSize:16]; self.messageLabel.textColor = [UIColor whiteColor]; + self.messageLabel.lineBreakMode = NSLineBreakByWordWrapping; // 设置 preferredMaxLayoutWidth 让 AutoLayout 能正确计算多行高度 CGFloat maxWidth = [UIScreen mainScreen].bounds.size.width * 0.75 - 16 - 24; self.messageLabel.preferredMaxLayoutWidth = maxWidth; @@ -94,20 +95,22 @@ make.center.equalTo(self.voiceButton); }]; - // bubbleView 约束 + // 关键修复:bubbleView 必须有明确的高度约束链 [self.bubbleView mas_makeConstraints:^(MASConstraintMaker *make) { 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.width.lessThanOrEqualTo(self.contentView).multipliedBy(0.75); }]; - // messageLabel 约束 + // 关键修复:messageLabel 约束必须完整,让 AutoLayout 能推导出 bubbleView 的高度 [self.messageLabel mas_makeConstraints:^(MASConstraintMaker *make) { 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.right.equalTo(self.bubbleView).offset(-12); + // 关键修复:给 messageLabel 一个最小高度,防止高度为 0 + make.height.greaterThanOrEqualTo(@20); }]; } @@ -127,10 +130,18 @@ NSLog(@"[KBChatAssistantMessageCell] 启动新的打字机效果"); [self startTypewriterEffectWithText:message.text]; } else { - // 直接显示完整文本 + // 关键修复:直接显示完整文本时,清除 attributedText,使用普通 text NSLog(@"[KBChatAssistantMessageCell] 直接显示完整文本(needsTypewriter: %d, isComplete: %d)", 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,不显示) @@ -191,30 +202,33 @@ self.fullText = text; self.currentCharIndex = 0; - // 先设置完整文本,让布局系统计算出正确的高度 + // 关键修复:先设置普通文本,让布局系统计算出正确的高度 self.messageLabel.text = text; - // 强制布局更新 - [self.messageLabel setNeedsLayout]; - [self.bubbleView setNeedsLayout]; + // 强制布局更新,确保 Cell 有正确的高度 [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 实现打字机效果 - // 先把所有文字设置为透明,然后逐个显示 - 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 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 秒显示一个字符(更快的打字机效果) self.typewriterTimer = [NSTimer scheduledTimerWithTimeInterval:0.03 target:self diff --git a/keyBoard/Class/AiTalk/V/KBChatTableView.m b/keyBoard/Class/AiTalk/V/KBChatTableView.m index 5dcaedd..81f60c0 100644 --- a/keyBoard/Class/AiTalk/V/KBChatTableView.m +++ b/keyBoard/Class/AiTalk/V/KBChatTableView.m @@ -64,9 +64,16 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟 self.tableView.delegate = self; self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; 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.showsVerticalScrollIndicator = YES; + // 关键修复:禁用内容自动调整,防止与外层 CollectionView 冲突 + if (@available(iOS 11.0, *)) { + self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + } [self addSubview:self.tableView]; // 注册 Cell @@ -182,13 +189,22 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟 } - (void)scrollToBottom { + [self scrollToBottomAnimated:YES]; +} + +- (void)scrollToBottomAnimated:(BOOL)animated { if (self.messages.count == 0) return; + // 关键修复:使用 layoutIfNeeded 确保布局完成后再滚动 + [self.tableView layoutIfNeeded]; + NSIndexPath *lastIndexPath = [NSIndexPath indexPathForRow:self.messages.count - 1 inSection:0]; + + // 直接滚动到最后一条消息,不检查是否可见(确保新消息能被看到) [self.tableView scrollToRowAtIndexPath:lastIndexPath atScrollPosition:UITableViewScrollPositionBottom - animated:YES]; + animated:animated]; } #pragma mark - Public Helpers @@ -240,6 +256,10 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟 for (NSInteger i = oldCount; i < newCount; i++) { [indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]]; } + + // 关键修复:批量插入前先布局,避免高度计算不准确 + [self.tableView layoutIfNeeded]; + [self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; } @@ -247,8 +267,10 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟 [self updateFooterVisibility]; if (autoScroll) { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self scrollToBottom]; + // 关键修复:插入完成后立即滚动,使用 dispatch_async 确保插入动画完成 + 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 delta = newContentHeight - oldContentHeight; CGFloat offsetY = oldOffsetY + delta; + // 关键修复:使用非动画方式设置 offset,避免抖动 [self.tableView setContentOffset:CGPointMake(0, offsetY) animated:NO]; return; } 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 - (void)assistantMessageCell:(KBChatAssistantMessageCell *)cell diff --git a/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m b/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m index deb74c2..9b2495c 100644 --- a/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m +++ b/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m @@ -61,6 +61,18 @@ return self; } +/// 关键修复:Cell 复用时重置状态 +- (void)prepareForReuse { + [super prepareForReuse]; + + // 停止音频播放 + [self.chatView stopPlayingAudio]; + + // 重置加载状态 + self.isLoading = NO; + self.hasLoadedData = NO; +} + #pragma mark - 1:控件初始化 - (void)setupUI { @@ -130,6 +142,8 @@ self.nameLabel.text = persona.name; self.openingLabel.text = persona.shortDesc.length > 0 ? persona.shortDesc : persona.introText; + // 关键修复:清空消息时停止音频播放,避免状态混乱 + [self.chatView stopPlayingAudio]; [self.chatView clearMessages]; } diff --git a/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m b/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m index 01dfc70..9dec325 100644 --- a/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m +++ b/keyBoard/Class/AiTalk/VC/KBAIHomeVC.m @@ -65,6 +65,9 @@ /// AiVM 实例 @property (nonatomic, strong) AiVM *aiVM; +/// 是否正在等待 AI 回复(用于禁止滚动) +@property (nonatomic, assign) BOOL isWaitingForAIResponse; + @end @implementation KBAIHomeVC @@ -83,6 +86,7 @@ self.currentIndex = 0; self.preloadedIndexes = [NSMutableSet set]; self.aiVM = [[AiVM alloc] init]; + self.isWaitingForAIResponse = NO; // 初始化状态 [self setupUI]; [self setupVoiceToTextManager]; @@ -259,6 +263,11 @@ #pragma mark - UIScrollViewDelegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView { + // 关键修复:如果正在等待 AI 回复,不进行预加载等操作 + if (self.isWaitingForAIResponse) { + return; + } + CGFloat pageHeight = scrollView.bounds.size.height; CGFloat offsetY = scrollView.contentOffset.y; NSInteger currentPage = offsetY / pageHeight; @@ -288,6 +297,16 @@ [self updateChatViewBottomInset]; } +/// 关键修复:禁止在等待 AI 回复时开始拖拽 +- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { + if (self.isWaitingForAIResponse) { + NSLog(@"[KBAIHomeVC] 正在等待 AI 回复,禁止滚动"); + // 强制停止滚动 + scrollView.scrollEnabled = NO; + scrollView.scrollEnabled = YES; + } +} + #pragma mark - 4:语音转写 - (void)setupVoiceToTextManager { @@ -607,6 +626,11 @@ [currentCell appendUserMessage:text]; } + // 关键修复:发送消息前禁止 CollectionView 滚动 + self.isWaitingForAIResponse = YES; + self.collectionView.scrollEnabled = NO; + NSLog(@"[KBAIHomeVC] 开始等待 AI 回复,禁止 CollectionView 滚动"); + __weak typeof(self) weakSelf = self; [self.aiVM requestChatMessageWithContent:text companionId:companionId @@ -617,10 +641,10 @@ } dispatch_async(dispatch_get_main_queue(), ^{ -// if (error) { -// NSLog(@"[KBAIHomeVC] 请求聊天失败:%@", error.localizedDescription); -// return; -// } + // 关键修复:收到响应后(无论成功或失败)重新启用 CollectionView 滚动 + strongSelf.isWaitingForAIResponse = NO; + strongSelf.collectionView.scrollEnabled = YES; + NSLog(@"[KBAIHomeVC] AI 回复完成,恢复 CollectionView 滚动"); if (response.code == 50030) { NSString *message = response.message ?: @"";