// // KBKeyboardView.m // CustomKeyboard // #import "KBKeyboardView.h" #import "KBKeyButton.h" #import "KBKey.h" #import "KBBackspaceLongPressHandler.h" #import "KBKeyboardLayoutConfig.h" #import "KBKeyboardLayoutResolver.h" #import "KBKeyboardKeyFactory.h" #import "KBKeyboardLayoutEngine.h" #import "KBKeyboardRowBuilder.h" #import "KBKeyboardInputHandler.h" #import "KBKeyboardLegacyBuilder.h" #import "KBKeyboardLegacyLayoutProvider.h" #import "KBKeyboardInteractionHandler.h" #import "KBKeyboardRowContainerBuilder.h" // 第二行字母行的左右占位比例(用于居中) static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5; @interface KBKeyboardView () @property (nonatomic, strong) NSMutableArray *rowViews; @property (nonatomic, strong) NSArray *> *keysForRows; @property (nonatomic, strong) KBBackspaceLongPressHandler *backspaceHandler; @property (nonatomic, strong) KBKeyboardLayoutConfig *layoutConfig; @property (nonatomic, strong) KBKeyboardKeyFactory *keyFactory; @property (nonatomic, strong) KBKeyboardLayoutEngine *layoutEngine; @property (nonatomic, strong) KBKeyboardRowBuilder *rowBuilder; @property (nonatomic, strong) KBKeyboardInputHandler *inputHandler; @property (nonatomic, strong) KBKeyboardLegacyBuilder *legacyBuilder; @property (nonatomic, strong) KBKeyboardLegacyLayoutProvider *legacyLayoutProvider; @property (nonatomic, strong) KBKeyboardInteractionHandler *interactionHandler; @property (nonatomic, strong) KBKeyboardRowContainerBuilder *rowContainerBuilder; /// 跨行统一字符键宽度(按最多字符键的行计算),0 表示不启用 @property (nonatomic, assign) CGFloat kb_uniformCharKeyWidth; /// 记录当前行间距,便于切换布局时判断是否需要重建容器 @property (nonatomic, assign) CGFloat kb_currentRowSpacing; /// 记录当前顶/底间距,便于切换布局时判断是否需要重建容器 @property (nonatomic, assign) CGFloat kb_currentTopInset; @property (nonatomic, assign) CGFloat kb_currentBottomInset; @end @implementation KBKeyboardView - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { self.backgroundColor = [UIColor clearColor]; _layoutStyle = KBKeyboardLayoutStyleLetters; // 默认小写:与需求一致,初始不开启 Shift _shiftOn = NO; _symbolsMoreOn = NO; // 数字面板默认第一页(123) // 从 App Group 读取当前 profileId 并设置布局 NSString *profileId = [[KBKeyboardLayoutResolver sharedResolver] currentProfileId]; if (profileId.length > 0) { _currentLayoutJsonId = [[KBKeyboardLayoutResolver sharedResolver] layoutJsonIdForProfileId:profileId]; NSLog(@"[KBKeyboardView] Loaded profileId: %@, layoutJsonId: %@", profileId, _currentLayoutJsonId); } else { _currentLayoutJsonId = @"letters"; NSLog(@"[KBKeyboardView] No profileId found, using default 'letters'"); } self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig]; self.backspaceHandler = [[KBBackspaceLongPressHandler alloc] initWithContainerView:self]; [self buildBase]; } return self; } // 当切换大布局(字母/数字)时,重置数字二级页状态 - (void)setLayoutStyle:(KBKeyboardLayoutStyle)layoutStyle { _layoutStyle = layoutStyle; if (_layoutStyle != KBKeyboardLayoutStyleNumbers) { _symbolsMoreOn = NO; } } #pragma mark - Base Layout - (void)buildBase { KBKeyboardLayout *layout = [self kb_currentLayout]; NSArray *rows = layout.rows ?: @[]; if (rows.count == 0) { // Fallback: 至少创建 4 行容器 rows = @[[KBKeyboardRowConfig new], [KBKeyboardRowConfig new], [KBKeyboardRowConfig new], [KBKeyboardRowConfig new]]; } CGFloat rowSpacing = [self.layoutEngine rowSpacingForLayout:layout]; CGFloat topInset = [self.layoutEngine topInsetForLayout:layout]; CGFloat bottomInset = [self.layoutEngine bottomInsetForLayout:layout]; self.kb_currentRowSpacing = rowSpacing; self.kb_currentTopInset = topInset; self.kb_currentBottomInset = bottomInset; [self.rowContainerBuilder rebuildRowContainersForRows:rows inContainer:self rowViews:self.rowViews rowSpacing:rowSpacing topInset:topInset bottomInset:bottomInset]; [self.interactionHandler bringPreviewToFrontIfNeededInContainer:self]; } #pragma mark - Public - (void)reloadKeys { [self.backspaceHandler bindDeleteButton:nil showClearLabel:NO]; KBKeyboardLayout *layout = [self kb_currentLayout]; CGFloat rowSpacing = [self.layoutEngine rowSpacingForLayout:layout]; CGFloat topInset = [self.layoutEngine topInsetForLayout:layout]; CGFloat bottomInset = [self.layoutEngine bottomInsetForLayout:layout]; NSLog(@"[KBKeyboardView] reloadKeys: layoutName=%@ rows=%lu shiftRows=%lu shiftOn=%d", self.currentLayoutJsonId, (unsigned long)layout.rows.count, (unsigned long)layout.shiftRows.count, self.shiftOn); NSArray *rows = nil; if (self.shiftOn && layout.shiftRows.count > 0) { rows = layout.shiftRows; } else { rows = layout.rows ?: @[]; } NSLog(@"[KBKeyboardView] reloadKeys: usingRows=%lu currentContainers=%lu", (unsigned long)rows.count, (unsigned long)self.rowViews.count); // 行数变化时(如从 4 行布局切到 5 行注音布局),重建行容器 if (rows.count >= 4 && (rows.count != self.rowViews.count || fabs(self.kb_currentRowSpacing - rowSpacing) > 0.1 || fabs(self.kb_currentTopInset - topInset) > 0.1 || fabs(self.kb_currentBottomInset - bottomInset) > 0.1)) { self.kb_currentRowSpacing = rowSpacing; self.kb_currentTopInset = topInset; self.kb_currentBottomInset = bottomInset; [self.rowContainerBuilder rebuildRowContainersForRows:rows inContainer:self rowViews:self.rowViews rowSpacing:rowSpacing topInset:topInset bottomInset:bottomInset]; [self.interactionHandler bringPreviewToFrontIfNeededInContainer:self]; } // 移除旧按钮 for (UIView *row in self.rowViews) { [row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; } if (rows.count < 4) { NSLog(@"[KBKeyboardView] reloadKeys: rows.count < 4, fallback to legacy"); self.kb_uniformCharKeyWidth = 0.0; [self kb_buildLegacyLayout]; return; } // 计算跨行统一字符键宽度(若各行字符键数量不同,则按最多键的行为基准) self.kb_uniformCharKeyWidth = [self.layoutEngine calculateUniformCharKeyWidthForRows:rows keyFactory:self.keyFactory shiftOn:self.shiftOn]; for (NSUInteger i = 0; i < rows.count && i < self.rowViews.count; i++) { [self.rowBuilder buildRow:self.rowViews[i] withRowConfig:rows[i] uniformCharWidth:self.kb_uniformCharKeyWidth shiftOn:self.shiftOn backspaceHandler:self.backspaceHandler target:self action:@selector(onKeyTapped:)]; } NSUInteger totalButtons = [self kb_totalKeyButtonCount]; NSLog(@"[KBKeyboardView] reloadKeys: totalButtons=%lu", (unsigned long)totalButtons); if (totalButtons == 0) { NSLog(@"[KBKeyboardView] config layout produced no keys, fallback to legacy."); for (UIView *row in self.rowViews) { [row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; } [self kb_buildLegacyLayout]; } } - (void)didMoveToWindow { [super didMoveToWindow]; if (!self.window) { return; } if ([self kb_totalKeyButtonCount] > 0) { return; } // 兜底:系统编辑菜单切出切回等场景下,若按键丢失则自动重建。 [self reloadKeys]; // 自动重建后再触发一次上层主题应用,避免“按键恢复了但皮肤背景没恢复”。 UIView *container = self.superview; if ([container respondsToSelector:@selector(kb_applyTheme)]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [container performSelector:@selector(kb_applyTheme)]; #pragma clang diagnostic pop } } - (NSUInteger)kb_totalKeyButtonCount { NSUInteger total = 0; for (UIView *row in self.rowViews) { total += [self.interactionHandler collectKeyButtonsInView:row].count; } return total; } #pragma mark - Hit Test - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView *hit = [super hitTest:point withEvent:event]; return [self.interactionHandler resolveHitView:hit point:point container:self rowViews:self.rowViews]; } #pragma mark - Config Helpers - (KBKeyboardLayoutConfig *)kb_layoutConfig { if (!self.layoutConfig) { self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig]; } return self.layoutConfig; } - (KBKeyboardKeyFactory *)keyFactory { if (!_keyFactory) { _keyFactory = [[KBKeyboardKeyFactory alloc] initWithLayoutConfig:[self kb_layoutConfig]]; } return _keyFactory; } - (KBKeyboardLayoutEngine *)layoutEngine { if (!_layoutEngine) { _layoutEngine = [[KBKeyboardLayoutEngine alloc] initWithLayoutConfig:[self kb_layoutConfig]]; } return _layoutEngine; } - (KBKeyboardRowBuilder *)rowBuilder { if (!_rowBuilder) { _rowBuilder = [[KBKeyboardRowBuilder alloc] initWithLayoutConfig:[self kb_layoutConfig] layoutEngine:self.layoutEngine keyFactory:self.keyFactory]; } return _rowBuilder; } - (KBKeyboardInputHandler *)inputHandler { if (!_inputHandler) { _inputHandler = [[KBKeyboardInputHandler alloc] init]; __weak typeof(self) weakSelf = self; _inputHandler.onToggleShift = ^{ __strong typeof(weakSelf) self = weakSelf; if (!self) { return; } self.shiftOn = !self.shiftOn; [self reloadKeys]; }; _inputHandler.onToggleSymbols = ^{ __strong typeof(weakSelf) self = weakSelf; if (!self) { return; } self.symbolsMoreOn = !self.symbolsMoreOn; [self reloadKeys]; }; _inputHandler.onKeyTapped = ^(KBKey *key) { __strong typeof(weakSelf) self = weakSelf; if (!self) { return; } if ([self.delegate respondsToSelector:@selector(keyboardView:didTapKey:)]) { [self.delegate keyboardView:self didTapKey:key]; } }; } return _inputHandler; } - (KBKeyboardLegacyBuilder *)legacyBuilder { if (!_legacyBuilder) { _legacyBuilder = [[KBKeyboardLegacyBuilder alloc] init]; } return _legacyBuilder; } - (KBKeyboardLegacyLayoutProvider *)legacyLayoutProvider { if (!_legacyLayoutProvider) { _legacyLayoutProvider = [[KBKeyboardLegacyLayoutProvider alloc] init]; } return _legacyLayoutProvider; } - (KBKeyboardInteractionHandler *)interactionHandler { if (!_interactionHandler) { _interactionHandler = [[KBKeyboardInteractionHandler alloc] init]; } return _interactionHandler; } - (KBKeyboardRowContainerBuilder *)rowContainerBuilder { if (!_rowContainerBuilder) { _rowContainerBuilder = [[KBKeyboardRowContainerBuilder alloc] init]; } return _rowContainerBuilder; } - (KBKeyboardLayout *)kb_layoutForName:(NSString *)name { return [[self kb_layoutConfig] layoutForName:name]; } - (KBKeyboardLayout *)kb_currentLayout { NSString *baseLayoutName = self.currentLayoutJsonId.length > 0 ? self.currentLayoutJsonId : @"letters"; if (self.layoutStyle == KBKeyboardLayoutStyleNumbers) { // 优先查找当前语言的数字/符号布局,如 letters_es_numbers / letters_es_symbols // 如果不存在,回退到通用布局 numbers / symbolsMore NSString *numbersName = [NSString stringWithFormat:@"%@_numbers", baseLayoutName]; NSString *symbolsName = [NSString stringWithFormat:@"%@_symbols", baseLayoutName]; NSString *targetName = self.symbolsMoreOn ? symbolsName : numbersName; KBKeyboardLayout *layout = [self kb_layoutForName:targetName]; if (layout && layout.rows.count >= 4) { return layout; } // 回退到通用布局 return [self kb_layoutForName:(self.symbolsMoreOn ? @"symbolsMore" : @"numbers")]; } return [self kb_layoutForName:baseLayoutName]; } - (void)reloadLayoutWithProfileId:(NSString *)profileId { if (profileId.length == 0) { NSLog(@"[KBKeyboardView] reloadLayoutWithProfileId: empty profileId, ignoring"); return; } NSString *newLayoutJsonId = [[KBKeyboardLayoutResolver sharedResolver] layoutJsonIdForProfileId:profileId]; if ([newLayoutJsonId isEqualToString:self.currentLayoutJsonId]) { NSLog(@"[KBKeyboardView] Layout already loaded: %@", newLayoutJsonId); return; } NSLog(@"[KBKeyboardView] Switching layout from %@ to %@", self.currentLayoutJsonId, newLayoutJsonId); self.currentLayoutJsonId = newLayoutJsonId; // 重新加载键盘布局 [self reloadKeys]; } - (void)kb_buildLegacyLayout { self.keysForRows = [self.legacyLayoutProvider keysForLayoutStyleIsNumbers:(self.layoutStyle == KBKeyboardLayoutStyleNumbers) shiftOn:self.shiftOn symbolsMoreOn:self.symbolsMoreOn keyFactory:self.keyFactory]; if (self.keysForRows.count < 4) { return; } if (self.rowViews.count < 4) { return; } [self.legacyBuilder buildRow:self.rowViews[0] withKeys:self.keysForRows[0] edgeSpacerMultiplier:0.0 shiftOn:self.shiftOn backspaceHandler:self.backspaceHandler target:self action:@selector(onKeyTapped:)]; CGFloat row2Spacer = (self.layoutStyle == KBKeyboardLayoutStyleLetters) ? kKBLettersRow2EdgeSpacerMultiplier : 0.0; [self.legacyBuilder buildRow:self.rowViews[1] withKeys:self.keysForRows[1] edgeSpacerMultiplier:row2Spacer shiftOn:self.shiftOn backspaceHandler:self.backspaceHandler target:self action:@selector(onKeyTapped:)]; [self.legacyBuilder buildRow:self.rowViews[2] withKeys:self.keysForRows[2] edgeSpacerMultiplier:0.0 shiftOn:self.shiftOn backspaceHandler:self.backspaceHandler target:self action:@selector(onKeyTapped:)]; [self.legacyBuilder buildRow:self.rowViews[3] withKeys:self.keysForRows[3] edgeSpacerMultiplier:0.0 shiftOn:self.shiftOn backspaceHandler:self.backspaceHandler target:self action:@selector(onKeyTapped:)]; } #pragma mark - Actions - (void)onKeyTapped:(KBKeyButton *)sender { [self.inputHandler handleKeyTap:sender.key]; } // 在字符键按下时,显示一个上方气泡预览(类似系统键盘)。 - (void)showPreviewForButton:(KBKeyButton *)button { [self.interactionHandler showPreviewForButton:button inContainer:self]; } - (void)hidePreview { [self.interactionHandler hidePreview]; } #pragma mark - Lazy - (NSMutableArray *)rowViews { if (!_rowViews) _rowViews = [NSMutableArray array]; return _rowViews; } @end