// // KBVoiceInputBar.m // keyBoard // // Created by Kiro on 2026/1/26. // #import "KBVoiceInputBar.h" #import "KBAiRecordButton.h" #import @interface KBVoiceInputBar () /// 状态标签 @property (nonatomic, strong) UILabel *statusLabel; /// 录音按钮 @property (nonatomic, strong) KBAiRecordButton *recordButton; /// 输入区域容器 @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; /// 是否正在录音 @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; self.inputMode = KBVoiceInputBarModeVoice; self.inputState = KBVoiceInputBarStateText; // 状态标签 [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); }]; // 输入区域容器 [self addSubview:self.inputContainer]; [self.inputContainer mas_makeConstraints:^(MASConstraintMaker *make) { // make.top.equalTo(self.statusLabel.mas_bottom).offset(12); make.left.equalTo(self).offset(20); make.right.equalTo(self).offset(-20); make.height.mas_equalTo(50); make.bottom.lessThanOrEqualTo(self).offset(-10); }]; UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleVoiceLongPress:)]; longPress.minimumPressDuration = 0.05; longPress.cancelsTouchesInView = NO; [self.inputContainer addGestureRecognizer:longPress]; // 文字输入视图 [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; } #pragma mark - Setter - (void)setStatusText:(NSString *)statusText { _statusText = [statusText copy]; self.statusLabel.text = statusText; [self updateCenterTextIfNeeded]; } - (void)setEnabled:(BOOL)enabled { _enabled = enabled; self.recordButton.userInteractionEnabled = enabled; self.recordButton.alpha = enabled ? 1.0 : 0.5; self.inputContainer.userInteractionEnabled = enabled; self.inputContainer.alpha = enabled ? 1.0 : 0.5; } - (void)setRecording:(BOOL)recording { _isRecording = recording; self.recordButton.state = recording ? KBAiRecordButtonStateRecording : KBAiRecordButtonStateNormal; 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]; } #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 = @"松开结束"; _recordButton.normalIconImage = [UIImage imageNamed:@"ai_jianpan_icon"]; _recordButton.recordingIconImage = [UIImage imageNamed:@"ai_luyining_icon"]; _recordButton.hidden = YES; } return _recordButton; } - (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 { // 文本模式点击时,触发回调让 VC 显示文本输入框 if (self.onTextSend) { self.onTextSend(nil); } } - (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 = @"按住说话"; } } @end