2025-11-12 16:03:30 +08:00
|
|
|
|
//
|
|
|
|
|
|
// KBStreamOverlayView.m
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
|
|
#import "KBStreamOverlayView.h"
|
|
|
|
|
|
#import "KBStreamTextView.h"
|
|
|
|
|
|
#import "Masonry.h"
|
|
|
|
|
|
|
|
|
|
|
|
@interface KBStreamOverlayView ()
|
|
|
|
|
|
@property (nonatomic, strong) KBStreamTextView *textViewInternal;
|
|
|
|
|
|
@property (nonatomic, strong) UIButton *closeButton;
|
2025-12-09 16:12:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 新增:流式打字机用的缓冲 & 定时器
|
|
|
|
|
|
@property (nonatomic, strong) NSMutableString *pendingText;
|
|
|
|
|
|
@property (nonatomic, strong) NSTimer *streamTimer;
|
|
|
|
|
|
@property (nonatomic, assign) NSInteger charsPerTick; // 每次“跳”几个字符
|
|
|
|
|
|
// 新增:标记 SSE 已经收到 done
|
|
|
|
|
|
@property (nonatomic, assign) BOOL streamDidReceiveDone;
|
2025-11-12 16:03:30 +08:00
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
|
|
@implementation KBStreamOverlayView
|
|
|
|
|
|
|
|
|
|
|
|
- (instancetype)initWithFrame:(CGRect)frame {
|
|
|
|
|
|
if (self = [super initWithFrame:frame]) {
|
|
|
|
|
|
self.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.92];
|
|
|
|
|
|
self.layer.cornerRadius = 12.0; self.layer.masksToBounds = YES;
|
|
|
|
|
|
|
|
|
|
|
|
[self addSubview:self.textViewInternal];
|
|
|
|
|
|
[self addSubview:self.closeButton];
|
|
|
|
|
|
|
|
|
|
|
|
[self.textViewInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.left.equalTo(self.mas_left).offset(0);
|
|
|
|
|
|
make.right.equalTo(self.mas_right).offset(0);
|
|
|
|
|
|
make.bottom.equalTo(self.mas_bottom).offset(0);
|
|
|
|
|
|
make.top.equalTo(self.mas_top).offset(0);
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
|
|
[self.closeButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.top.equalTo(self.mas_top).offset(8);
|
|
|
|
|
|
make.right.equalTo(self.mas_right).offset(-8);
|
|
|
|
|
|
make.height.mas_equalTo(28);
|
|
|
|
|
|
make.width.mas_greaterThanOrEqualTo(56);
|
|
|
|
|
|
}];
|
2025-12-09 16:12:54 +08:00
|
|
|
|
_pendingText = [NSMutableString string];
|
|
|
|
|
|
_charsPerTick = 2; // 每次输出 1~2 个字符,可以自己调
|
|
|
|
|
|
_streamDidReceiveDone = NO;
|
|
|
|
|
|
|
2025-11-12 16:03:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
return self;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (KBStreamTextView *)textViewInternal {
|
|
|
|
|
|
if (!_textViewInternal) {
|
|
|
|
|
|
_textViewInternal = [[KBStreamTextView alloc] init];
|
|
|
|
|
|
}
|
|
|
|
|
|
return _textViewInternal;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UIButton *)closeButton {
|
|
|
|
|
|
if (!_closeButton) {
|
|
|
|
|
|
UIButton *del = [UIButton buttonWithType:UIButtonTypeSystem];
|
|
|
|
|
|
del.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.35];
|
|
|
|
|
|
del.layer.cornerRadius = 14; del.layer.masksToBounds = YES;
|
|
|
|
|
|
del.titleLabel.font = [UIFont systemFontOfSize:13 weight:UIFontWeightSemibold];
|
2025-12-05 21:54:10 +08:00
|
|
|
|
[del setTitle:KBLocalized(@"common_back") forState:UIControlStateNormal];
|
2025-11-12 16:03:30 +08:00
|
|
|
|
[del setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
|
|
|
|
|
[del addTarget:self action:@selector(onTapClose) forControlEvents:UIControlEventTouchUpInside];
|
|
|
|
|
|
_closeButton = del;
|
|
|
|
|
|
}
|
|
|
|
|
|
return _closeButton;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)onTapClose {
|
|
|
|
|
|
if ([self.delegate respondsToSelector:@selector(streamOverlayDidTapClose:)]) {
|
|
|
|
|
|
[self.delegate streamOverlayDidTapClose:self];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)appendChunk:(NSString *)text {
|
|
|
|
|
|
if (text.length == 0) return;
|
2025-12-09 16:12:54 +08:00
|
|
|
|
if (![NSThread isMainThread]) {
|
|
|
|
|
|
__weak typeof(self) weakSelf = self;
|
|
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
|
|
[weakSelf appendChunk:text];
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[self.pendingText appendString:text];
|
|
|
|
|
|
[self startStreamTimerIfNeeded];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)startStreamTimerIfNeeded {
|
|
|
|
|
|
if (self.streamTimer) return;
|
|
|
|
|
|
self.streamTimer = [NSTimer scheduledTimerWithTimeInterval:0.02
|
|
|
|
|
|
target:self
|
|
|
|
|
|
selector:@selector(handleStreamTick)
|
|
|
|
|
|
userInfo:nil
|
|
|
|
|
|
repeats:YES];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)stopStreamTimer {
|
|
|
|
|
|
[self.streamTimer invalidate];
|
|
|
|
|
|
self.streamTimer = nil;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)handleStreamTick {
|
|
|
|
|
|
if (self.pendingText.length == 0) {
|
|
|
|
|
|
// 如果已经收到 done 并且没有待播内容了,这里再真正 finish
|
|
|
|
|
|
if (self.streamDidReceiveDone) {
|
|
|
|
|
|
[self.textViewInternal finishStreaming];
|
|
|
|
|
|
}
|
|
|
|
|
|
[self stopStreamTimer];
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
NSInteger len = MIN(self.charsPerTick, self.pendingText.length);
|
|
|
|
|
|
NSString *slice = [self.pendingText substringToIndex:len];
|
|
|
|
|
|
[self.pendingText deleteCharactersInRange:NSMakeRange(0, len)];
|
|
|
|
|
|
|
|
|
|
|
|
[self.textViewInternal appendStreamText:slice];
|
2025-11-12 16:03:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 16:12:54 +08:00
|
|
|
|
|
2025-11-12 16:03:30 +08:00
|
|
|
|
- (void)finish {
|
2025-12-09 16:12:54 +08:00
|
|
|
|
if (![NSThread isMainThread]) {
|
|
|
|
|
|
__weak typeof(self) weakSelf = self;
|
|
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
|
|
[weakSelf finish];
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 只标记“流已结束”
|
|
|
|
|
|
self.streamDidReceiveDone = YES;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果此时已经没有待播内容了,可以立即结束
|
|
|
|
|
|
if (self.pendingText.length == 0) {
|
|
|
|
|
|
[self stopStreamTimer];
|
|
|
|
|
|
[self.textViewInternal finishStreaming];
|
|
|
|
|
|
}
|
|
|
|
|
|
// 否则等 handleStreamTick 把 pendingText 慢慢播完,
|
|
|
|
|
|
// 它看到 pendingText == 0 且 streamDidReceiveDone == YES 时会自动调用 finishStreaming
|
2025-11-12 16:03:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 16:12:54 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-11-12 16:03:30 +08:00
|
|
|
|
- (KBStreamTextView *)textView { return self.textViewInternal; }
|
|
|
|
|
|
|
|
|
|
|
|
@end
|