2026-01-26 18:17:02 +08:00
|
|
|
|
//
|
|
|
|
|
|
// KBVoiceInputBar.m
|
|
|
|
|
|
// keyBoard
|
|
|
|
|
|
//
|
|
|
|
|
|
// Created by Kiro on 2026/1/26.
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
|
|
#import "KBVoiceInputBar.h"
|
|
|
|
|
|
#import "KBAiRecordButton.h"
|
|
|
|
|
|
#import <Masonry/Masonry.h>
|
|
|
|
|
|
|
|
|
|
|
|
@interface KBVoiceInputBar () <KBAiRecordButtonDelegate>
|
|
|
|
|
|
|
|
|
|
|
|
/// 状态标签
|
|
|
|
|
|
@property (nonatomic, strong) UILabel *statusLabel;
|
|
|
|
|
|
|
|
|
|
|
|
/// 录音按钮
|
|
|
|
|
|
@property (nonatomic, strong) KBAiRecordButton *recordButton;
|
|
|
|
|
|
|
2026-01-27 16:28:17 +08:00
|
|
|
|
/// 输入区域容器
|
|
|
|
|
|
@property (nonatomic, strong) UIView *inputContainer;
|
|
|
|
|
|
|
|
|
|
|
|
/// 文字输入视图
|
|
|
|
|
|
@property (nonatomic, strong) UIView *textInputView;
|
|
|
|
|
|
@property (nonatomic, strong) UIButton *textCenterButton;
|
|
|
|
|
|
|
|
|
|
|
|
/// 语音输入视图
|
|
|
|
|
|
@property (nonatomic, strong) UIView *voiceInputView;
|
|
|
|
|
|
@property (nonatomic, strong) UILabel *voiceCenterLabel;
|
|
|
|
|
|
|
|
|
|
|
|
/// 左侧切换按钮(麦克风/键盘共用)
|
|
|
|
|
|
@property (nonatomic, strong) UIButton *toggleIconButton;
|
|
|
|
|
|
|
|
|
|
|
|
/// 录音中视图
|
|
|
|
|
|
@property (nonatomic, strong) UIView *recordingView;
|
|
|
|
|
|
@property (nonatomic, strong) UIImageView *recordingCenterIconView;
|
|
|
|
|
|
|
|
|
|
|
|
/// 取消视图
|
|
|
|
|
|
@property (nonatomic, strong) UIView *cancelView;
|
|
|
|
|
|
@property (nonatomic, strong) UILabel *cancelLabel;
|
|
|
|
|
|
|
|
|
|
|
|
/// 隐藏输入框(用于弹起键盘)
|
|
|
|
|
|
@property (nonatomic, strong) UITextField *hiddenTextField;
|
|
|
|
|
|
|
2026-01-26 18:17:02 +08:00
|
|
|
|
/// 是否正在录音
|
|
|
|
|
|
@property (nonatomic, assign) BOOL isRecording;
|
|
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
|
|
@implementation KBVoiceInputBar
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - Lifecycle
|
|
|
|
|
|
|
|
|
|
|
|
- (instancetype)initWithFrame:(CGRect)frame {
|
|
|
|
|
|
if (self = [super initWithFrame:frame]) {
|
|
|
|
|
|
[self setupUI];
|
|
|
|
|
|
}
|
|
|
|
|
|
return self;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (instancetype)initWithCoder:(NSCoder *)coder {
|
|
|
|
|
|
if (self = [super initWithCoder:coder]) {
|
|
|
|
|
|
[self setupUI];
|
|
|
|
|
|
}
|
|
|
|
|
|
return self;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - 1:控件初始化
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setupUI {
|
|
|
|
|
|
self.backgroundColor = [UIColor clearColor];
|
|
|
|
|
|
self.enabled = YES;
|
|
|
|
|
|
self.isRecording = NO;
|
2026-01-27 16:28:17 +08:00
|
|
|
|
self.inputMode = KBVoiceInputBarModeVoice;
|
|
|
|
|
|
self.inputState = KBVoiceInputBarStateText;
|
2026-01-26 18:17:02 +08:00
|
|
|
|
|
|
|
|
|
|
// 状态标签
|
|
|
|
|
|
[self addSubview:self.statusLabel];
|
|
|
|
|
|
[self.statusLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.top.equalTo(self).offset(16);
|
|
|
|
|
|
make.left.equalTo(self).offset(20);
|
|
|
|
|
|
make.right.equalTo(self).offset(-20);
|
|
|
|
|
|
make.height.mas_equalTo(20);
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
2026-01-27 16:28:17 +08:00
|
|
|
|
// 输入区域容器
|
|
|
|
|
|
[self addSubview:self.inputContainer];
|
|
|
|
|
|
[self.inputContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
2026-01-28 17:21:19 +08:00
|
|
|
|
// make.top.equalTo(self.statusLabel.mas_bottom).offset(12);
|
2026-01-26 18:17:02 +08:00
|
|
|
|
make.left.equalTo(self).offset(20);
|
|
|
|
|
|
make.right.equalTo(self).offset(-20);
|
|
|
|
|
|
make.height.mas_equalTo(50);
|
2026-01-28 17:21:19 +08:00
|
|
|
|
make.bottom.lessThanOrEqualTo(self).offset(-10);
|
2026-01-26 18:17:02 +08:00
|
|
|
|
}];
|
|
|
|
|
|
|
2026-01-27 16:28:17 +08:00
|
|
|
|
UILongPressGestureRecognizer *longPress =
|
|
|
|
|
|
[[UILongPressGestureRecognizer alloc] initWithTarget:self
|
|
|
|
|
|
action:@selector(handleVoiceLongPress:)];
|
|
|
|
|
|
longPress.minimumPressDuration = 0.05;
|
|
|
|
|
|
longPress.cancelsTouchesInView = NO;
|
|
|
|
|
|
[self.inputContainer addGestureRecognizer:longPress];
|
2026-01-26 18:17:02 +08:00
|
|
|
|
|
2026-01-27 16:28:17 +08:00
|
|
|
|
// 文字输入视图
|
|
|
|
|
|
[self.inputContainer addSubview:self.textInputView];
|
|
|
|
|
|
[self.textInputView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.edges.equalTo(self.inputContainer);
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
|
|
[self.inputContainer addSubview:self.toggleIconButton];
|
|
|
|
|
|
[self.toggleIconButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.left.equalTo(self.textInputView).offset(16);
|
|
|
|
|
|
make.centerY.equalTo(self.textInputView);
|
|
|
|
|
|
make.width.height.mas_equalTo(24);
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
|
|
[self.textInputView addSubview:self.textCenterButton];
|
|
|
|
|
|
[self.textCenterButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.left.equalTo(self.toggleIconButton.mas_right).offset(12);
|
|
|
|
|
|
make.right.equalTo(self.textInputView).offset(-16);
|
|
|
|
|
|
make.centerY.equalTo(self.textInputView);
|
|
|
|
|
|
make.height.mas_equalTo(30);
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
|
|
// 语音输入视图
|
|
|
|
|
|
[self.inputContainer addSubview:self.voiceInputView];
|
|
|
|
|
|
[self.voiceInputView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.edges.equalTo(self.inputContainer);
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
|
|
[self.voiceInputView addSubview:self.voiceCenterLabel];
|
|
|
|
|
|
[self.voiceCenterLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.left.equalTo(self.toggleIconButton.mas_right).offset(12);
|
|
|
|
|
|
make.right.equalTo(self.voiceInputView).offset(-16);
|
|
|
|
|
|
make.centerY.equalTo(self.voiceInputView);
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
|
|
// 录音中视图
|
|
|
|
|
|
[self.inputContainer addSubview:self.recordingView];
|
|
|
|
|
|
[self.recordingView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.edges.equalTo(self.inputContainer);
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
|
|
[self.recordingView addSubview:self.recordingCenterIconView];
|
|
|
|
|
|
[self.recordingCenterIconView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.center.equalTo(self.recordingView);
|
|
|
|
|
|
make.width.height.mas_equalTo(36);
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
|
|
// 取消视图
|
|
|
|
|
|
[self.inputContainer addSubview:self.cancelView];
|
|
|
|
|
|
[self.cancelView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.edges.equalTo(self.inputContainer);
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
|
|
[self.cancelView addSubview:self.cancelLabel];
|
|
|
|
|
|
[self.cancelLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.center.equalTo(self.cancelView);
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
|
|
// 隐藏输入框(仅用于弹起键盘)
|
|
|
|
|
|
[self addSubview:self.hiddenTextField];
|
|
|
|
|
|
[self.hiddenTextField mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.width.height.mas_equalTo(1);
|
|
|
|
|
|
make.left.top.equalTo(self);
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
|
|
// 隐藏旧的录音按钮和状态标签
|
|
|
|
|
|
self.statusLabel.hidden = YES;
|
|
|
|
|
|
|
|
|
|
|
|
// 统一设置初始状态
|
|
|
|
|
|
self.inputState = KBVoiceInputBarStateText;
|
2026-01-26 18:17:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - Setter
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setStatusText:(NSString *)statusText {
|
|
|
|
|
|
_statusText = [statusText copy];
|
|
|
|
|
|
self.statusLabel.text = statusText;
|
2026-01-27 16:28:17 +08:00
|
|
|
|
[self updateCenterTextIfNeeded];
|
2026-01-26 18:17:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setEnabled:(BOOL)enabled {
|
|
|
|
|
|
_enabled = enabled;
|
|
|
|
|
|
self.recordButton.userInteractionEnabled = enabled;
|
|
|
|
|
|
self.recordButton.alpha = enabled ? 1.0 : 0.5;
|
2026-01-27 16:28:17 +08:00
|
|
|
|
self.inputContainer.userInteractionEnabled = enabled;
|
|
|
|
|
|
self.inputContainer.alpha = enabled ? 1.0 : 0.5;
|
2026-01-26 18:17:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setRecording:(BOOL)recording {
|
|
|
|
|
|
_isRecording = recording;
|
|
|
|
|
|
self.recordButton.state = recording ? KBAiRecordButtonStateRecording : KBAiRecordButtonStateNormal;
|
2026-01-27 16:28:17 +08:00
|
|
|
|
if (recording) {
|
|
|
|
|
|
self.inputState = KBVoiceInputBarStateRecording;
|
|
|
|
|
|
} else if (self.inputState == KBVoiceInputBarStateRecording) {
|
|
|
|
|
|
self.inputState = KBVoiceInputBarStateVoice;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setInputMode:(KBVoiceInputBarMode)inputMode {
|
|
|
|
|
|
_inputMode = inputMode;
|
|
|
|
|
|
if (inputMode == KBVoiceInputBarModeText) {
|
|
|
|
|
|
[self.toggleIconButton setImage:[UIImage imageNamed:@"ai_maikefeng_icon"]
|
|
|
|
|
|
forState:UIControlStateNormal];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
[self.toggleIconButton setImage:[UIImage imageNamed:@"ai_jianpan_icon"]
|
|
|
|
|
|
forState:UIControlStateNormal];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setInputState:(KBVoiceInputBarState)inputState {
|
|
|
|
|
|
_inputState = inputState;
|
|
|
|
|
|
self.textInputView.hidden = (inputState != KBVoiceInputBarStateText);
|
|
|
|
|
|
self.voiceInputView.hidden = (inputState != KBVoiceInputBarStateVoice);
|
|
|
|
|
|
self.recordingView.hidden = (inputState != KBVoiceInputBarStateRecording);
|
|
|
|
|
|
self.cancelView.hidden = (inputState != KBVoiceInputBarStateCancel);
|
|
|
|
|
|
self.toggleIconButton.hidden = (inputState == KBVoiceInputBarStateRecording ||
|
|
|
|
|
|
inputState == KBVoiceInputBarStateCancel);
|
|
|
|
|
|
if (inputState == KBVoiceInputBarStateText) {
|
|
|
|
|
|
self.inputMode = KBVoiceInputBarModeText;
|
|
|
|
|
|
} else if (inputState == KBVoiceInputBarStateVoice) {
|
|
|
|
|
|
self.inputMode = KBVoiceInputBarModeVoice;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!self.toggleIconButton.hidden) {
|
|
|
|
|
|
[self.inputContainer bringSubviewToFront:self.toggleIconButton];
|
|
|
|
|
|
}
|
|
|
|
|
|
[self updateCenterTextIfNeeded];
|
2026-01-26 18:17:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - Public Methods
|
|
|
|
|
|
|
|
|
|
|
|
- (void)updateVolumeRMS:(float)rms {
|
|
|
|
|
|
[self.recordButton updateVolumeRMS:rms];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - KBAiRecordButtonDelegate
|
|
|
|
|
|
|
|
|
|
|
|
- (void)recordButtonDidBeginPress:(KBAiRecordButton *)button {
|
|
|
|
|
|
if (!self.enabled) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.isRecording = YES;
|
|
|
|
|
|
|
|
|
|
|
|
if ([self.delegate respondsToSelector:@selector(voiceInputBarDidBeginRecording:)]) {
|
|
|
|
|
|
[self.delegate voiceInputBarDidBeginRecording:self];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)recordButtonDidEndPress:(KBAiRecordButton *)button {
|
|
|
|
|
|
self.isRecording = NO;
|
|
|
|
|
|
|
|
|
|
|
|
if ([self.delegate respondsToSelector:@selector(voiceInputBarDidEndRecording:)]) {
|
|
|
|
|
|
[self.delegate voiceInputBarDidEndRecording:self];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)recordButtonDidCancelPress:(KBAiRecordButton *)button {
|
|
|
|
|
|
self.isRecording = NO;
|
|
|
|
|
|
|
|
|
|
|
|
if ([self.delegate respondsToSelector:@selector(voiceInputBarDidCancelRecording:)]) {
|
|
|
|
|
|
[self.delegate voiceInputBarDidCancelRecording:self];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - Lazy Load
|
|
|
|
|
|
|
|
|
|
|
|
- (UILabel *)statusLabel {
|
|
|
|
|
|
if (!_statusLabel) {
|
|
|
|
|
|
_statusLabel = [[UILabel alloc] init];
|
|
|
|
|
|
_statusLabel.text = @"按住按钮开始对话";
|
|
|
|
|
|
_statusLabel.font = [UIFont systemFontOfSize:14];
|
|
|
|
|
|
_statusLabel.textColor = [UIColor secondaryLabelColor];
|
|
|
|
|
|
_statusLabel.textAlignment = NSTextAlignmentCenter;
|
|
|
|
|
|
}
|
|
|
|
|
|
return _statusLabel;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (KBAiRecordButton *)recordButton {
|
|
|
|
|
|
if (!_recordButton) {
|
|
|
|
|
|
_recordButton = [[KBAiRecordButton alloc] init];
|
|
|
|
|
|
_recordButton.delegate = self;
|
|
|
|
|
|
_recordButton.normalTitle = @"按住说话";
|
|
|
|
|
|
_recordButton.recordingTitle = @"松开结束";
|
2026-01-27 16:28:17 +08:00
|
|
|
|
_recordButton.normalIconImage = [UIImage imageNamed:@"ai_jianpan_icon"];
|
|
|
|
|
|
_recordButton.recordingIconImage = [UIImage imageNamed:@"ai_luyining_icon"];
|
|
|
|
|
|
_recordButton.hidden = YES;
|
2026-01-26 18:17:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
return _recordButton;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 16:28:17 +08:00
|
|
|
|
- (UIView *)inputContainer {
|
|
|
|
|
|
if (!_inputContainer) {
|
|
|
|
|
|
_inputContainer = [[UIView alloc] init];
|
|
|
|
|
|
_inputContainer.clipsToBounds = YES;
|
|
|
|
|
|
_inputContainer.layer.cornerRadius = 25;
|
|
|
|
|
|
_inputContainer.backgroundColor = [UIColor clearColor];
|
|
|
|
|
|
}
|
|
|
|
|
|
return _inputContainer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UIView *)textInputView {
|
|
|
|
|
|
if (!_textInputView) {
|
|
|
|
|
|
_textInputView = [[UIView alloc] init];
|
|
|
|
|
|
_textInputView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.7];
|
|
|
|
|
|
}
|
|
|
|
|
|
return _textInputView;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UIButton *)textCenterButton {
|
|
|
|
|
|
if (!_textCenterButton) {
|
|
|
|
|
|
_textCenterButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
|
|
|
|
|
[_textCenterButton setTitle:@"发送一个消息给她" forState:UIControlStateNormal];
|
|
|
|
|
|
_textCenterButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
|
|
|
|
|
|
[_textCenterButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
|
|
|
|
|
_textCenterButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
|
|
|
|
|
|
[_textCenterButton addTarget:self
|
|
|
|
|
|
action:@selector(handleTextCenterTap)
|
|
|
|
|
|
forControlEvents:UIControlEventTouchUpInside];
|
|
|
|
|
|
}
|
|
|
|
|
|
return _textCenterButton;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UIView *)voiceInputView {
|
|
|
|
|
|
if (!_voiceInputView) {
|
|
|
|
|
|
_voiceInputView = [[UIView alloc] init];
|
|
|
|
|
|
_voiceInputView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.7];
|
|
|
|
|
|
}
|
|
|
|
|
|
return _voiceInputView;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UILabel *)voiceCenterLabel {
|
|
|
|
|
|
if (!_voiceCenterLabel) {
|
|
|
|
|
|
_voiceCenterLabel = [[UILabel alloc] init];
|
|
|
|
|
|
_voiceCenterLabel.text = @"按住说话";
|
|
|
|
|
|
_voiceCenterLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
|
|
|
|
|
|
_voiceCenterLabel.textColor = [UIColor whiteColor];
|
|
|
|
|
|
_voiceCenterLabel.textAlignment = NSTextAlignmentCenter;
|
|
|
|
|
|
}
|
|
|
|
|
|
return _voiceCenterLabel;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UIButton *)toggleIconButton {
|
|
|
|
|
|
if (!_toggleIconButton) {
|
|
|
|
|
|
_toggleIconButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
|
|
|
|
|
[_toggleIconButton setImage:[UIImage imageNamed:@"ai_maikefeng_icon"]
|
|
|
|
|
|
forState:UIControlStateNormal];
|
|
|
|
|
|
[_toggleIconButton addTarget:self
|
|
|
|
|
|
action:@selector(handleToggleIconTap)
|
|
|
|
|
|
forControlEvents:UIControlEventTouchUpInside];
|
|
|
|
|
|
_toggleIconButton.exclusiveTouch = YES;
|
|
|
|
|
|
}
|
|
|
|
|
|
return _toggleIconButton;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UIView *)recordingView {
|
|
|
|
|
|
if (!_recordingView) {
|
|
|
|
|
|
_recordingView = [[UIView alloc] init];
|
|
|
|
|
|
_recordingView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.6];
|
|
|
|
|
|
}
|
|
|
|
|
|
return _recordingView;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UIImageView *)recordingCenterIconView {
|
|
|
|
|
|
if (!_recordingCenterIconView) {
|
|
|
|
|
|
_recordingCenterIconView = [[UIImageView alloc] init];
|
|
|
|
|
|
_recordingCenterIconView.image = [UIImage imageNamed:@"ai_luyining_icon"];
|
|
|
|
|
|
_recordingCenterIconView.contentMode = UIViewContentModeScaleAspectFit;
|
|
|
|
|
|
}
|
|
|
|
|
|
return _recordingCenterIconView;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UIView *)cancelView {
|
|
|
|
|
|
if (!_cancelView) {
|
|
|
|
|
|
_cancelView = [[UIView alloc] init];
|
|
|
|
|
|
_cancelView.backgroundColor = [UIColor colorWithRed:0.75 green:0.3 blue:0.3 alpha:1.0];
|
|
|
|
|
|
}
|
|
|
|
|
|
return _cancelView;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UILabel *)cancelLabel {
|
|
|
|
|
|
if (!_cancelLabel) {
|
|
|
|
|
|
_cancelLabel = [[UILabel alloc] init];
|
|
|
|
|
|
_cancelLabel.text = @"Release To Cancel";
|
|
|
|
|
|
_cancelLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
|
|
|
|
|
|
_cancelLabel.textColor = [UIColor whiteColor];
|
|
|
|
|
|
}
|
|
|
|
|
|
return _cancelLabel;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (UITextField *)hiddenTextField {
|
|
|
|
|
|
if (!_hiddenTextField) {
|
|
|
|
|
|
_hiddenTextField = [[UITextField alloc] init];
|
|
|
|
|
|
_hiddenTextField.hidden = YES;
|
|
|
|
|
|
}
|
|
|
|
|
|
return _hiddenTextField;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - Actions
|
|
|
|
|
|
|
|
|
|
|
|
- (void)handleToggleIconTap {
|
|
|
|
|
|
if (self.inputState == KBVoiceInputBarStateText) {
|
|
|
|
|
|
self.inputState = KBVoiceInputBarStateVoice;
|
|
|
|
|
|
[self endEditing:YES];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.inputState = KBVoiceInputBarStateText;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)handleTextCenterTap {
|
2026-01-29 17:56:53 +08:00
|
|
|
|
// 文本模式点击时,触发回调让 VC 显示文本输入框
|
|
|
|
|
|
if (self.onTextSend) {
|
|
|
|
|
|
self.onTextSend(nil);
|
|
|
|
|
|
}
|
2026-01-27 16:28:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)handleVoiceLongPress:(UILongPressGestureRecognizer *)gesture {
|
|
|
|
|
|
if (self.inputState != KBVoiceInputBarStateVoice &&
|
|
|
|
|
|
self.inputState != KBVoiceInputBarStateRecording &&
|
|
|
|
|
|
self.inputState != KBVoiceInputBarStateCancel) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
CGPoint location = [gesture locationInView:self.inputContainer];
|
|
|
|
|
|
BOOL isInside = CGRectContainsPoint(self.inputContainer.bounds, location);
|
|
|
|
|
|
CGPoint iconPoint = [gesture locationInView:self.toggleIconButton];
|
|
|
|
|
|
BOOL isOnToggleIcon = CGRectContainsPoint(self.toggleIconButton.bounds, iconPoint);
|
|
|
|
|
|
if (isOnToggleIcon) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
switch (gesture.state) {
|
|
|
|
|
|
case UIGestureRecognizerStateBegan: {
|
|
|
|
|
|
if (self.inputState != KBVoiceInputBarStateVoice) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.inputState = KBVoiceInputBarStateRecording;
|
|
|
|
|
|
if ([self.delegate respondsToSelector:@selector(voiceInputBarDidBeginRecording:)]) {
|
|
|
|
|
|
[self.delegate voiceInputBarDidBeginRecording:self];
|
|
|
|
|
|
}
|
|
|
|
|
|
} break;
|
|
|
|
|
|
case UIGestureRecognizerStateChanged: {
|
|
|
|
|
|
if (isInside) {
|
|
|
|
|
|
self.inputState = KBVoiceInputBarStateRecording;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.inputState = KBVoiceInputBarStateCancel;
|
|
|
|
|
|
}
|
|
|
|
|
|
} break;
|
|
|
|
|
|
case UIGestureRecognizerStateEnded: {
|
|
|
|
|
|
if (isInside) {
|
|
|
|
|
|
if ([self.delegate respondsToSelector:@selector(voiceInputBarDidEndRecording:)]) {
|
|
|
|
|
|
[self.delegate voiceInputBarDidEndRecording:self];
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if ([self.delegate respondsToSelector:@selector(voiceInputBarDidCancelRecording:)]) {
|
|
|
|
|
|
[self.delegate voiceInputBarDidCancelRecording:self];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.inputState = KBVoiceInputBarStateVoice;
|
|
|
|
|
|
} break;
|
|
|
|
|
|
case UIGestureRecognizerStateCancelled:
|
|
|
|
|
|
case UIGestureRecognizerStateFailed: {
|
|
|
|
|
|
if ([self.delegate respondsToSelector:@selector(voiceInputBarDidCancelRecording:)]) {
|
|
|
|
|
|
[self.delegate voiceInputBarDidCancelRecording:self];
|
|
|
|
|
|
}
|
|
|
|
|
|
self.inputState = KBVoiceInputBarStateVoice;
|
|
|
|
|
|
} break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)updateCenterTextIfNeeded {
|
|
|
|
|
|
if (self.inputState == KBVoiceInputBarStateText) {
|
|
|
|
|
|
[self.textCenterButton setTitle:@"发送一个消息给她" forState:UIControlStateNormal];
|
|
|
|
|
|
} else if (self.inputState == KBVoiceInputBarStateVoice) {
|
|
|
|
|
|
self.voiceCenterLabel.text = @"按住说话";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 18:17:02 +08:00
|
|
|
|
@end
|