// // KeyboardViewController+Panels.m // CustomKeyboard // // Created by Codex on 2026/02/22. // #import "KeyboardViewController+Private.h" #import "KBAuthManager.h" #import "KBBackspaceUndoManager.h" #import "KBChatMessage.h" #import "KBChatPanelView.h" #import "KBFunctionView.h" #import "KBHostAppLauncher.h" #import "KBInputBufferManager.h" #import "KBKey.h" #import "KBKeyBoardMainView.h" #import "KBKeyboardSubscriptionView.h" #import "KBSettingView.h" #import "Masonry.h" #import #import @implementation KeyboardViewController (Panels) #pragma mark - Panel Mode - (void)kb_setPanelMode:(KBKeyboardPanelMode)mode animated:(BOOL)animated { if (mode == self.kb_panelMode) { return; } KBKeyboardPanelMode fromMode = self.kb_panelMode; self.kb_panelMode = mode; // 主键盘视图是基础承载:确保存在(键盘隐藏后会被释放) [self kb_ensureKeyBoardMainViewIfNeeded]; // 1) 先收起所有面板(再展开目标面板),避免互相调用导致漏关/层级错乱 [self kb_setSubscriptionPanelVisible:NO animated:animated]; [self kb_setSettingViewVisible:NO animated:animated]; [self kb_setChatPanelVisible:NO animated:animated]; [self kb_setFunctionPanelVisible:NO]; // 2) 再展开目标面板 switch (mode) { case KBKeyboardPanelModeFunction: [self kb_setFunctionPanelVisible:YES]; break; case KBKeyboardPanelModeChat: [self kb_setChatPanelVisible:YES animated:animated]; break; case KBKeyboardPanelModeSettings: [self kb_setSettingViewVisible:YES animated:animated]; break; case KBKeyboardPanelModeSubscription: [self kb_setSubscriptionPanelVisible:YES animated:animated]; break; case KBKeyboardPanelModeMain: default: break; } // 3) 事件埋点:保持原逻辑(仅功能面板/主面板会互相曝光) if (mode == KBKeyboardPanelModeFunction) { [[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:@"enter_keyboard_function_panel" pageId:@"keyboard_function_panel" extra:nil completion:nil]; } else if (mode == KBKeyboardPanelModeMain && fromMode == KBKeyboardPanelModeFunction) { [[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:@"enter_keyboard_main_panel" pageId:@"keyboard_main_panel" extra:nil completion:nil]; } else if (mode == KBKeyboardPanelModeSettings) { [[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:@"enter_keyboard_settings" pageId:@"keyboard_settings" extra:nil completion:nil]; } else if (mode == KBKeyboardPanelModeSubscription) { [[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:@"enter_keyboard_subscription_panel" pageId:@"keyboard_subscription_panel" extra:nil completion:nil]; } // 4) 层级:保证当前面板在最上层 if (mode == KBKeyboardPanelModeSubscription) { [self.contentView bringSubviewToFront:self.subscriptionView]; } else if (mode == KBKeyboardPanelModeSettings) { [self.contentView bringSubviewToFront:self.settingView]; } else if (mode == KBKeyboardPanelModeChat) { [self.contentView bringSubviewToFront:self.chatPanelView]; } else if (mode == KBKeyboardPanelModeFunction) { [self.contentView bringSubviewToFront:self.functionView]; } else { [self.contentView bringSubviewToFront:self.keyBoardMainView]; } } /// 对外兼容:切换显示功能面板/键盘主视图 - (void)showFunctionPanel:(BOOL)show { if (show) { [self kb_setPanelMode:KBKeyboardPanelModeFunction animated:NO]; return; } if (self.kb_panelMode == KBKeyboardPanelModeFunction) { [self kb_setPanelMode:KBKeyboardPanelModeMain animated:NO]; } } /// 对外兼容:显示/隐藏设置页(高度与 keyBoardMainView 一致),右侧滑入/滑出 - (void)showSettingView:(BOOL)show { if (show) { [self kb_setPanelMode:KBKeyboardPanelModeSettings animated:YES]; return; } if (self.kb_panelMode == KBKeyboardPanelModeSettings) { [self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES]; } } /// 对外兼容:显示/隐藏聊天面板(覆盖整个键盘区域) - (void)showChatPanel:(BOOL)show { if (show) { [self kb_setPanelMode:KBKeyboardPanelModeChat animated:YES]; return; } if (self.kb_panelMode == KBKeyboardPanelModeChat) { [self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES]; } } - (void)kb_setFunctionPanelVisible:(BOOL)visible { if (visible) { [self kb_ensureFunctionViewIfNeeded]; } if (_functionView) { _functionView.hidden = !visible; } else if (visible) { // ensure 后按理已存在;这里兜底一次,避免异常情况下状态不一致 self.functionView.hidden = NO; } self.keyBoardMainView.hidden = visible; } - (void)kb_setChatPanelVisible:(BOOL)visible animated:(BOOL)animated { if (visible == self.chatPanelVisible) { return; } self.chatPanelVisible = visible; if (visible) { // 记录打开聊天面板时宿主输入框已有的文本,发送时只取新增部分 [[KBInputBufferManager shared] refreshFromProxyIfPossible:self.textDocumentProxy]; self.chatPanelBaselineText = [KBInputBufferManager shared].liveText ?: @""; [self kb_ensureChatPanelViewIfNeeded]; self.chatPanelView.hidden = NO; self.chatPanelView.alpha = 0.0; if (animated) { [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ self.chatPanelView.alpha = 1.0; } completion:nil]; } else { self.chatPanelView.alpha = 1.0; } } else { // 从未创建过聊天面板时,直接返回,避免 show/hide 触发额外内存分配 if (!_chatPanelView) { [self kb_updateKeyboardLayoutIfNeeded]; return; } if (animated) { [UIView animateWithDuration:0.18 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{ self.chatPanelView.alpha = 0.0; } completion:^(BOOL finished) { self.chatPanelView.hidden = YES; }]; } else { self.chatPanelView.alpha = 0.0; self.chatPanelView.hidden = YES; } } [self kb_updateKeyboardLayoutIfNeeded]; } - (void)kb_setSettingViewVisible:(BOOL)visible animated:(BOOL)animated { if (visible) { KBSettingView *settingView = self.settingView; if (!settingView.superview) { settingView.hidden = YES; [self.contentView addSubview:settingView]; [settingView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.contentView); }]; [settingView.backButton addTarget:self action:@selector(onTapSettingsBack) forControlEvents:UIControlEventTouchUpInside]; } [self.contentView bringSubviewToFront:settingView]; // 以 keyBoardMainView 的实际宽度为准,避免首次添加时 self.view 宽度尚未计算 [self.contentView layoutIfNeeded]; CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds); if (w <= 0) { w = CGRectGetWidth(self.contentView.bounds); } if (w <= 0) { w = [self kb_portraitWidth]; } settingView.transform = CGAffineTransformMakeTranslation(w, 0); settingView.hidden = NO; if (animated) { [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ settingView.transform = CGAffineTransformIdentity; } completion:nil]; } else { settingView.transform = CGAffineTransformIdentity; } } else { KBSettingView *settingView = _settingView; if (!settingView) { return; } if (!settingView.superview || settingView.hidden) { return; } CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds); if (w <= 0) { w = CGRectGetWidth(self.contentView.bounds); } if (w <= 0) { w = [self kb_portraitWidth]; } if (animated) { [UIView animateWithDuration:0.22 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{ settingView.transform = CGAffineTransformMakeTranslation(w, 0); } completion:^(BOOL finished) { settingView.hidden = YES; }]; } else { settingView.transform = CGAffineTransformMakeTranslation(w, 0); settingView.hidden = YES; } } } - (void)kb_setSubscriptionPanelVisible:(BOOL)visible animated:(BOOL)animated { if (visible) { KBKeyboardSubscriptionView *panel = self.subscriptionView; if (!panel.superview) { panel.hidden = YES; [self.contentView addSubview:panel]; [panel mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.contentView); }]; } [self.contentView bringSubviewToFront:panel]; panel.hidden = NO; panel.alpha = 0.0; CGFloat height = CGRectGetHeight(self.contentView.bounds); if (height <= 0) { height = 260; } panel.transform = CGAffineTransformMakeTranslation(0, height); [panel refreshProductsIfNeeded]; if (animated) { [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ panel.alpha = 1.0; panel.transform = CGAffineTransformIdentity; } completion:nil]; } else { panel.alpha = 1.0; panel.transform = CGAffineTransformIdentity; } return; } KBKeyboardSubscriptionView *panel = _subscriptionView; if (!panel) { return; } if (!panel.superview || panel.hidden) { return; } CGFloat height = CGRectGetHeight(panel.bounds); if (height <= 0) { height = CGRectGetHeight(self.contentView.bounds); } if (animated) { [UIView animateWithDuration:0.22 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{ panel.alpha = 0.0; panel.transform = CGAffineTransformMakeTranslation(0, height); } completion:^(BOOL finished) { panel.hidden = YES; panel.alpha = 1.0; panel.transform = CGAffineTransformIdentity; }]; } else { panel.hidden = YES; panel.alpha = 1.0; panel.transform = CGAffineTransformIdentity; } } // 延迟创建:仅在用户真正打开功能面板时才创建/布局,降低默认内存占用。 - (void)kb_ensureFunctionViewIfNeeded { if (_functionView && _functionView.superview) { return; } KBFunctionView *v = self.functionView; if (!v.superview) { v.hidden = YES; [self.contentView addSubview:v]; [v mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.contentView); }]; } } // 延迟创建:仅在用户打开聊天面板时才创建/布局。 - (void)kb_ensureChatPanelViewIfNeeded { if (_chatPanelView && _chatPanelView.superview) { return; } CGFloat portraitWidth = [self kb_portraitWidth]; CGFloat chatPanelHeight = [self kb_chatPanelHeightForWidth:portraitWidth]; KBChatPanelView *v = self.chatPanelView; if (!v.superview) { [self.contentView addSubview:v]; [v mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.contentView); make.bottom.equalTo(self.keyBoardMainView.mas_top); self.chatPanelHeightConstraint = make.height.mas_equalTo(chatPanelHeight); }]; v.hidden = YES; } } // 延迟创建:键盘主面板(按键区)在隐藏时会被释放;再次显示时需要重建。 - (void)kb_ensureKeyBoardMainViewIfNeeded { if (_keyBoardMainView && _keyBoardMainView.superview) { return; } CGFloat portraitWidth = [self kb_portraitWidth]; CGFloat keyboardBaseHeight = [self kb_keyboardBaseHeightForWidth:portraitWidth]; KBKeyBoardMainView *v = self.keyBoardMainView; if (!v.superview) { [self.contentView addSubview:v]; [v mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.contentView); make.bottom.equalTo(self.contentView); self.keyBoardMainHeightConstraint = make.height.mas_equalTo(keyboardBaseHeight); }]; } [self.contentView bringSubviewToFront:v]; } // 键盘隐藏时释放可重建资源(背景图/缓存/非必需面板),降低扩展内存峰值。 - (void)kb_releaseMemoryWhenKeyboardHidden { [KBHUD setContainerView:nil]; self.bgImageView.image = nil; self.kb_cachedGradientImage = nil; [self.kb_defaultGradientLayer removeFromSuperlayer]; self.kb_defaultGradientLayer = nil; [[SDImageCache sharedImageCache] clearMemory]; // 聊天相关可能持有音频数据/临时文件,键盘隐藏时直接清空,避免累计占用。 if (self.chatAudioPlayer) { [self.chatAudioPlayer stop]; self.chatAudioPlayer = nil; } if (_chatMessages.count > 0) { NSString *tmpRoot = NSTemporaryDirectory(); for (KBChatMessage *msg in _chatMessages.copy) { if (tmpRoot.length > 0 && msg.audioFilePath.length > 0 && [msg.audioFilePath hasPrefix:tmpRoot]) { [[NSFileManager defaultManager] removeItemAtPath:msg.audioFilePath error:nil]; } } [_chatMessages removeAllObjects]; } if (_keyBoardMainView) { [_keyBoardMainView removeFromSuperview]; _keyBoardMainView = nil; } self.keyBoardMainHeightConstraint = nil; if (_functionView) { [_functionView removeFromSuperview]; _functionView = nil; } if (_chatPanelView) { [_chatPanelView removeFromSuperview]; _chatPanelView = nil; } self.chatPanelVisible = NO; self.kb_panelMode = KBKeyboardPanelModeMain; if (_subscriptionView) { [_subscriptionView removeFromSuperview]; _subscriptionView = nil; } if (_settingView) { [_settingView removeFromSuperview]; _settingView = nil; } } // MARK: - KBKeyBoardMainViewDelegate - (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapKey:(KBKey *)key { switch (key.type) { case KBKeyTypeCharacter: { [[KBBackspaceUndoManager shared] registerNonClearAction]; NSString *text = key.output ?: key.title ?: @""; [self.textDocumentProxy insertText:text]; [self kb_updateCurrentWordWithInsertedText:text]; [[KBInputBufferManager shared] appendText:text]; } break; case KBKeyTypeBackspace: [[KBInputBufferManager shared] refreshFromProxyIfPossible:self.textDocumentProxy]; [[KBInputBufferManager shared] prepareSnapshotForDeleteWithContextBefore: self.textDocumentProxy.documentContextBeforeInput after: self.textDocumentProxy .documentContextAfterInput]; [[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:self.textDocumentProxy count:1]; [self kb_scheduleContextRefreshResetSuppression:NO]; [[KBInputBufferManager shared] applyHoldDeleteCount:1]; break; case KBKeyTypeSpace: [[KBBackspaceUndoManager shared] registerNonClearAction]; [self.textDocumentProxy insertText:@" "]; [self kb_clearCurrentWord]; [[KBInputBufferManager shared] appendText:@" "]; break; case KBKeyTypeReturn: if (self.chatPanelVisible) { [self kb_handleChatSendAction]; break; } [[KBBackspaceUndoManager shared] registerNonClearAction]; [self.textDocumentProxy insertText:@"\n"]; [self kb_clearCurrentWord]; [[KBInputBufferManager shared] appendText:@"\n"]; break; case KBKeyTypeGlobe: [self advanceToNextInputMode]; break; case KBKeyTypeCustom: [[KBBackspaceUndoManager shared] registerNonClearAction]; // 点击自定义键切换到功能面板 [self kb_setPanelMode:KBKeyboardPanelModeFunction animated:NO]; [self kb_clearCurrentWord]; break; case KBKeyTypeModeChange: case KBKeyTypeShift: // 这些已在 KBKeyBoardMainView/KBKeyboardView 内部处理 break; } } - (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapToolActionAtIndex:(NSInteger)index { NSDictionary *extra = @{@"index" : @(index)}; [[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_toolbar_action" pageId:@"keyboard_main_panel" elementId:@"toolbar_action" extra:extra completion:nil]; if (index == 0) { [self kb_setPanelMode:KBKeyboardPanelModeFunction animated:YES]; [self kb_clearCurrentWord]; return; } if (index == 1) { [self kb_setPanelMode:KBKeyboardPanelModeChat animated:YES]; return; } [self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES]; } - (void)keyBoardMainViewDidTapSettings:(KBKeyBoardMainView *)keyBoardMainView { [[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_settings_btn" pageId:@"keyboard_main_panel" elementId:@"settings_btn" extra:nil completion:nil]; [self kb_setPanelMode:KBKeyboardPanelModeSettings animated:YES]; } - (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectEmoji:(NSString *)emoji { if (emoji.length == 0) { return; } [[KBBackspaceUndoManager shared] registerNonClearAction]; [self.textDocumentProxy insertText:emoji]; [self kb_clearCurrentWord]; [[KBInputBufferManager shared] appendText:emoji]; } - (void)keyBoardMainViewDidTapUndo:(KBKeyBoardMainView *)keyBoardMainView { [[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_undo_btn" pageId:@"keyboard_main_panel" elementId:@"undo_btn" extra:nil completion:nil]; [[KBBackspaceUndoManager shared] performUndoFromResponder:self.view]; [self kb_scheduleContextRefreshResetSuppression:YES]; } - (void)keyBoardMainViewDidTapEmojiSearch: (KBKeyBoardMainView *)keyBoardMainView { // [[KBMaiPointReporter sharedReporter] // reportClickWithEventName:@"click_keyboard_emoji_search_btn" // pageId:@"keyboard_main_panel" // elementId:@"emoji_search_btn" // extra:nil // completion:nil]; [KBHUD showInfo:KBLocalized(@"Search coming soon")]; } // MARK: - KBFunctionViewDelegate - (void)functionView:(KBFunctionView *)functionView didTapToolActionAtIndex:(NSInteger)index { // 需求:当 index == 0 时,切回键盘主视图 if (index == 0) { [self kb_setPanelMode:KBKeyboardPanelModeMain animated:NO]; } } - (void)functionView:(KBFunctionView *_Nullable)functionView didRightTapToolActionAtIndex:(NSInteger)index { [[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_function_right_action" pageId:@"keyboard_function_panel" elementId:@"right_action" extra:@{@"action" : @"login_or_recharge"} completion:nil]; if (!KBAuthManager.shared.isLoggedIn) { NSString *schemeStr = [NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME]; NSURL *scheme = [NSURL URLWithString:schemeStr]; // 从当前视图作为起点,通过响应链找到 UIApplication 再调起主 App BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view]; return; } NSString *schemeStr = [NSString stringWithFormat:@"%@://recharge?src=keyboard", KB_APP_SCHEME]; NSURL *scheme = [NSURL URLWithString:schemeStr]; // 从当前视图作为起点,通过响应链找到 UIApplication 再调起主 App BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view]; if (!ok) { // 失败兜底:给个文案提示 // 比如:请回到桌面手动打开 XXX App 进行设置/充值 [KBHUD showInfo:@"请回到桌面手动打开App进行充值"]; } } - (void)functionViewDidRequestSubscription:(KBFunctionView *)functionView { [self showSubscriptionPanel]; } #pragma mark - Actions - (void)onTapSettingsBack { [[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_settings_back_btn" pageId:@"keyboard_settings" elementId:@"back_btn" extra:nil completion:nil]; [self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES]; } @end