发送文本处理ui和逻辑
This commit is contained in:
@@ -46,6 +46,9 @@ typedef NS_ENUM(NSInteger, KBVoiceInputBarState) {
|
|||||||
/// 代理
|
/// 代理
|
||||||
@property (nonatomic, weak) id<KBVoiceInputBarDelegate> delegate;
|
@property (nonatomic, weak) id<KBVoiceInputBarDelegate> delegate;
|
||||||
|
|
||||||
|
/// 文本发送回调(文本模式下发送消息时触发)
|
||||||
|
@property (nonatomic, copy) void (^onTextSend)(NSString *text);
|
||||||
|
|
||||||
/// 状态文本(显示在按钮上方)
|
/// 状态文本(显示在按钮上方)
|
||||||
@property (nonatomic, copy) NSString *statusText;
|
@property (nonatomic, copy) NSString *statusText;
|
||||||
|
|
||||||
|
|||||||
@@ -408,8 +408,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)handleTextCenterTap {
|
- (void)handleTextCenterTap {
|
||||||
self.inputState = KBVoiceInputBarStateText;
|
// 文本模式点击时,触发回调让 VC 显示文本输入框
|
||||||
[self.hiddenTextField becomeFirstResponder];
|
if (self.onTextSend) {
|
||||||
|
self.onTextSend(nil);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)handleVoiceLongPress:(UILongPressGestureRecognizer *)gesture {
|
- (void)handleVoiceLongPress:(UILongPressGestureRecognizer *)gesture {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
#import "KBAIMessageVC.h"
|
#import "KBAIMessageVC.h"
|
||||||
#import <Masonry/Masonry.h>
|
#import <Masonry/Masonry.h>
|
||||||
|
|
||||||
@interface KBAIHomeVC () <UICollectionViewDelegate, UICollectionViewDataSource, KBVoiceToTextManagerDelegate, KBVoiceRecordManagerDelegate, UIGestureRecognizerDelegate, KBChatLimitPopViewDelegate>
|
@interface KBAIHomeVC () <UICollectionViewDelegate, UICollectionViewDataSource, KBVoiceToTextManagerDelegate, KBVoiceRecordManagerDelegate, UIGestureRecognizerDelegate, KBChatLimitPopViewDelegate, UITextViewDelegate>
|
||||||
|
|
||||||
/// 人设列表容器
|
/// 人设列表容器
|
||||||
@property (nonatomic, strong) UICollectionView *collectionView;
|
@property (nonatomic, strong) UICollectionView *collectionView;
|
||||||
@@ -31,11 +31,24 @@
|
|||||||
@property (nonatomic, assign) CGFloat voiceInputBarHeight;
|
@property (nonatomic, assign) CGFloat voiceInputBarHeight;
|
||||||
@property (nonatomic, assign) CGFloat baseInputBarBottomSpacing;
|
@property (nonatomic, assign) CGFloat baseInputBarBottomSpacing;
|
||||||
@property (nonatomic, assign) CGFloat currentKeyboardHeight;
|
@property (nonatomic, assign) CGFloat currentKeyboardHeight;
|
||||||
/// 仅用于标记“由 KBVoiceInputBar 触发的键盘”是否处于激活态
|
/// 仅用于标记"由 KBVoiceInputBar 触发的键盘"是否处于激活态
|
||||||
@property (nonatomic, assign) BOOL voiceInputKeyboardActive;
|
@property (nonatomic, assign) BOOL voiceInputKeyboardActive;
|
||||||
@property (nonatomic, strong) UITapGestureRecognizer *dismissKeyboardTap;
|
@property (nonatomic, strong) UITapGestureRecognizer *dismissKeyboardTap;
|
||||||
@property (nonatomic, weak) LSTPopView *chatLimitPopView;
|
@property (nonatomic, weak) LSTPopView *chatLimitPopView;
|
||||||
|
|
||||||
|
/// 文本输入容器视图(键盘弹起时显示)
|
||||||
|
@property (nonatomic, strong) UIView *textInputContainerView;
|
||||||
|
/// 文本输入框
|
||||||
|
@property (nonatomic, strong) UITextView *textInputTextView;
|
||||||
|
/// 发送按钮
|
||||||
|
@property (nonatomic, strong) UIButton *sendButton;
|
||||||
|
/// 占位符标签
|
||||||
|
@property (nonatomic, strong) UILabel *placeholderLabel;
|
||||||
|
/// 文本输入容器底部约束
|
||||||
|
@property (nonatomic, strong) MASConstraint *textInputContainerBottomConstraint;
|
||||||
|
/// 是否处于文本输入模式
|
||||||
|
@property (nonatomic, assign) BOOL isTextInputMode;
|
||||||
|
|
||||||
/// 底部毛玻璃背景
|
/// 底部毛玻璃背景
|
||||||
@property (nonatomic, strong) UIView *bottomBackgroundView;
|
@property (nonatomic, strong) UIView *bottomBackgroundView;
|
||||||
@property (nonatomic, strong) UIVisualEffectView *bottomBlurEffectView;
|
@property (nonatomic, strong) UIVisualEffectView *bottomBlurEffectView;
|
||||||
@@ -94,12 +107,16 @@
|
|||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 仅允许 KBVoiceInputBar 触发键盘联动(避免评论输入等场景误触发)
|
/// 仅允许 KBVoiceInputBar 或文本输入框触发键盘联动
|
||||||
- (BOOL)kb_isKeyboardFromVoiceInputBar {
|
- (BOOL)kb_isKeyboardFromVoiceInputBar {
|
||||||
UIView *firstResponder = [self kb_findFirstResponderInView:self.view];
|
UIView *firstResponder = [self kb_findFirstResponderInView:self.view];
|
||||||
if (!firstResponder) {
|
if (!firstResponder) {
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
|
// 文本输入模式下,textInputTextView 也算
|
||||||
|
if (firstResponder == self.textInputTextView) {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
return [firstResponder isDescendantOfView:self.voiceInputBar];
|
return [firstResponder isDescendantOfView:self.voiceInputBar];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,9 +134,12 @@
|
|||||||
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.isWaitingForAIResponse = NO;
|
||||||
|
self.isTextInputMode = NO;
|
||||||
|
|
||||||
[self setupUI];
|
[self setupUI];
|
||||||
|
[self setupTextInputView];
|
||||||
|
[self setupVoiceInputBarCallback];
|
||||||
[self setupVoiceToTextManager];
|
[self setupVoiceToTextManager];
|
||||||
[self setupVoiceRecordManager];
|
[self setupVoiceRecordManager];
|
||||||
[self setupKeyboardNotifications];
|
[self setupKeyboardNotifications];
|
||||||
@@ -151,13 +171,12 @@
|
|||||||
make.right.equalTo(self.view).offset(-16);
|
make.right.equalTo(self.view).offset(-16);
|
||||||
make.width.height.mas_equalTo(32);
|
make.width.height.mas_equalTo(32);
|
||||||
}];
|
}];
|
||||||
|
|
||||||
// 底部毛玻璃背景
|
// 底部毛玻璃背景
|
||||||
[self.view addSubview:self.bottomBackgroundView];
|
[self.view addSubview:self.bottomBackgroundView];
|
||||||
[self.bottomBackgroundView addSubview:self.bottomBlurEffectView];
|
[self.bottomBackgroundView addSubview:self.bottomBlurEffectView];
|
||||||
[self.bottomBackgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.bottomBackgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.left.right.equalTo(self.view);
|
make.left.right.equalTo(self.view);
|
||||||
// self.bottomBackgroundBottomConstraint = make.bottom.equalTo(self.view).offset(-self.baseInputBarBottomSpacing);
|
|
||||||
make.bottom.equalTo(self.view);
|
make.bottom.equalTo(self.view);
|
||||||
make.height.mas_equalTo(self.voiceInputBarHeight);
|
make.height.mas_equalTo(self.voiceInputBarHeight);
|
||||||
}];
|
}];
|
||||||
@@ -170,10 +189,73 @@
|
|||||||
[self.voiceInputBar mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.voiceInputBar mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.left.right.equalTo(self.view);
|
make.left.right.equalTo(self.view);
|
||||||
self.voiceInputBarBottomConstraint = make.bottom.equalTo(self.view).offset(-self.baseInputBarBottomSpacing);
|
self.voiceInputBarBottomConstraint = make.bottom.equalTo(self.view).offset(-self.baseInputBarBottomSpacing);
|
||||||
make.height.mas_equalTo(self.voiceInputBarHeight); // 根据实际需要调整高度
|
make.height.mas_equalTo(self.voiceInputBarHeight);
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 设置文本输入视图
|
||||||
|
- (void)setupTextInputView {
|
||||||
|
// 文本输入容器(初始隐藏)
|
||||||
|
[self.view addSubview:self.textInputContainerView];
|
||||||
|
[self.textInputContainerView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.left.right.equalTo(self.view);
|
||||||
|
self.textInputContainerBottomConstraint = make.bottom.equalTo(self.view).offset(100); // 初始在屏幕外
|
||||||
|
make.height.mas_greaterThanOrEqualTo(50);
|
||||||
|
}];
|
||||||
|
|
||||||
|
[self.textInputContainerView addSubview:self.textInputTextView];
|
||||||
|
[self.textInputContainerView addSubview:self.sendButton];
|
||||||
|
[self.textInputTextView addSubview:self.placeholderLabel];
|
||||||
|
|
||||||
|
[self.sendButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.right.equalTo(self.textInputContainerView).offset(-16);
|
||||||
|
make.bottom.equalTo(self.textInputContainerView).offset(-10);
|
||||||
|
make.width.mas_equalTo(60);
|
||||||
|
make.height.mas_equalTo(36);
|
||||||
|
}];
|
||||||
|
|
||||||
|
[self.textInputTextView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.left.equalTo(self.textInputContainerView).offset(16);
|
||||||
|
make.right.equalTo(self.sendButton.mas_left).offset(-10);
|
||||||
|
make.top.equalTo(self.textInputContainerView).offset(8);
|
||||||
|
make.bottom.equalTo(self.textInputContainerView).offset(-8);
|
||||||
|
make.height.mas_greaterThanOrEqualTo(36);
|
||||||
|
make.height.mas_lessThanOrEqualTo(100);
|
||||||
|
}];
|
||||||
|
|
||||||
|
[self.placeholderLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.left.equalTo(self.textInputTextView).offset(5);
|
||||||
|
make.top.equalTo(self.textInputTextView).offset(8);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置 VoiceInputBar 的文本发送回调
|
||||||
|
- (void)setupVoiceInputBarCallback {
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
self.voiceInputBar.onTextSend = ^(NSString *text) {
|
||||||
|
// 文本模式下点击,显示文本输入框
|
||||||
|
[weakSelf showTextInputView];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 显示文本输入视图
|
||||||
|
- (void)showTextInputView {
|
||||||
|
self.isTextInputMode = YES;
|
||||||
|
self.voiceInputBar.hidden = YES;
|
||||||
|
self.textInputContainerView.hidden = NO;
|
||||||
|
[self.textInputTextView becomeFirstResponder];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 隐藏文本输入视图
|
||||||
|
- (void)hideTextInputView {
|
||||||
|
self.isTextInputMode = NO;
|
||||||
|
[self.textInputTextView resignFirstResponder];
|
||||||
|
self.textInputContainerView.hidden = YES;
|
||||||
|
self.voiceInputBar.hidden = NO;
|
||||||
|
self.textInputTextView.text = @"";
|
||||||
|
self.placeholderLabel.hidden = NO;
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - 2:数据加载
|
#pragma mark - 2:数据加载
|
||||||
|
|
||||||
- (void)loadPersonas {
|
- (void)loadPersonas {
|
||||||
@@ -191,7 +273,6 @@
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
NSLog(@"加载人设列表失败:%@", error.localizedDescription);
|
NSLog(@"加载人设列表失败:%@", error.localizedDescription);
|
||||||
// TODO: 显示错误提示
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,24 +281,18 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 追加数据
|
|
||||||
[weakSelf.personas addObjectsFromArray:pageModel.records];
|
[weakSelf.personas addObjectsFromArray:pageModel.records];
|
||||||
weakSelf.hasMore = pageModel.hasMore;
|
weakSelf.hasMore = pageModel.hasMore;
|
||||||
|
|
||||||
// 刷新 UI
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[weakSelf.collectionView reloadData];
|
[weakSelf.collectionView reloadData];
|
||||||
|
|
||||||
// 首次加载,预加载前 3 个
|
|
||||||
if (weakSelf.currentPage == 1) {
|
if (weakSelf.currentPage == 1) {
|
||||||
[weakSelf preloadDataForIndexes:@[@0, @1, @2]];
|
[weakSelf preloadDataForIndexes:@[@0, @1, @2]];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
NSLog(@"加载成功:当前 %ld 条,总共 %ld 条,还有更多:%@",
|
NSLog(@"加载成功:当前 %ld 条,总共 %ld 条,还有更多:%@",
|
||||||
weakSelf.personas.count,
|
weakSelf.personas.count, pageModel.total, pageModel.hasMore ? @"是" : @"否");
|
||||||
pageModel.total,
|
|
||||||
pageModel.hasMore ? @"是" : @"否");
|
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +300,6 @@
|
|||||||
if (!self.hasMore || self.isLoading) {
|
if (!self.hasMore || self.isLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.currentPage++;
|
self.currentPage++;
|
||||||
[self loadPersonas];
|
[self loadPersonas];
|
||||||
}
|
}
|
||||||
@@ -238,20 +312,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
NSMutableArray *indexesToPreload = [NSMutableArray array];
|
NSMutableArray *indexesToPreload = [NSMutableArray array];
|
||||||
|
|
||||||
// 上一个
|
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
[indexesToPreload addObject:@(index - 1)];
|
[indexesToPreload addObject:@(index - 1)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当前
|
|
||||||
[indexesToPreload addObject:@(index)];
|
[indexesToPreload addObject:@(index)];
|
||||||
|
|
||||||
// 下一个
|
|
||||||
if (index < self.personas.count - 1) {
|
if (index < self.personas.count - 1) {
|
||||||
[indexesToPreload addObject:@(index + 1)];
|
[indexesToPreload addObject:@(index + 1)];
|
||||||
}
|
}
|
||||||
|
|
||||||
[self preloadDataForIndexes:indexesToPreload];
|
[self preloadDataForIndexes:indexesToPreload];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +327,6 @@
|
|||||||
if ([self.preloadedIndexes containsObject:indexNum]) {
|
if ([self.preloadedIndexes containsObject:indexNum]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
[self.preloadedIndexes addObject:indexNum];
|
[self.preloadedIndexes addObject:indexNum];
|
||||||
|
|
||||||
NSInteger index = [indexNum integerValue];
|
NSInteger index = [indexNum integerValue];
|
||||||
@@ -270,11 +336,9 @@
|
|||||||
|
|
||||||
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:0];
|
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:0];
|
||||||
KBPersonaChatCell *cell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
|
KBPersonaChatCell *cell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
|
||||||
|
|
||||||
if (cell) {
|
if (cell) {
|
||||||
[cell preloadDataIfNeeded];
|
[cell preloadDataIfNeeded];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSLog(@"预加载第 %ld 个人设", (long)index);
|
NSLog(@"预加载第 %ld 个人设", (long)index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -289,20 +353,14 @@
|
|||||||
KBPersonaChatCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"KBPersonaChatCell" forIndexPath:indexPath];
|
KBPersonaChatCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"KBPersonaChatCell" forIndexPath:indexPath];
|
||||||
cell.persona = self.personas[indexPath.item];
|
cell.persona = self.personas[indexPath.item];
|
||||||
[self updateChatViewBottomInset];
|
[self updateChatViewBottomInset];
|
||||||
|
|
||||||
// 标记为已预加载
|
|
||||||
[self.preloadedIndexes addObject:@(indexPath.item)];
|
[self.preloadedIndexes addObject:@(indexPath.item)];
|
||||||
|
|
||||||
// 直接触发预加载
|
|
||||||
[cell preloadDataIfNeeded];
|
[cell preloadDataIfNeeded];
|
||||||
|
|
||||||
return cell;
|
return cell;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - UIScrollViewDelegate
|
#pragma mark - UIScrollViewDelegate
|
||||||
|
|
||||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
||||||
// 关键修复:如果正在等待 AI 回复,不进行预加载等操作
|
|
||||||
if (self.isWaitingForAIResponse) {
|
if (self.isWaitingForAIResponse) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -311,14 +369,12 @@
|
|||||||
CGFloat offsetY = scrollView.contentOffset.y;
|
CGFloat offsetY = scrollView.contentOffset.y;
|
||||||
NSInteger currentPage = offsetY / pageHeight;
|
NSInteger currentPage = offsetY / pageHeight;
|
||||||
|
|
||||||
// 滑动超过 30% 就预加载
|
|
||||||
if (fmod(offsetY, pageHeight) > pageHeight * 0.3) {
|
if (fmod(offsetY, pageHeight) > pageHeight * 0.3) {
|
||||||
[self preloadAdjacentCellsForIndex:currentPage + 1];
|
[self preloadAdjacentCellsForIndex:currentPage + 1];
|
||||||
} else {
|
} else {
|
||||||
[self preloadAdjacentCellsForIndex:currentPage];
|
[self preloadAdjacentCellsForIndex:currentPage];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 接近底部时加载更多
|
|
||||||
if (offsetY + scrollView.bounds.size.height >= scrollView.contentSize.height - pageHeight) {
|
if (offsetY + scrollView.bounds.size.height >= scrollView.contentSize.height - pageHeight) {
|
||||||
[self loadMorePersonas];
|
[self loadMorePersonas];
|
||||||
}
|
}
|
||||||
@@ -332,15 +388,12 @@
|
|||||||
if (currentPage < self.personas.count) {
|
if (currentPage < self.personas.count) {
|
||||||
NSLog(@"当前在第 %ld 个人设:%@", (long)currentPage, self.personas[currentPage].name);
|
NSLog(@"当前在第 %ld 个人设:%@", (long)currentPage, self.personas[currentPage].name);
|
||||||
}
|
}
|
||||||
|
|
||||||
[self updateChatViewBottomInset];
|
[self updateChatViewBottomInset];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 关键修复:禁止在等待 AI 回复时开始拖拽
|
|
||||||
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
|
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
|
||||||
if (self.isWaitingForAIResponse) {
|
if (self.isWaitingForAIResponse) {
|
||||||
NSLog(@"[KBAIHomeVC] 正在等待 AI 回复,禁止滚动");
|
NSLog(@"[KBAIHomeVC] 正在等待 AI 回复,禁止滚动");
|
||||||
// 强制停止滚动
|
|
||||||
scrollView.scrollEnabled = NO;
|
scrollView.scrollEnabled = NO;
|
||||||
scrollView.scrollEnabled = YES;
|
scrollView.scrollEnabled = YES;
|
||||||
}
|
}
|
||||||
@@ -377,45 +430,42 @@
|
|||||||
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
||||||
UIViewAnimationOptions options = ([userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue] << 16);
|
UIViewAnimationOptions options = ([userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue] << 16);
|
||||||
|
|
||||||
// 将键盘的 frame 转换到当前 view 的坐标系
|
|
||||||
CGRect convertedFrame = [self.view convertRect:endFrame fromView:nil];
|
CGRect convertedFrame = [self.view convertRect:endFrame fromView:nil];
|
||||||
CGFloat keyboardHeight = MAX(0.0, CGRectGetMaxY(self.view.bounds) - CGRectGetMinY(convertedFrame));
|
CGFloat keyboardHeight = MAX(0.0, CGRectGetMaxY(self.view.bounds) - CGRectGetMinY(convertedFrame));
|
||||||
|
|
||||||
// Gate:只让 KBVoiceInputBar 触发聊天区域的联动(评论输入等场景会误触发)
|
|
||||||
if (keyboardHeight > 0.0) {
|
if (keyboardHeight > 0.0) {
|
||||||
if (![self kb_isKeyboardFromVoiceInputBar]) {
|
if (![self kb_isKeyboardFromVoiceInputBar]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.voiceInputKeyboardActive = YES;
|
self.voiceInputKeyboardActive = YES;
|
||||||
} else {
|
} else {
|
||||||
// 键盘收起时:只有之前是由 KBVoiceInputBar 触发的,才需要恢复
|
|
||||||
if (!self.voiceInputKeyboardActive) {
|
if (!self.voiceInputKeyboardActive) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.voiceInputKeyboardActive = NO;
|
self.voiceInputKeyboardActive = NO;
|
||||||
|
// 键盘隐藏时,如果是文本输入模式,隐藏文本输入框并显示 VoiceInputBar
|
||||||
|
if (self.isTextInputMode) {
|
||||||
|
[self hideTextInputView];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.currentKeyboardHeight = keyboardHeight;
|
self.currentKeyboardHeight = keyboardHeight;
|
||||||
|
|
||||||
NSLog(@"[KBAIHomeVC] 键盘高度: %.2f, 屏幕高度: %.2f, 键盘 Y: %.2f",
|
NSLog(@"[KBAIHomeVC] 键盘高度: %.2f", keyboardHeight);
|
||||||
keyboardHeight, CGRectGetMaxY(self.view.bounds), CGRectGetMinY(convertedFrame));
|
|
||||||
|
|
||||||
// 问题1修复:计算 VoiceInputBar 应该距离 view.bottom 多远
|
|
||||||
CGFloat bottomSpacing;
|
CGFloat bottomSpacing;
|
||||||
if (keyboardHeight > 0.0) {
|
if (keyboardHeight > 0.0) {
|
||||||
// 键盘弹起时:让 VoiceInputBar 紧贴键盘上方(距离 5px)
|
|
||||||
// bottomSpacing = 键盘高度 - 5px(因为是负的 offset)
|
|
||||||
bottomSpacing = keyboardHeight - 5.0;
|
bottomSpacing = keyboardHeight - 5.0;
|
||||||
|
// 文本输入模式:更新文本输入容器位置
|
||||||
|
if (self.isTextInputMode) {
|
||||||
|
[self.textInputContainerBottomConstraint setOffset:-keyboardHeight];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 键盘隐藏时:恢复到原始位置
|
|
||||||
bottomSpacing = self.baseInputBarBottomSpacing;
|
bottomSpacing = self.baseInputBarBottomSpacing;
|
||||||
|
[self.textInputContainerBottomConstraint setOffset:100]; // 移出屏幕
|
||||||
}
|
}
|
||||||
|
|
||||||
NSLog(@"[KBAIHomeVC] VoiceInputBar bottomSpacing: %.2f", bottomSpacing);
|
|
||||||
|
|
||||||
[self.voiceInputBarBottomConstraint setOffset:-bottomSpacing];
|
[self.voiceInputBarBottomConstraint setOffset:-bottomSpacing];
|
||||||
|
|
||||||
// 问题2修复:键盘弹起时更新 ChatView 的 bottomInset
|
|
||||||
[self updateChatViewBottomInset];
|
[self updateChatViewBottomInset];
|
||||||
|
|
||||||
[UIView animateWithDuration:duration
|
[UIView animateWithDuration:duration
|
||||||
@@ -447,6 +497,9 @@
|
|||||||
if ([touch.view isDescendantOfView:self.voiceInputBar]) {
|
if ([touch.view isDescendantOfView:self.voiceInputBar]) {
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
|
if ([touch.view isDescendantOfView:self.textInputContainerView]) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,46 +539,27 @@
|
|||||||
return visibleCell;
|
return visibleCell;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Private
|
#pragma mark - Private
|
||||||
|
|
||||||
- (void)updateChatViewBottomInset {
|
- (void)updateChatViewBottomInset {
|
||||||
// 键盘弹起时,增加 bottomInset 让最后一条消息显示在 VoiceInputBar 上方
|
|
||||||
CGFloat bottomInset;
|
CGFloat bottomInset;
|
||||||
|
|
||||||
if (self.currentKeyboardHeight > 0.0) {
|
if (self.currentKeyboardHeight > 0.0) {
|
||||||
// 键盘弹起时:
|
CGFloat avatarBottomSpace = KB_TABBAR_HEIGHT + 50 + 20;
|
||||||
// avatarImageView 距离屏幕底部的距离 = KB_TABBAR_HEIGHT + 50 + 20
|
CGFloat chatViewPhysicalBottomSpace = avatarBottomSpace + 54 + 10;
|
||||||
// chatView 物理底部到屏幕底部 = KB_TABBAR_HEIGHT + 50 + 20 + 54(头像) + 10(间距) ≈ 153
|
|
||||||
// 但是 VoiceInputBar 和键盘会遮挡 chatView
|
|
||||||
// 需要的 bottomInset = 键盘高度 + VoiceInputBar 高度 - chatView 已经避开的底部区域
|
|
||||||
|
|
||||||
CGFloat avatarBottomSpace = KB_TABBAR_HEIGHT + 50 + 20; // avatarImageView 距离底部的距离
|
|
||||||
CGFloat chatViewPhysicalBottomSpace = avatarBottomSpace + 54 + 10; // chatView 物理底部距离屏幕底部
|
|
||||||
|
|
||||||
// 需要抬高的额外距离 = (键盘 + InputBar) - chatView 已经避开的空间
|
|
||||||
bottomInset = (self.currentKeyboardHeight + self.voiceInputBarHeight) - chatViewPhysicalBottomSpace;
|
bottomInset = (self.currentKeyboardHeight + self.voiceInputBarHeight) - chatViewPhysicalBottomSpace;
|
||||||
|
|
||||||
// 确保不会是负数
|
|
||||||
bottomInset = MAX(bottomInset, 0);
|
bottomInset = MAX(bottomInset, 0);
|
||||||
|
|
||||||
NSLog(@"[KBAIHomeVC] 键盘弹起 - bottomInset: %.2f (键盘: %.2f + InputBar: %.2f - 已避开: %.2f)",
|
|
||||||
bottomInset, self.currentKeyboardHeight, self.voiceInputBarHeight, chatViewPhysicalBottomSpace);
|
|
||||||
} else {
|
} else {
|
||||||
// 键盘隐藏时:不需要额外的 bottomInset
|
|
||||||
bottomInset = 0;
|
bottomInset = 0;
|
||||||
NSLog(@"[KBAIHomeVC] 键盘隐藏 - bottomInset: %.2f", bottomInset);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (NSIndexPath *indexPath in self.collectionView.indexPathsForVisibleItems) {
|
for (NSIndexPath *indexPath in self.collectionView.indexPathsForVisibleItems) {
|
||||||
KBPersonaChatCell *cell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
|
KBPersonaChatCell *cell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
|
||||||
if (cell) {
|
if (cell) {
|
||||||
[cell updateChatViewBottomInset:bottomInset];
|
[cell updateChatViewBottomInset:bottomInset];
|
||||||
|
|
||||||
// 键盘弹起时,自动滚动到最后一条消息
|
|
||||||
if (self.currentKeyboardHeight > 0.0) {
|
if (self.currentKeyboardHeight > 0.0) {
|
||||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||||
[cell.chatView scrollToBottom];
|
[cell.chatView scrollToBottom];
|
||||||
@@ -557,6 +591,18 @@
|
|||||||
[pop pop];
|
[pop pop];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma mark - UITextViewDelegate
|
||||||
|
|
||||||
|
- (void)textViewDidChange:(UITextView *)textView {
|
||||||
|
self.placeholderLabel.hidden = textView.text.length > 0;
|
||||||
|
// 动态调整高度
|
||||||
|
CGSize size = [textView sizeThatFits:CGSizeMake(textView.frame.size.width, CGFLOAT_MAX)];
|
||||||
|
CGFloat newHeight = MIN(MAX(size.height, 36), 100);
|
||||||
|
[textView mas_updateConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.height.mas_greaterThanOrEqualTo(newHeight);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - Lazy Load
|
#pragma mark - Lazy Load
|
||||||
|
|
||||||
- (UICollectionView *)collectionView {
|
- (UICollectionView *)collectionView {
|
||||||
@@ -590,6 +636,55 @@
|
|||||||
return _voiceInputBar;
|
return _voiceInputBar;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (UIView *)textInputContainerView {
|
||||||
|
if (!_textInputContainerView) {
|
||||||
|
_textInputContainerView = [[UIView alloc] init];
|
||||||
|
_textInputContainerView.backgroundColor = [UIColor whiteColor];
|
||||||
|
_textInputContainerView.hidden = YES;
|
||||||
|
}
|
||||||
|
return _textInputContainerView;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UITextView *)textInputTextView {
|
||||||
|
if (!_textInputTextView) {
|
||||||
|
_textInputTextView = [[UITextView alloc] init];
|
||||||
|
_textInputTextView.font = [UIFont systemFontOfSize:16];
|
||||||
|
_textInputTextView.textColor = [UIColor blackColor];
|
||||||
|
_textInputTextView.backgroundColor = [UIColor colorWithRed:0.95 green:0.95 blue:0.95 alpha:1.0];
|
||||||
|
_textInputTextView.layer.cornerRadius = 18;
|
||||||
|
_textInputTextView.layer.masksToBounds = YES;
|
||||||
|
_textInputTextView.textContainerInset = UIEdgeInsetsMake(8, 8, 8, 8);
|
||||||
|
_textInputTextView.delegate = self;
|
||||||
|
_textInputTextView.returnKeyType = UIReturnKeySend;
|
||||||
|
_textInputTextView.enablesReturnKeyAutomatically = YES;
|
||||||
|
}
|
||||||
|
return _textInputTextView;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIButton *)sendButton {
|
||||||
|
if (!_sendButton) {
|
||||||
|
_sendButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||||
|
[_sendButton setTitle:KBLocalized(@"发送") forState:UIControlStateNormal];
|
||||||
|
[_sendButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||||
|
_sendButton.titleLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightMedium];
|
||||||
|
_sendButton.backgroundColor = [UIColor colorWithRed:0.2 green:0.6 blue:1.0 alpha:1.0];
|
||||||
|
_sendButton.layer.cornerRadius = 18;
|
||||||
|
_sendButton.layer.masksToBounds = YES;
|
||||||
|
[_sendButton addTarget:self action:@selector(sendButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||||
|
}
|
||||||
|
return _sendButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UILabel *)placeholderLabel {
|
||||||
|
if (!_placeholderLabel) {
|
||||||
|
_placeholderLabel = [[UILabel alloc] init];
|
||||||
|
_placeholderLabel.text = KBLocalized(@"输入消息...");
|
||||||
|
_placeholderLabel.font = [UIFont systemFontOfSize:16];
|
||||||
|
_placeholderLabel.textColor = [UIColor lightGrayColor];
|
||||||
|
}
|
||||||
|
return _placeholderLabel;
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - KBChatLimitPopViewDelegate
|
#pragma mark - KBChatLimitPopViewDelegate
|
||||||
|
|
||||||
- (void)chatLimitPopViewDidTapCancel:(KBChatLimitPopView *)view {
|
- (void)chatLimitPopViewDidTapCancel:(KBChatLimitPopView *)view {
|
||||||
@@ -654,6 +749,24 @@
|
|||||||
[self.navigationController pushViewController:vc animated:YES];
|
[self.navigationController pushViewController:vc animated:YES];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 发送按钮点击 - 直接调用 handleTranscribedText
|
||||||
|
- (void)sendButtonTapped {
|
||||||
|
NSString *text = [self.textInputTextView.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||||
|
if (text.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空输入框
|
||||||
|
self.textInputTextView.text = @"";
|
||||||
|
self.placeholderLabel.hidden = NO;
|
||||||
|
|
||||||
|
// 隐藏键盘和文本输入框
|
||||||
|
[self hideTextInputView];
|
||||||
|
|
||||||
|
// 直接调用 handleTranscribedText,不走语音录制流程
|
||||||
|
[self handleTranscribedText:text];
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - KBVoiceToTextManagerDelegate
|
#pragma mark - KBVoiceToTextManagerDelegate
|
||||||
|
|
||||||
- (void)voiceToTextManager:(KBVoiceToTextManager *)manager
|
- (void)voiceToTextManager:(KBVoiceToTextManager *)manager
|
||||||
@@ -689,7 +802,6 @@
|
|||||||
unsigned long long fileSize = [attributes[NSFileSize] unsignedLongLongValue];
|
unsigned long long fileSize = [attributes[NSFileSize] unsignedLongLongValue];
|
||||||
NSLog(@"[KBAIHomeVC] 录音完成,时长: %.2fs,大小: %llu bytes", duration, fileSize);
|
NSLog(@"[KBAIHomeVC] 录音完成,时长: %.2fs,大小: %llu bytes", duration, fileSize);
|
||||||
|
|
||||||
// 显示 loading 气泡
|
|
||||||
KBPersonaChatCell *currentCell = [self currentPersonaCell];
|
KBPersonaChatCell *currentCell = [self currentPersonaCell];
|
||||||
if (currentCell) {
|
if (currentCell) {
|
||||||
[currentCell appendLoadingUserMessage];
|
[currentCell appendLoadingUserMessage];
|
||||||
@@ -725,12 +837,10 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新 loading 气泡为实际文本
|
|
||||||
if (cell) {
|
if (cell) {
|
||||||
[cell updateLastUserMessage:transcript];
|
[cell updateLastUserMessage:transcript];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送聊天请求(不追加消息,因为已经更新了)
|
|
||||||
[strongSelf handleTranscribedText:transcript appendToUI:NO];
|
[strongSelf handleTranscribedText:transcript appendToUI:NO];
|
||||||
});
|
});
|
||||||
}];
|
}];
|
||||||
@@ -756,7 +866,7 @@
|
|||||||
if (text.length == 0) {
|
if (text.length == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
NSLog(@"[KBAIHomeVC] 语音识别结果:%@", text);
|
NSLog(@"[KBAIHomeVC] 发送消息:%@", text);
|
||||||
|
|
||||||
NSInteger companionId = [self currentCompanionId];
|
NSInteger companionId = [self currentCompanionId];
|
||||||
if (companionId <= 0) {
|
if (companionId <= 0) {
|
||||||
@@ -769,7 +879,6 @@
|
|||||||
[currentCell appendUserMessage:text];
|
[currentCell appendUserMessage:text];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关键修复:发送消息前禁止 CollectionView 滚动
|
|
||||||
self.isWaitingForAIResponse = YES;
|
self.isWaitingForAIResponse = YES;
|
||||||
self.collectionView.scrollEnabled = NO;
|
self.collectionView.scrollEnabled = NO;
|
||||||
NSLog(@"[KBAIHomeVC] 开始等待 AI 回复,禁止 CollectionView 滚动");
|
NSLog(@"[KBAIHomeVC] 开始等待 AI 回复,禁止 CollectionView 滚动");
|
||||||
@@ -784,12 +893,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
// 关键修复:收到响应后(无论成功或失败)重新启用 CollectionView 滚动
|
|
||||||
strongSelf.isWaitingForAIResponse = NO;
|
strongSelf.isWaitingForAIResponse = NO;
|
||||||
strongSelf.collectionView.scrollEnabled = YES;
|
strongSelf.collectionView.scrollEnabled = YES;
|
||||||
NSLog(@"[KBAIHomeVC] AI 回复完成,恢复 CollectionView 滚动");
|
NSLog(@"[KBAIHomeVC] AI 回复完成,恢复 CollectionView 滚动");
|
||||||
|
|
||||||
// 收到响应后,隐藏用户消息的 loading
|
|
||||||
KBPersonaChatCell *cell = [strongSelf currentPersonaCell];
|
KBPersonaChatCell *cell = [strongSelf currentPersonaCell];
|
||||||
if (cell) {
|
if (cell) {
|
||||||
[cell markLastUserMessageLoadingComplete];
|
[cell markLastUserMessageLoadingComplete];
|
||||||
|
|||||||
Reference in New Issue
Block a user