Files
keyboard/keyBoard/Class/AiTalk/V/KBChatAssistantMessageCell.m
2026-01-23 21:51:37 +08:00

340 lines
14 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// KBChatAssistantMessageCell.m
// keyBoard
//
// Created by Kiro on 2026/1/23.
//
#import "KBChatAssistantMessageCell.h"
#import "KBAiChatMessage.h"
#import <Masonry/Masonry.h>
@interface KBChatAssistantMessageCell ()
@property (nonatomic, strong) UIButton *voiceButton;
@property (nonatomic, strong) UILabel *durationLabel;
@property (nonatomic, strong) UIView *bubbleView;
@property (nonatomic, strong, readwrite) UILabel *messageLabel; // readwrite 允许内部修改
@property (nonatomic, strong) UIActivityIndicatorView *loadingIndicator;
@property (nonatomic, strong) KBAiChatMessage *currentMessage;
// 打字机效果
@property (nonatomic, strong) NSTimer *typewriterTimer;
@property (nonatomic, copy) NSString *fullText;
@property (nonatomic, assign) NSInteger currentCharIndex;
@end
@implementation KBChatAssistantMessageCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style
reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.backgroundColor = [UIColor clearColor];
self.selectionStyle = UITableViewCellSelectionStyleNone;
[self setupUI];
}
return self;
}
- (void)setupUI {
// 语音按钮
self.voiceButton = [UIButton buttonWithType:UIButtonTypeCustom];
[self.voiceButton setImage:[UIImage systemImageNamed:@"play.circle.fill"]
forState:UIControlStateNormal];
self.voiceButton.tintColor = [UIColor whiteColor];
[self.voiceButton addTarget:self
action:@selector(voiceButtonTapped)
forControlEvents:UIControlEventTouchUpInside];
[self.contentView addSubview:self.voiceButton];
// 语音时长标签
self.durationLabel = [[UILabel alloc] init];
self.durationLabel.font = [UIFont systemFontOfSize:14];
self.durationLabel.textColor = [UIColor whiteColor];
[self.contentView addSubview:self.durationLabel];
// 加载指示器
self.loadingIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium];
self.loadingIndicator.color = [UIColor whiteColor];
self.loadingIndicator.hidesWhenStopped = YES;
[self.contentView addSubview:self.loadingIndicator];
// 气泡视图
self.bubbleView = [[UIView alloc] init];
self.bubbleView.backgroundColor = [UIColor colorWithRed:0.2 green:0.2 blue:0.2 alpha:0.7];
self.bubbleView.layer.cornerRadius = 16;
self.bubbleView.layer.masksToBounds = YES;
[self.contentView addSubview:self.bubbleView];
// 消息标签
self.messageLabel = [[UILabel alloc] init];
self.messageLabel.numberOfLines = 0;
self.messageLabel.font = [UIFont systemFontOfSize:16];
self.messageLabel.textColor = [UIColor whiteColor];
// 设置 preferredMaxLayoutWidth 让 AutoLayout 能正确计算多行高度
CGFloat maxWidth = [UIScreen mainScreen].bounds.size.width * 0.75 - 16 - 24;
self.messageLabel.preferredMaxLayoutWidth = maxWidth;
[self.bubbleView addSubview:self.messageLabel];
// 布局约束
[self.voiceButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.contentView).offset(16);
make.top.equalTo(self.contentView).offset(8);
make.width.height.mas_equalTo(24);
}];
[self.durationLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.voiceButton.mas_right).offset(4);
make.centerY.equalTo(self.voiceButton);
}];
[self.loadingIndicator mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.voiceButton);
}];
// bubbleView 约束
[self.bubbleView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.voiceButton.mas_bottom).offset(4);
make.bottom.equalTo(self.contentView).offset(-4);
make.left.equalTo(self.contentView).offset(16);
make.width.lessThanOrEqualTo(self.contentView).multipliedBy(0.75);
}];
// messageLabel 约束
[self.messageLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.bubbleView).offset(10);
make.bottom.equalTo(self.bubbleView).offset(-10);
make.left.equalTo(self.bubbleView).offset(12);
make.right.equalTo(self.bubbleView).offset(-12);
}];
}
- (void)configureWithMessage:(KBAiChatMessage *)message {
NSLog(@"[KBChatAssistantMessageCell] 配置消息 - 文本长度: %lu, isComplete: %d, needsTypewriter: %d, 打字机运行中: %d",
(unsigned long)message.text.length, message.isComplete, message.needsTypewriterEffect,
(self.typewriterTimer && self.typewriterTimer.isValid));
// 先停止之前的打字机效果(无论是否是同一条消息)
[self stopTypewriterEffect];
self.currentMessage = message;
// 只有明确需要打字机效果的消息才使用打字机
if (message.needsTypewriterEffect && !message.isComplete && message.text.length > 0) {
// 启动新的打字机效果
NSLog(@"[KBChatAssistantMessageCell] 启动新的打字机效果");
[self startTypewriterEffectWithText:message.text];
} else {
// 直接显示完整文本
NSLog(@"[KBChatAssistantMessageCell] 直接显示完整文本needsTypewriter: %d, isComplete: %d",
message.needsTypewriterEffect, message.isComplete);
self.messageLabel.text = message.text;
}
// 格式化语音时长(如果时长为 0不显示
if (message.audioDuration > 0) {
NSInteger seconds = (NSInteger)ceil(message.audioDuration);
self.durationLabel.text = [NSString stringWithFormat:@"%ld\"", (long)seconds];
} else {
self.durationLabel.text = @"";
}
// 如果有 audioId 或 audioData显示语音按钮
BOOL hasAudio = (message.audioId.length > 0) ||
(message.audioData != nil && message.audioData.length > 0);
self.voiceButton.hidden = !hasAudio;
self.durationLabel.hidden = !hasAudio;
}
- (void)updateVoicePlayingState:(BOOL)isPlaying {
if (isPlaying) {
[self.voiceButton setImage:[UIImage systemImageNamed:@"pause.circle.fill"]
forState:UIControlStateNormal];
} else {
[self.voiceButton setImage:[UIImage systemImageNamed:@"play.circle.fill"]
forState:UIControlStateNormal];
}
}
- (void)showLoadingAnimation {
// 隐藏按钮图标,显示加载动画
[self.voiceButton setImage:nil forState:UIControlStateNormal];
[self.loadingIndicator startAnimating];
}
- (void)hideLoadingAnimation {
// 停止加载动画,恢复按钮图标
[self.loadingIndicator stopAnimating];
[self.voiceButton setImage:[UIImage systemImageNamed:@"play.circle.fill"]
forState:UIControlStateNormal];
}
- (void)voiceButtonTapped {
if ([self.delegate respondsToSelector:@selector(assistantMessageCell:didTapVoiceButtonForMessage:)]) {
[self.delegate assistantMessageCell:self didTapVoiceButtonForMessage:self.currentMessage];
}
}
#pragma mark - Typewriter Effect
- (void)startTypewriterEffectWithText:(NSString *)text {
if (text.length == 0) {
NSLog(@"[KBChatAssistantMessageCell] 文本为空,跳过打字机效果");
return;
}
NSLog(@"[KBChatAssistantMessageCell] 开始打字机效果,文本长度: %lu, 文本: %@",
(unsigned long)text.length, text);
self.fullText = text;
self.currentCharIndex = 0;
// 先设置完整文本,让布局系统计算出正确的高度
self.messageLabel.text = text;
// 强制布局更新
[self.messageLabel setNeedsLayout];
[self.bubbleView setNeedsLayout];
[self.contentView setNeedsLayout];
[self layoutIfNeeded];
NSLog(@"[KBChatAssistantMessageCell] 布局后 Label frame: %@", NSStringFromCGRect(self.messageLabel.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;
// 确保在主线程创建定时器
dispatch_async(dispatch_get_main_queue(), ^{
// 每 0.03 秒显示一个字符(更快的打字机效果)
self.typewriterTimer = [NSTimer scheduledTimerWithTimeInterval:0.03
target:self
selector:@selector(typewriterTick)
userInfo:nil
repeats:YES];
// 将定时器添加到 RunLoop 的 common 模式,确保滚动时也能触发
[[NSRunLoop currentRunLoop] addTimer:self.typewriterTimer forMode:NSRunLoopCommonModes];
// 立即触发一次,显示第一个字符
[self typewriterTick];
NSLog(@"[KBChatAssistantMessageCell] 定时器已创建: %@", self.typewriterTimer);
});
}
- (void)typewriterTick {
// 使用局部变量保存 fullText避免多线程问题
NSString *text = self.fullText;
// 检查 fullText 是否有效
if (!text || text.length == 0) {
NSLog(@"[KBChatAssistantMessageCell] fullText 无效,停止打字机");
[self stopTypewriterEffect];
return;
}
// 如果当前索引小于完整文本长度,继续显示
if (self.currentCharIndex < text.length) {
self.currentCharIndex++;
// 使用 NSAttributedString 显示前 N 个字符,隐藏后面的字符
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:text];
// 已显示的部分:白色
if (self.currentCharIndex > 0) {
[attributedText addAttribute:NSForegroundColorAttributeName
value:[UIColor whiteColor]
range:NSMakeRange(0, self.currentCharIndex)];
}
// 未显示的部分:透明
if (self.currentCharIndex < text.length) {
[attributedText addAttribute:NSForegroundColorAttributeName
value:[UIColor clearColor]
range:NSMakeRange(self.currentCharIndex, text.length - self.currentCharIndex)];
}
// 设置字体
[attributedText addAttribute:NSFontAttributeName
value:self.messageLabel.font
range:NSMakeRange(0, text.length)];
self.messageLabel.attributedText = attributedText;
// 每 10 个字符打印一次日志
if (self.currentCharIndex % 10 == 0) {
NSString *displayText = [text substringToIndex:self.currentCharIndex];
NSLog(@"[KBChatAssistantMessageCell] 打字机进度: %ld/%lu, 当前文本: %@",
(long)self.currentCharIndex, (unsigned long)text.length, displayText);
NSLog(@"[KBChatAssistantMessageCell] Label 状态 - frame: %@, hidden: %d, alpha: %.2f",
NSStringFromCGRect(self.messageLabel.frame),
self.messageLabel.hidden,
self.messageLabel.alpha);
}
} else {
// 打字机效果已完成,停止定时器并标记消息完成
NSLog(@"[KBChatAssistantMessageCell] 打字机效果完成,停止定时器");
// 检查 text 是否为空
if (!text || text.length == 0) {
NSLog(@"[KBChatAssistantMessageCell] text 为空,跳过");
[self stopTypewriterEffect];
return;
}
[self stopTypewriterEffect];
// 显示完整文本(白色),使用 attributedText 保持布局
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:text];
[attributedText addAttribute:NSForegroundColorAttributeName
value:[UIColor whiteColor]
range:NSMakeRange(0, text.length)];
[attributedText addAttribute:NSFontAttributeName
value:self.messageLabel.font
range:NSMakeRange(0, text.length)];
self.messageLabel.attributedText = attributedText;
// 标记消息为完成状态,避免 Cell 复用时重新触发打字机
if (self.currentMessage) {
self.currentMessage.isComplete = YES;
self.currentMessage.needsTypewriterEffect = NO;
NSLog(@"[KBChatAssistantMessageCell] 标记消息完成 - 文本: %@", self.currentMessage.text);
}
}
}
- (void)stopTypewriterEffect {
if (self.typewriterTimer && self.typewriterTimer.isValid) {
NSLog(@"[KBChatAssistantMessageCell] 停止打字机定时器");
[self.typewriterTimer invalidate];
}
self.typewriterTimer = nil;
self.currentCharIndex = 0;
self.fullText = nil;
}
- (void)prepareForReuse {
[super prepareForReuse];
[self stopTypewriterEffect];
// 清空文本,避免复用时显示旧内容
self.messageLabel.text = @"";
self.messageLabel.attributedText = nil;
}
- (void)dealloc {
[self stopTypewriterEffect];
}
@end