From f30b1d7640c2d30a0650a527df9ef19dafbe5c72 Mon Sep 17 00:00:00 2001 From: CodeST <694468528@qq.com> Date: Wed, 4 Mar 2026 12:54:57 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CustomKeyboard/View/KBKeyboardView.m | 1544 ----------------- .../KBKeyboardView/KBKeyboardInputHandler.h | 27 + .../KBKeyboardView/KBKeyboardInputHandler.m | 26 + .../KBKeyboardInteractionHandler.h | 28 + .../KBKeyboardInteractionHandler.m | 156 ++ .../KBKeyboardView/KBKeyboardKeyFactory.h | 28 + .../KBKeyboardView/KBKeyboardKeyFactory.m | 186 ++ .../KBKeyboardView/KBKeyboardLayoutEngine.h | 42 + .../KBKeyboardView/KBKeyboardLayoutEngine.m | 245 +++ .../KBKeyboardView/KBKeyboardLegacyBuilder.h | 28 + .../KBKeyboardView/KBKeyboardLegacyBuilder.m | 291 ++++ .../KBKeyboardLegacyLayoutProvider.h | 22 + .../KBKeyboardLegacyLayoutProvider.m | 193 +++ .../KBKeyboardView/KBKeyboardRowBuilder.h | 33 + .../KBKeyboardView/KBKeyboardRowBuilder.m | 330 ++++ .../KBKeyboardRowContainerBuilder.h | 24 + .../KBKeyboardRowContainerBuilder.m | 59 + .../{ => KBKeyboardView}/KBKeyboardView.h | 0 .../View/KBKeyboardView/KBKeyboardView.m | 414 +++++ keyBoard.xcodeproj/project.pbxproj | 60 +- 20 files changed, 2190 insertions(+), 1546 deletions(-) delete mode 100644 CustomKeyboard/View/KBKeyboardView.m create mode 100644 CustomKeyboard/View/KBKeyboardView/KBKeyboardInputHandler.h create mode 100644 CustomKeyboard/View/KBKeyboardView/KBKeyboardInputHandler.m create mode 100644 CustomKeyboard/View/KBKeyboardView/KBKeyboardInteractionHandler.h create mode 100644 CustomKeyboard/View/KBKeyboardView/KBKeyboardInteractionHandler.m create mode 100644 CustomKeyboard/View/KBKeyboardView/KBKeyboardKeyFactory.h create mode 100644 CustomKeyboard/View/KBKeyboardView/KBKeyboardKeyFactory.m create mode 100644 CustomKeyboard/View/KBKeyboardView/KBKeyboardLayoutEngine.h create mode 100644 CustomKeyboard/View/KBKeyboardView/KBKeyboardLayoutEngine.m create mode 100644 CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyBuilder.h create mode 100644 CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyBuilder.m create mode 100644 CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyLayoutProvider.h create mode 100644 CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyLayoutProvider.m create mode 100644 CustomKeyboard/View/KBKeyboardView/KBKeyboardRowBuilder.h create mode 100644 CustomKeyboard/View/KBKeyboardView/KBKeyboardRowBuilder.m create mode 100644 CustomKeyboard/View/KBKeyboardView/KBKeyboardRowContainerBuilder.h create mode 100644 CustomKeyboard/View/KBKeyboardView/KBKeyboardRowContainerBuilder.m rename CustomKeyboard/View/{ => KBKeyboardView}/KBKeyboardView.h (100%) create mode 100644 CustomKeyboard/View/KBKeyboardView/KBKeyboardView.m diff --git a/CustomKeyboard/View/KBKeyboardView.m b/CustomKeyboard/View/KBKeyboardView.m deleted file mode 100644 index 1e538d5..0000000 --- a/CustomKeyboard/View/KBKeyboardView.m +++ /dev/null @@ -1,1544 +0,0 @@ -// -// KBKeyboardView.m -// CustomKeyboard -// - -#import "KBKeyboardView.h" -#import "KBKeyButton.h" -#import "KBKey.h" -#import "KBSkinManager.h" -#import "KBKeyPreviewView.h" -#import "KBBackspaceLongPressHandler.h" -#import "KBKeyboardLayoutConfig.h" -#import "KBKeyboardLayoutResolver.h" - -// UI 常量统一管理,方便后续调试样式(以 375 宽设计稿为基准,通过 KBFit 做等比缩放) -#define kKBRowVerticalSpacing KBFit(8.0f) -#define kKBRowHorizontalInset KBFit(6.0f) -#define kKBRowHeight KBFit(40.0f) - -static const NSTimeInterval kKBPreviewShowDuration = 0.08; -static const NSTimeInterval kKBPreviewHideDuration = 0.06; - -static const CGFloat kKBSpecialKeySquareMultiplier = 1.2; -static const CGFloat kKBReturnWidthMultiplier = 2.4; -static const CGFloat kKBSpaceWidthMultiplier = 3.0; - -// 第二行字母行的左右占位比例(用于居中) -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) KBKeyPreviewView *previewView; -@property (nonatomic, strong) KBKeyboardLayoutConfig *layoutConfig; -/// 跨行统一字符键宽度(按最多字符键的行计算),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 kb_rowSpacingForLayout:layout]; - CGFloat topInset = [self kb_topInsetForLayout:layout]; - CGFloat bottomInset = [self kb_bottomInsetForLayout:layout]; - self.kb_currentRowSpacing = rowSpacing; - self.kb_currentTopInset = topInset; - self.kb_currentBottomInset = bottomInset; - [self kb_rebuildRowContainersForRows:rows - rowSpacing:rowSpacing - topInset:topInset - bottomInset:bottomInset]; -} - -/// 根据行配置数组,动态创建/重建行容器(支持 4 行、5 行等任意行数) -- (void)kb_rebuildRowContainersForRows:(NSArray *)rowConfigs - rowSpacing:(CGFloat)rowSpacing - topInset:(CGFloat)topInset - bottomInset:(CGFloat)bottomInset { - // 移除旧的行容器 - for (UIView *row in self.rowViews) { - [row removeFromSuperview]; - } - [self.rowViews removeAllObjects]; - - NSUInteger rowCount = rowConfigs.count; - if (rowCount == 0) return; - - KBKeyboardLayoutConfig *config = [self kb_layoutConfig]; - - UIView *firstRow = nil; - UIView *previousRow = nil; - for (NSUInteger i = 0; i < rowCount; i++) { - UIView *rowView = [UIView new]; - [self addSubview:rowView]; - [self.rowViews addObject:rowView]; - - [rowView mas_makeConstraints:^(MASConstraintMaker *make) { - if (previousRow) { - make.top.equalTo(previousRow.mas_bottom).offset(rowSpacing); - } else { - make.top.equalTo(self.mas_top).offset(topInset); - } - make.left.right.equalTo(self); - // 所有行等高,自动根据可用空间分配行高 - if (firstRow) { - make.height.equalTo(firstRow); - } - }]; - - // 最后一行锚定到底部 - if (i == rowCount - 1) { - [rowView mas_makeConstraints:^(MASConstraintMaker *make) { - make.bottom.equalTo(self.mas_bottom).offset(-bottomInset); - }]; - } - - if (!firstRow) firstRow = rowView; - previousRow = rowView; - } - - // 确保预览气泡视图在最上层 - if (self.previewView && self.previewView.superview == self) { - [self bringSubviewToFront:self.previewView]; - } -} - -#pragma mark - Public - -- (void)reloadKeys { - [self.backspaceHandler bindDeleteButton:nil showClearLabel:NO]; - - KBKeyboardLayout *layout = [self kb_currentLayout]; - CGFloat rowSpacing = [self kb_rowSpacingForLayout:layout]; - CGFloat topInset = [self kb_topInsetForLayout:layout]; - CGFloat bottomInset = [self kb_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 kb_rebuildRowContainersForRows:rows - rowSpacing:rowSpacing - topInset:topInset - bottomInset:bottomInset]; - } - - // 移除旧按钮 - 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 kb_calculateUniformCharKeyWidthForRows:rows]; - - for (NSUInteger i = 0; i < rows.count && i < self.rowViews.count; i++) { - [self buildRow:self.rowViews[i] withRowConfig:rows[i]]; - } - - 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 kb_collectKeyButtonsInView:row].count; - } - return total; -} - -#pragma mark - Hit Test - -- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { - UIView *hit = [super hitTest:point withEvent:event]; - if ([hit isKindOfClass:[KBKeyButton class]]) { - return hit; - } - if ([self kb_isHitInsideKeyRows:hit]) { - KBKeyButton *btn = [self kb_nearestKeyButtonForPoint:point]; - if (btn) { return btn; } - } - return hit; -} - -- (BOOL)kb_isHitInsideKeyRows:(UIView *)hitView { - if (!hitView) { return NO; } - if (hitView == self) { return YES; } - for (UIView *row in self.rowViews) { - if ([hitView isDescendantOfView:row]) { return YES; } - } - return NO; -} - -- (KBKeyButton *)kb_nearestKeyButtonForPoint:(CGPoint)point { - KBKeyButton *best = nil; - CGFloat bestDistance = CGFLOAT_MAX; - NSArray *rows = self.rowViews; - - UIView *targetRow = nil; - for (UIView *row in rows) { - CGRect rowFrame = [self convertRect:row.bounds fromView:row]; - if (CGRectContainsPoint(rowFrame, point)) { - targetRow = row; - break; - } - } - - NSArray *candidateRows = targetRow ? @[targetRow] : rows; - for (UIView *row in candidateRows) { - NSArray *buttons = [self kb_collectKeyButtonsInView:row]; - for (KBKeyButton *btn in buttons) { - CGRect frame = [self convertRect:btn.frame fromView:btn.superview]; - CGFloat dx = point.x - CGRectGetMidX(frame); - CGFloat dy = point.y - CGRectGetMidY(frame); - CGFloat dist = (dx * dx) + (dy * dy); - if (dist < bestDistance) { - bestDistance = dist; - best = btn; - } - } - } - return best; -} - -- (NSArray *)kb_collectKeyButtonsInView:(UIView *)view { - if (!view) { return @[]; } - NSMutableArray *buttons = [NSMutableArray array]; - [self kb_collectKeyButtonsInView:view into:buttons]; - return buttons.copy; -} - -- (void)kb_collectKeyButtonsInView:(UIView *)view - into:(NSMutableArray *)buttons { - for (UIView *sub in view.subviews) { - if ([sub isKindOfClass:[KBKeyButton class]]) { - [buttons addObject:(KBKeyButton *)sub]; - continue; - } - if (sub.subviews.count > 0) { - [self kb_collectKeyButtonsInView:sub into:buttons]; - } - } -} - -#pragma mark - Key Model Construction - -// 创建当前布局下各行的 KBKey 列表 -- (NSArray *> *)buildKeysForCurrentLayout { - if (self.layoutStyle == KBKeyboardLayoutStyleNumbers) { - return [self buildKeysForNumbersLayout]; - } else { - return [self buildKeysForLettersLayout]; - } -} - -#pragma mark - Letters Layout - -- (NSArray *> *)buildKeysForLettersLayout { - // 字母布局(QWERTY) - NSArray *r1Letters = @[ @"q", @"w", @"e", @"r", @"t", @"y", @"u", @"i", @"o", @"p" ]; - NSArray *r2Letters = @[ @"a", @"s", @"d", @"f", @"g", @"h", @"j", @"k", @"l" ]; - NSArray *r3Letters = @[ @"z", @"x", @"c", @"v", @"b", @"n", @"m" ]; - - NSMutableArray *row1 = [NSMutableArray arrayWithCapacity:r1Letters.count]; - for (NSString *s in r1Letters) { - [row1 addObject:[self kb_letterKeyWithChar:s]]; - } - - NSMutableArray *row2 = [NSMutableArray arrayWithCapacity:r2Letters.count]; - for (NSString *s in r2Letters) { - [row2 addObject:[self kb_letterKeyWithChar:s]]; - } - - // 第三行:Shift + Z...M + Backspace - NSMutableArray *row3 = [NSMutableArray array]; - KBKey *shift = [KBKey keyWithIdentifier:@"shift" - title:@"⇧" - output:@"" - type:KBKeyTypeShift]; - // Shift 键也支持大小写两套皮肤图: - // - shift.caseVariant = Lower 时,使用 KBSkinIconMap 中 "shift" 对应的短名; - // - shift.caseVariant = Upper 时,使用 "shift_upper" 对应的短名。 - shift.caseVariant = self.shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower; - [row3 addObject:shift]; - - for (NSString *s in r3Letters) { - [row3 addObject:[self kb_letterKeyWithChar:s]]; - } - - KBKey *backspace = [KBKey keyWithIdentifier:@"backspace" - title:@"⌫" - output:@"" - type:KBKeyTypeBackspace]; - [row3 addObject:backspace]; - - NSArray *row4 = [self kb_bottomControlRowKeysForLettersLayout]; - - return @[row1.copy, row2.copy, row3.copy, row4]; -} - -#pragma mark - Numbers / Symbols Layout - -- (NSArray *> *)buildKeysForNumbersLayout { - // 数字/符号布局:3 行主键 + 底部控制行 - NSArray *r1 = nil; - NSArray *r2 = nil; - NSArray *r3 = nil; - - if (!self.symbolsMoreOn) { - // 数字第一页(123) - r1 = @[ [KBKey keyWithIdentifier:@"digit_1" title:@"1" output:@"1" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"digit_2" title:@"2" output:@"2" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"digit_3" title:@"3" output:@"3" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"digit_4" title:@"4" output:@"4" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"digit_5" title:@"5" output:@"5" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"digit_6" title:@"6" output:@"6" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"digit_7" title:@"7" output:@"7" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"digit_8" title:@"8" output:@"8" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"digit_9" title:@"9" output:@"9" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"digit_0" title:@"0" output:@"0" type:KBKeyTypeCharacter] ]; - r2 = @[ [KBKey keyWithIdentifier:@"sym_minus" title:@"-" output:@"-" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_slash" title:@"/" output:@"/" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_colon" title:@":" output:@":" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_semicolon" title:@";" output:@";" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_paren_l" title:@"(" output:@"(" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_paren_r" title:@")" output:@")" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_money" title:@"¥" output:@"¥" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_amp" title:@"&" output:@"&" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_at" title:@"@" output:@"@" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_quote_double" title:@"\"" output:@"\"" type:KBKeyTypeCharacter] ]; - - r3 = [self kb_symbolsCommonThirdRowWithToggleIsMore:NO]; - } else { - // 数字第二页(#+=):前两行替换为更多符号,左下角按钮文案改为“123” - r1 = @[ [KBKey keyWithIdentifier:@"sym_bracket_l" title:@"[" output:@"[" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_bracket_r" title:@"]" output:@"]" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_brace_l" title:@"{" output:@"{" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_brace_r" title:@"}" output:@"}" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_hash" title:@"#" output:@"#" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_percent" title:@"%" output:@"%" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_caret" title:@"^" output:@"^" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_asterisk" title:@"*" output:@"*" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_plus" title:@"+" output:@"+" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_equal" title:@"=" output:@"=" type:KBKeyTypeCharacter] ]; - r2 = @[ [KBKey keyWithIdentifier:@"sym_underscore" title:@"_" output:@"_" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_backslash" title:@"\\" output:@"\\" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_pipe" title:@"|" output:@"|" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_tilde" title:@"~" output:@"~" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_lt" title:@"<" output:@"<" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_gt" title:@">" output:@">" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_dollar" title:@"$" output:@"$" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_euro" title:@"€" output:@"€" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_pound" title:@"£" output:@"£" type:KBKeyTypeCharacter], - [KBKey keyWithIdentifier:@"sym_bullet" title:@"•" output:@"•" type:KBKeyTypeCharacter] ]; - - r3 = [self kb_symbolsCommonThirdRowWithToggleIsMore:YES]; - } - - NSArray *r4 = [self kb_bottomControlRowKeysForNumbersLayout]; - return @[r1, r2, r3, r4]; -} - -#pragma mark - Key Factories - -// 字母键工厂:根据 shiftOn 决定显示与输出大小写 -- (KBKey *)kb_letterKeyWithChar:(NSString *)charString { - if (charString.length == 0) { return nil; } - NSString *lower = charString.lowercaseString; - NSString *upper = charString.uppercaseString; - - NSString *shown = self.shiftOn ? upper : lower; - NSString *identifier = [NSString stringWithFormat:@"letter_%@", lower]; - - KBKey *k = [KBKey keyWithIdentifier:identifier - title:shown - output:shown - type:KBKeyTypeCharacter]; - k.caseVariant = self.shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower; - return k; -} - -// 数字布局第三行公共部分(左下角是 123 或 #+=) -- (NSArray *)kb_symbolsCommonThirdRowWithToggleIsMore:(BOOL)isMorePage { - NSString *identifier = isMorePage ? @"symbols_toggle_123" : @"symbols_toggle_more"; - NSString *title = isMorePage ? @"123" : @"#+="; - - KBKey *toggle = [KBKey keyWithIdentifier:identifier - title:title - output:@"" - type:KBKeyTypeSymbolsToggle]; - KBKey *comma = [KBKey keyWithIdentifier:@"sym_comma" title:@"," output:@"," type:KBKeyTypeCharacter]; - KBKey *dot = [KBKey keyWithIdentifier:@"sym_dot" title:@"." output:@"." type:KBKeyTypeCharacter]; - KBKey *q = [KBKey keyWithIdentifier:@"sym_question" title:@"?" output:@"?" type:KBKeyTypeCharacter]; - KBKey *ex = [KBKey keyWithIdentifier:@"sym_exclam" title:@"!" output:@"!" type:KBKeyTypeCharacter]; - KBKey *quote = [KBKey keyWithIdentifier:@"sym_quote_single" title:@"'" output:@"'" type:KBKeyTypeCharacter]; - KBKey *back = [KBKey keyWithIdentifier:@"backspace" - title:@"⌫" - output:@"" - type:KBKeyTypeBackspace]; - return @[ toggle, comma, dot, q, ex, quote, back ]; -} - -// 底部控制行(字母布局) -- (NSArray *)kb_bottomControlRowKeysForLettersLayout { - KBKey *mode123 = [KBKey keyWithIdentifier:@"mode_123" - title:@"123" - output:@"" - type:KBKeyTypeModeChange]; - KBKey *emoji = [KBKey keyWithIdentifier:KBKeyIdentifierEmojiPanel - title:@"😊" - output:@"" - type:KBKeyTypeCustom]; - KBKey *space = [KBKey keyWithIdentifier:@"space" - title:@"space" - output:@" " - type:KBKeyTypeSpace]; - KBKey *ret = [KBKey keyWithIdentifier:@"return" - title:KBLocalized(@"Send") - output:@"\n" - type:KBKeyTypeReturn]; - return @[ mode123, emoji, space, ret ]; -} - -// 底部控制行(数字布局) -- (NSArray *)kb_bottomControlRowKeysForNumbersLayout { - KBKey *modeABC = [KBKey keyWithIdentifier:@"mode_abc" - title:@"abc" - output:@"" - type:KBKeyTypeModeChange]; - KBKey *emoji = [KBKey keyWithIdentifier:KBKeyIdentifierEmojiPanel - title:@"😊" - output:@"" - type:KBKeyTypeCustom]; - KBKey *space = [KBKey keyWithIdentifier:@"space" - title:@"space" - output:@" " - type:KBKeyTypeSpace]; - KBKey *ret = [KBKey keyWithIdentifier:@"return" - title:KBLocalized(@"Send") - output:@"\n" - type:KBKeyTypeReturn]; - return @[ modeABC, emoji, space, ret ]; -} - -#pragma mark - Row Building - -- (void)buildRow:(UIView *)row withRowConfig:(KBKeyboardRowConfig *)rowConfig { - if (!row || !rowConfig) { return; } - CGFloat gap = [self kb_gapForRow:rowConfig]; - CGFloat insetLeft = [self kb_insetLeftForRow:rowConfig]; - CGFloat insetRight = [self kb_insetRightForRow:rowConfig]; - - if (rowConfig.segments) { - KBKeyboardRowSegments *segments = rowConfig.segments; - NSArray *leftItems = [segments leftItems]; - NSArray *centerItems = [segments centerItems]; - NSArray *rightItems = [segments rightItems]; - UIView *leftContainer = [UIView new]; - UIView *centerContainer = [UIView new]; - UIView *rightContainer = [UIView new]; - [row addSubview:leftContainer]; - [row addSubview:centerContainer]; - [row addSubview:rightContainer]; - - [leftContainer mas_makeConstraints:^(MASConstraintMaker *make) { - make.left.equalTo(row.mas_left).offset(insetLeft); - make.top.bottom.equalTo(row); - }]; - [rightContainer mas_makeConstraints:^(MASConstraintMaker *make) { - make.right.equalTo(row.mas_right).offset(-insetRight); - make.top.bottom.equalTo(row); - }]; - [centerContainer mas_makeConstraints:^(MASConstraintMaker *make) { - make.centerX.equalTo(row); - make.top.bottom.equalTo(row); - make.left.greaterThanOrEqualTo(leftContainer.mas_right).offset(gap); - make.right.lessThanOrEqualTo(rightContainer.mas_left).offset(-gap); - }]; - - if (leftItems.count == 0) { - [leftContainer mas_makeConstraints:^(MASConstraintMaker *make) { - make.width.mas_equalTo(0); - }]; - } - if (centerItems.count == 0) { - [centerContainer mas_makeConstraints:^(MASConstraintMaker *make) { - make.width.mas_equalTo(0); - }]; - } - if (rightItems.count == 0) { - [rightContainer mas_makeConstraints:^(MASConstraintMaker *make) { - make.width.mas_equalTo(0); - }]; - } - - [self kb_buildButtonsInContainer:leftContainer - items:leftItems - gap:gap - insetLeft:0 - insetRight:0 - alignCenter:NO - isTopLevelRow:NO]; - [self kb_buildButtonsInContainer:centerContainer - items:centerItems - gap:gap - insetLeft:0 - insetRight:0 - alignCenter:NO - isTopLevelRow:NO]; - [self kb_buildButtonsInContainer:rightContainer - items:rightItems - gap:gap - insetLeft:0 - insetRight:0 - alignCenter:NO - isTopLevelRow:NO]; - return; - } - - BOOL alignCenter = [rowConfig.align.lowercaseString isEqualToString:@"center"]; - [self kb_buildButtonsInContainer:row - items:[rowConfig resolvedItems] - gap:gap - insetLeft:insetLeft - insetRight:insetRight - alignCenter:alignCenter - isTopLevelRow:YES]; -} - -- (void)kb_buildButtonsInContainer:(UIView *)container - items:(NSArray *)items - gap:(CGFloat)gap - insetLeft:(CGFloat)insetLeft - insetRight:(CGFloat)insetRight - alignCenter:(BOOL)alignCenter - isTopLevelRow:(BOOL)isTopLevelRow { - if (items.count == 0) { return; } - - UIView *leftSpacer = nil; - UIView *rightSpacer = nil; - if (alignCenter) { - leftSpacer = [UIView new]; - rightSpacer = [UIView new]; - [container addSubview:leftSpacer]; - [container addSubview:rightSpacer]; - [leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) { - make.left.equalTo(container.mas_left).offset(insetLeft); - make.top.bottom.equalTo(container); - }]; - [rightSpacer mas_makeConstraints:^(MASConstraintMaker *make) { - make.right.equalTo(container.mas_right).offset(-insetRight); - make.top.bottom.equalTo(container); - }]; - [leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) { - make.width.equalTo(rightSpacer); - }]; - } - - BOOL usingUniformWidth = (self.kb_uniformCharKeyWidth > 0.0); - BOOL allCharacterKeys = YES; // 跟踪该容器内是否全部为字符键 - - KBKeyButton *previous = nil; - KBKeyButton *firstCharBtn = nil; // 用于非统一模式下的行内等宽约束 - for (KBKeyboardRowItem *item in items) { - KBKeyButton *btn = [self kb_buttonForItem:item]; - if (!btn) { continue; } - [container addSubview:btn]; - - [btn mas_makeConstraints:^(MASConstraintMaker *make) { - make.top.bottom.equalTo(container); - if (previous) { - make.left.equalTo(previous.mas_right).offset(gap); - } else { - if (leftSpacer) { - make.left.equalTo(leftSpacer.mas_right).offset(gap); - } else { - make.left.equalTo(container.mas_left).offset(insetLeft); - } - } - }]; - - // 字符键(letter/digit/sym)使用等宽约束,自动分配可用空间; - // 功能键(shift/backspace/mode 等)使用固定宽度。 - BOOL isCharacterKey = [item.itemId hasPrefix:@"letter:"] || - [item.itemId hasPrefix:@"digit:"] || - [item.itemId hasPrefix:@"sym:"]; - - if (!isCharacterKey) { allCharacterKeys = NO; } - - if (isCharacterKey && usingUniformWidth) { - // 使用跨行统一的固定宽度 - CGFloat w = self.kb_uniformCharKeyWidth; - [btn mas_makeConstraints:^(MASConstraintMaker *make) { - make.width.mas_equalTo(w); - }]; - } else if (isCharacterKey) { - // 行内等宽:所有字符键与第一个字符键等宽 - if (firstCharBtn) { - [btn mas_makeConstraints:^(MASConstraintMaker *make) { - make.width.equalTo(firstCharBtn); - }]; - } else { - firstCharBtn = btn; - } - } else { - CGFloat width = [self kb_widthForItem:item key:btn.key]; - if (width > 0.0) { - [btn mas_makeConstraints:^(MASConstraintMaker *make) { - make.width.mas_equalTo(width); - }]; - } - } - - previous = btn; - } - - if (!previous) { return; } - - // 当使用统一宽度且顶层行全部为字符键时,跳过右锚约束以实现左对齐(列对齐) - BOOL skipRightAnchor = isTopLevelRow && usingUniformWidth && allCharacterKeys; - - if (!skipRightAnchor) { - [previous mas_makeConstraints:^(MASConstraintMaker *make) { - if (rightSpacer) { - make.right.equalTo(rightSpacer.mas_left).offset(-gap); - } else { - make.right.equalTo(container.mas_right).offset(-insetRight); - } - }]; - } -} - -- (void)buildRow:(UIView *)row withKeys:(NSArray *)keys { - [self buildRow:row withKeys:keys edgeSpacerMultiplier:0.0]; -} - -- (void)buildRow:(UIView *)row - withKeys:(NSArray *)keys -edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier { - - // 第 4 行(底部控制行)使用单独的布局规则: - // 123/ABC、Emoji、Send 给定尺寸,Space 自动吃掉剩余宽度。 - BOOL isBottomControlRow = [self kb_isBottomControlRowWithKeys:keys]; - - CGFloat spacing = 0; // 键与键之间的间距 - UIView *previous = nil; - UIView *leftSpacer = nil; - UIView *rightSpacer = nil; - - if (edgeSpacerMultiplier > 0.0) { - leftSpacer = [UIView new]; - rightSpacer = [UIView new]; - leftSpacer.backgroundColor = [UIColor clearColor]; - rightSpacer.backgroundColor = [UIColor clearColor]; - [row addSubview:leftSpacer]; - [row addSubview:rightSpacer]; - [leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) { - make.left.equalTo(row.mas_left).offset(kKBRowHorizontalInset); - make.centerY.equalTo(row); - make.height.mas_equalTo(1); - }]; - [rightSpacer mas_makeConstraints:^(MASConstraintMaker *make) { - make.right.equalTo(row.mas_right).offset(-kKBRowHorizontalInset); - make.centerY.equalTo(row); - make.height.mas_equalTo(1); - }]; - } - - for (NSInteger i = 0; i < keys.count; i++) { - KBKey *key = keys[i]; - KBKeyButton *btn = [[KBKeyButton alloc] init]; - btn.key = key; - [btn setTitle:key.title forState:UIControlStateNormal]; - // 在设置完标题后,按当前皮肤应用图标与文字显隐 - [btn applyThemeForCurrentKey]; - [btn addTarget:self action:@selector(onKeyTapped:) forControlEvents:UIControlEventTouchDown]; - [row addSubview:btn]; - - if (key.type == KBKeyTypeBackspace) { - [self.backspaceHandler bindDeleteButton:btn showClearLabel:YES]; - } - - // Shift 按钮选中态随大小写状态变化 - if (key.type == KBKeyTypeShift) { - btn.selected = self.shiftOn; - } - - [btn mas_makeConstraints:^(MASConstraintMaker *make) { - make.top.bottom.equalTo(row); - if (previous) { - make.left.equalTo(previous.mas_right).offset(spacing); - } else { - if (leftSpacer) { - make.left.equalTo(leftSpacer.mas_right).offset(spacing); - } else { - make.left.equalTo(row.mas_left).offset(kKBRowHorizontalInset); - } - } - }]; - - // 字符键:等宽 - if (key.type == KBKeyTypeCharacter) { - if (previous && [previous isKindOfClass:[KBKeyButton class]]) { - KBKeyButton *prevBtn = (KBKeyButton *)previous; - if (prevBtn.key.type == KBKeyTypeCharacter) { - [btn mas_makeConstraints:^(MASConstraintMaker *make) { - make.width.equalTo(previous); - }]; - } - } - } else { - // special keys: 宽度在第二遍统一设置 - } - - previous = btn; - } - - // 右侧使用内边距或右占位 - if (previous) { - [previous mas_makeConstraints:^(MASConstraintMaker *make) { - if (rightSpacer) { - make.right.equalTo(rightSpacer.mas_left).offset(-spacing); - } else { - make.right.equalTo(row.mas_right).offset(-kKBRowHorizontalInset); - } - }]; - } - - // 底部控制行:在第一轮已完成左右约束的前提下,仅给 123/ABC、Emoji、Send 指定宽度, - // Space 不加宽度约束,让其自动填充剩余空间。 - if (isBottomControlRow) { - [self kb_applyBottomControlRowWidthInRow:row]; - return; - } - - // 第二遍:以首个字符键为基准,统一设置特殊键宽度倍数 - KBKeyButton *firstChar = nil; - BOOL hasCharacterInRow = NO; - for (UIView *v in row.subviews) { - if (![v isKindOfClass:[KBKeyButton class]]) continue; - KBKeyButton *b = (KBKeyButton *)v; - if (b.key.type == KBKeyTypeCharacter) { - firstChar = b; - hasCharacterInRow = YES; - break; - } - } - // 若该行没有字符键(例如底部控制行之外的特殊行),则使用行内第一个按钮作为基准宽度 - if (!firstChar) { - for (UIView *v in row.subviews) { - if ([v isKindOfClass:[KBKeyButton class]]) { - firstChar = (KBKeyButton *)v; - break; - } - } - } - if (firstChar) { - // 如果该行本身没有字符键(如底部控制行),且基准按钮是 123/ABC/#+= 等, - // 也将其约束为 1:1,避免 123/ABC 不是正方形。 - if (!hasCharacterInRow && - (firstChar.key.type == KBKeyTypeModeChange || - firstChar.key.type == KBKeyTypeSymbolsToggle || - firstChar.key.type == KBKeyTypeCustom)) { - [firstChar mas_makeConstraints:^(MASConstraintMaker *make) { - make.width.equalTo(firstChar.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier); - }]; - } - - for (UIView *v in row.subviews) { - if (![v isKindOfClass:[KBKeyButton class]]) continue; - KBKeyButton *b = (KBKeyButton *)v; - - // 避免对基准按钮自身添加 self == self * k 的无效约束 - if (b == firstChar) continue; - if (b.key.type == KBKeyTypeCharacter) continue; - - BOOL isBottomModeKey = (b.key.type == KBKeyTypeModeChange) || - (b.key.type == KBKeyTypeSymbolsToggle) || - (b.key.type == KBKeyTypeCustom); - // 一类键强制近似正方形(宽 ~ 高) - if (b.key.type == KBKeyTypeShift || - b.key.type == KBKeyTypeBackspace || - isBottomModeKey) { - [b mas_makeConstraints:^(MASConstraintMaker *make) { - make.width.equalTo(b.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier); - }]; - continue; - } - - CGFloat multiplier = 1.5; - - // Space:宽度更大 - if (b.key.type == KBKeyTypeSpace) { - multiplier = kKBSpaceWidthMultiplier; - } - // Send 按钮:宽度为基准键的 2.4 倍 - else if (b.key.type == KBKeyTypeReturn) { - multiplier = kKBReturnWidthMultiplier; - } - // 其它特殊键(如 Globe)保持适度放大 - else if (b.key.type == KBKeyTypeGlobe) { - multiplier = 1.5; - } - - [b mas_makeConstraints:^(MASConstraintMaker *make) { - make.width.equalTo(firstChar).multipliedBy(multiplier); - }]; - } - - // 如果有左右占位,则把占位宽度设置为字符键宽度的一定倍数,以实现整体居中; - // 同时强约束左右占位宽度相等,避免在某些系统上由于布局冲突导致只压缩一侧, - // 出现“左侧有空隙,右侧无空隙”的情况。 - if (leftSpacer && rightSpacer) { - // 1) 左右占位宽度必须相等 - [leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) { - make.width.equalTo(rightSpacer); - }]; - // 2) 同时都接近字符键宽度的 edgeSpacerMultiplier 倍数 - [rightSpacer mas_makeConstraints:^(MASConstraintMaker *make) { - make.width.equalTo(firstChar).multipliedBy(edgeSpacerMultiplier); - }]; - } - } -} - -#pragma mark - Row Helpers (Bottom Control Row) - -// 判断是否为底部控制行:包含 Space + Return,且有 ModeChange/SymbolsToggle, -// 并且不再含字符键。 -- (BOOL)kb_isBottomControlRowWithKeys:(NSArray *)keys { - BOOL hasSpace = NO; - BOOL hasReturn = NO; - BOOL hasModeOrSymbols = NO; - for (KBKey *k in keys) { - if (k.type == KBKeyTypeCharacter) { - return NO; - } - if (k.type == KBKeyTypeSpace) { - hasSpace = YES; - } else if (k.type == KBKeyTypeReturn) { - hasReturn = YES; - } else if (k.type == KBKeyTypeModeChange || k.type == KBKeyTypeSymbolsToggle) { - hasModeOrSymbols = YES; - } - } - return hasSpace && hasReturn && hasModeOrSymbols; -} - -// 为底部控制行设置宽度: -// - 123/ABC、Emoji:正方形(宽 = 行高 * multiplier) -// - Send:宽 = 模式键宽度的 2 倍 -// - Space:不加宽度约束,依靠左右约束自动填充剩余空间。 -- (void)kb_applyBottomControlRowWidthInRow:(UIView *)row { - KBKeyButton *modeBtn = nil; - NSMutableArray *customButtons = [NSMutableArray array]; - KBKeyButton *spaceBtn = nil; - KBKeyButton *retBtn = nil; - - for (UIView *v in row.subviews) { - if (![v isKindOfClass:[KBKeyButton class]]) continue; - KBKeyButton *b = (KBKeyButton *)v; - switch (b.key.type) { - case KBKeyTypeModeChange: - case KBKeyTypeSymbolsToggle: - modeBtn = b; - break; - case KBKeyTypeCustom: - [customButtons addObject:b]; - break; - case KBKeyTypeSpace: - spaceBtn = b; - break; - case KBKeyTypeReturn: - retBtn = b; - break; - default: - break; - } - } - - if (!modeBtn || customButtons.count == 0 || !spaceBtn || !retBtn) { - return; - } - - [modeBtn mas_makeConstraints:^(MASConstraintMaker *make) { - make.width.equalTo(row.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier); - }]; - for (KBKeyButton *custom in customButtons) { - [custom mas_makeConstraints:^(MASConstraintMaker *make) { - make.width.equalTo(row.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier); - }]; - } - [retBtn mas_makeConstraints:^(MASConstraintMaker *make) { - make.width.equalTo(modeBtn.mas_width).multipliedBy(2.0); - }]; - // Space 不设置宽度;通过此前已建立的左右约束自动占满剩余宽度。 -} - -#pragma mark - Uniform Character Key Width - -/// 计算跨行统一字符键宽度:遍历每行各自的 insets/gap/非字符键宽度, -/// 取各行可用字符键宽度的最小值,确保所有行都能容纳。 -/// 当各行有效宽度相同时返回 0(无需统一)。 -- (CGFloat)kb_calculateUniformCharKeyWidthForRows:(NSArray *)rows { - CGFloat minWidth = CGFLOAT_MAX; - CGFloat maxWidth = 0.0; - BOOL hasCharRow = NO; - CGFloat containerWidth = KBScreenWidth(); - - for (KBKeyboardRowConfig *row in rows) { - if (row.segments) { continue; } // 跳过分段行 - NSArray *items = [row resolvedItems]; - NSUInteger charCount = 0; - CGFloat nonCharWidth = 0.0; - for (KBKeyboardRowItem *item in items) { - BOOL isChar = [item.itemId hasPrefix:@"letter:"] || - [item.itemId hasPrefix:@"digit:"] || - [item.itemId hasPrefix:@"sym:"]; - if (isChar) { - charCount++; - } else { - KBKey *key = [self kb_keyForItemId:item.itemId]; - CGFloat w = [self kb_widthForItem:item key:key]; - nonCharWidth += w; - } - } - if (charCount == 0) { continue; } // 跳过无字符键的行(如底部控制行) - hasCharRow = YES; - - // 使用每行各自配置的 insets 和 gap - CGFloat gap = [self kb_gapForRow:row]; - CGFloat insetLeft = [self kb_insetLeftForRow:row]; - CGFloat insetRight = [self kb_insetRightForRow:row]; - CGFloat totalGaps = (items.count > 1) ? (items.count - 1) * gap : 0.0; - CGFloat available = containerWidth - insetLeft - insetRight - totalGaps - nonCharWidth; - CGFloat width = available / charCount; - - if (width < minWidth) { minWidth = width; } - if (width > maxWidth) { maxWidth = width; } - } - - if (!hasCharRow || minWidth <= 0.0 || minWidth >= CGFLOAT_MAX) { return 0.0; } - // 各行有效宽度相同时无需统一 - if (fabs(maxWidth - minWidth) < 0.5) { return 0.0; } - return minWidth; -} - -#pragma mark - Config Helpers - -- (KBKeyboardLayoutConfig *)kb_layoutConfig { - if (!self.layoutConfig) { - self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig]; - } - return self.layoutConfig; -} - -- (CGFloat)kb_rowSpacingForLayout:(KBKeyboardLayout *)layout { - KBKeyboardLayoutConfig *config = [self kb_layoutConfig]; - NSNumber *layoutSpacing = layout.rowSpacing; - return [self kb_metricValue:layoutSpacing fallback:config.metrics.rowSpacing defaultValue:8.0]; -} - -- (CGFloat)kb_topInsetForLayout:(KBKeyboardLayout *)layout { - KBKeyboardLayoutConfig *config = [self kb_layoutConfig]; - NSNumber *layoutInset = layout.topInset; - return [self kb_metricValue:layoutInset fallback:config.metrics.topInset defaultValue:8.0]; -} - -- (CGFloat)kb_bottomInsetForLayout:(KBKeyboardLayout *)layout { - KBKeyboardLayoutConfig *config = [self kb_layoutConfig]; - NSNumber *layoutInset = layout.bottomInset; - return [self kb_metricValue:layoutInset fallback:config.metrics.bottomInset defaultValue:6.0]; -} - -- (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 buildKeysForCurrentLayout]; - if (self.keysForRows.count < 4) { return; } - if (self.rowViews.count < 4) { return; } - - [self buildRow:self.rowViews[0] withKeys:self.keysForRows[0]]; - - CGFloat row2Spacer = (self.layoutStyle == KBKeyboardLayoutStyleLetters) - ? kKBLettersRow2EdgeSpacerMultiplier : 0.0; - [self buildRow:self.rowViews[1] withKeys:self.keysForRows[1] edgeSpacerMultiplier:row2Spacer]; - - [self buildRow:self.rowViews[2] withKeys:self.keysForRows[2]]; - [self buildRow:self.rowViews[3] withKeys:self.keysForRows[3]]; -} - -- (CGFloat)kb_scaledValue:(CGFloat)designValue { - KBKeyboardLayoutConfig *config = [self kb_layoutConfig]; - if (config) { - return [config scaledValue:designValue]; - } - return KBFit(designValue); -} - -- (CGFloat)kb_numberValue:(NSNumber *)value defaultValue:(CGFloat)defaultValue { - if ([value isKindOfClass:[NSNumber class]]) { - return value.doubleValue; - } - return defaultValue; -} - -- (CGFloat)kb_metricValue:(NSNumber *)value fallback:(NSNumber *)fallback defaultValue:(CGFloat)defaultValue { - CGFloat v = [self kb_numberValue:value defaultValue:-1.0]; - if (v < 0.0) { - v = [self kb_numberValue:fallback defaultValue:defaultValue]; - } - if (v < 0.0) { - v = defaultValue; - } - return [self kb_scaledValue:v]; -} - -- (CGFloat)kb_rowHeightForRow:(KBKeyboardRowConfig *)row { - KBKeyboardLayoutConfig *config = [self kb_layoutConfig]; - NSNumber *height = row.height ?: config.metrics.keyHeight; - CGFloat value = [self kb_numberValue:height defaultValue:40.0]; - return [self kb_scaledValue:value]; -} - -- (CGFloat)kb_gapForRow:(KBKeyboardRowConfig *)row { - KBKeyboardLayoutConfig *config = [self kb_layoutConfig]; - return [self kb_metricValue:row.gap fallback:config.metrics.gap defaultValue:5.0]; -} - -- (CGFloat)kb_insetLeftForRow:(KBKeyboardRowConfig *)row { - KBKeyboardLayoutConfig *config = [self kb_layoutConfig]; - return [self kb_metricValue:row.insetLeft fallback:config.metrics.edgeInset defaultValue:0.0]; -} - -- (CGFloat)kb_insetRightForRow:(KBKeyboardRowConfig *)row { - KBKeyboardLayoutConfig *config = [self kb_layoutConfig]; - return [self kb_metricValue:row.insetRight fallback:config.metrics.edgeInset defaultValue:0.0]; -} - -- (KBKeyButton *)kb_buttonForItem:(KBKeyboardRowItem *)item { - if (item.itemId.length == 0) { return nil; } - KBKeyboardKeyDef *def = [[self kb_layoutConfig] keyDefForIdentifier:item.itemId]; - KBKey *key = [self kb_keyForItemId:item.itemId]; - if (!key) { return nil; } - - KBKeyButton *btn = [[KBKeyButton alloc] init]; - btn.key = key; - [btn setTitle:key.title forState:UIControlStateNormal]; - - UIColor *bgColor = [self kb_backgroundColorForItem:item keyDef:def]; - if (bgColor) { - btn.customBackgroundColor = bgColor; - } - - CGFloat fontSize = [self kb_fontSizeForItem:item key:key]; - if (fontSize > 0.0) { - btn.titleLabel.font = [UIFont systemFontOfSize:fontSize weight:UIFontWeightSemibold]; - } - - [btn applyThemeForCurrentKey]; - [btn addTarget:self action:@selector(onKeyTapped:) forControlEvents:UIControlEventTouchDown]; - - if (key.type == KBKeyTypeBackspace) { - [self.backspaceHandler bindDeleteButton:btn showClearLabel:YES]; - } - if (key.type == KBKeyTypeShift) { - btn.selected = self.shiftOn; - } - [self kb_applySymbolIfNeededForButton:btn keyDef:def fontSize:fontSize]; - return btn; -} - -- (void)kb_applySymbolIfNeededForButton:(KBKeyButton *)button - keyDef:(KBKeyboardKeyDef *)def - fontSize:(CGFloat)fontSize { - if (!button || !def) { return; } - if (button.iconView.image != nil) { return; } - NSString *symbolName = button.isSelected ? def.selectedSymbolName : def.symbolName; - if (symbolName.length == 0) { return; } - - UIImage *image = [UIImage systemImageNamed:symbolName]; - if (!image) { return; } - - UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:fontSize weight:UIFontWeightSemibold]; - image = [image imageWithConfiguration:config]; - - button.iconView.image = image; - button.iconView.hidden = NO; - button.iconView.contentMode = UIViewContentModeCenter; - button.titleLabel.hidden = YES; - - UIColor *textColor = [KBSkinManager shared].current.keyTextColor ?: [UIColor blackColor]; - button.iconView.tintColor = button.isSelected ? [UIColor blackColor] : textColor; -} - -- (UIColor *)kb_backgroundColorForItem:(KBKeyboardRowItem *)item keyDef:(KBKeyboardKeyDef *)def { - NSString *hex = def.backgroundColor; - if (hex.length == 0) { - hex = [self kb_layoutConfig].defaultKeyBackground; - } - if (hex.length == 0) { return nil; } - return [KBSkinManager colorFromHexString:hex defaultColor:nil]; -} - -- (CGFloat)kb_metricWidthForKey:(NSString *)key { - KBKeyboardLayoutMetrics *m = [self kb_layoutConfig].metrics; - if ([key isEqualToString:@"letterWidth"]) { return m.letterWidth.doubleValue; } - if ([key isEqualToString:@"controlWidth"]) { return m.controlWidth.doubleValue; } - if ([key isEqualToString:@"sendWidth"]) { return m.sendWidth.doubleValue; } - if ([key isEqualToString:@"symbolsWideWidth"]) { return m.symbolsWideWidth.doubleValue; } - if ([key isEqualToString:@"symbolsSideWidth"]) { return m.symbolsSideWidth.doubleValue; } - return 0.0; -} - -- (CGFloat)kb_widthForItem:(KBKeyboardRowItem *)item key:(KBKey *)key { - CGFloat width = 0.0; - if (item.widthValue.doubleValue > 0.0) { - width = item.widthValue.doubleValue; - } else if (item.width.length > 0) { - if ([item.width.lowercaseString isEqualToString:@"flex"]) { - return 0.0; - } - width = [self kb_metricWidthForKey:item.width]; - if (width <= 0.0) { - width = item.width.doubleValue; - } - } - - if (width <= 0.0) { - KBKeyboardLayoutMetrics *m = [self kb_layoutConfig].metrics; - if ([item.itemId hasPrefix:@"letter:"] || - [item.itemId hasPrefix:@"digit:"] || - [item.itemId hasPrefix:@"sym:"]) { - width = m.letterWidth.doubleValue; - } else if (key.type == KBKeyTypeReturn) { - width = m.sendWidth.doubleValue; - } else if (key.type == KBKeyTypeSpace) { - return 0.0; - } else { - width = m.controlWidth.doubleValue; - } - } - - if (width <= 0.0) { - if ([item.itemId hasPrefix:@"letter:"] || - [item.itemId hasPrefix:@"digit:"] || - [item.itemId hasPrefix:@"sym:"]) { - width = 32.0; - } else if (key.type == KBKeyTypeReturn) { - width = 88.0; - } else if (key.type == KBKeyTypeSpace) { - return 0.0; - } else { - width = 41.0; - } - } - - return width > 0.0 ? [self kb_scaledValue:width] : 0.0; -} - -- (CGFloat)kb_fontSizeForItem:(KBKeyboardRowItem *)item key:(KBKey *)key { - NSString *fontKey = nil; - if ([item.itemId hasPrefix:@"letter:"]) { - fontKey = @"letter"; - } else if ([item.itemId hasPrefix:@"digit:"]) { - fontKey = @"digit"; - } else if ([item.itemId hasPrefix:@"sym:"]) { - fontKey = @"symbol"; - } else { - KBKeyboardKeyDef *def = [[self kb_layoutConfig] keyDefForIdentifier:item.itemId]; - fontKey = def.font; - } - - if (fontKey.length == 0) { - switch (key.type) { - case KBKeyTypeModeChange: - case KBKeyTypeSymbolsToggle: - fontKey = @"mode"; - break; - case KBKeyTypeSpace: - fontKey = @"space"; - break; - case KBKeyTypeReturn: - fontKey = @"send"; - break; - default: - fontKey = @"symbol"; - break; - } - } - - return [self kb_fontSizeForFontKey:fontKey]; -} - -- (CGFloat)kb_fontSizeForFontKey:(NSString *)fontKey { - KBKeyboardLayoutFonts *fonts = [self kb_layoutConfig].fonts; - CGFloat size = 0.0; - if ([fontKey isEqualToString:@"letter"]) { size = fonts.letter.doubleValue; } - else if ([fontKey isEqualToString:@"digit"]) { size = fonts.digit.doubleValue; } - else if ([fontKey isEqualToString:@"symbol"]) { size = fonts.symbol.doubleValue; } - else if ([fontKey isEqualToString:@"mode"]) { size = fonts.mode.doubleValue; } - else if ([fontKey isEqualToString:@"space"]) { size = fonts.space.doubleValue; } - else if ([fontKey isEqualToString:@"send"]) { size = fonts.send.doubleValue; } - if (size <= 0.0) { size = 18.0; } - return [self kb_scaledValue:size]; -} - -- (KBKey *)kb_keyForItemId:(NSString *)itemId { - if (itemId.length == 0) { return nil; } - KBKeyboardKeyDef *def = [[self kb_layoutConfig] keyDefForIdentifier:itemId]; - if (def) { - return [self kb_keyFromDef:def identifier:itemId]; - } - - NSRange range = [itemId rangeOfString:@":"]; - if (range.location != NSNotFound) { - NSString *prefix = [itemId substringToIndex:range.location]; - NSString *value = [itemId substringFromIndex:range.location + 1]; - if ([prefix isEqualToString:@"letter"]) { - if (value.length >= 1) { - return [self kb_letterKeyWithChar:value]; - } - return nil; - } - if ([prefix isEqualToString:@"digit"]) { - NSString *identifier = [NSString stringWithFormat:@"digit_%@", value]; - KBKey *k = [KBKey keyWithIdentifier:identifier title:value output:value type:KBKeyTypeCharacter]; - k.caseVariant = KBKeyCaseVariantNone; - return k; - } - if ([prefix isEqualToString:@"sym"]) { - NSString *identifier = [self kb_identifierForSymbol:value]; - KBKey *k = [KBKey keyWithIdentifier:identifier title:value output:value type:KBKeyTypeCharacter]; - k.caseVariant = KBKeyCaseVariantNone; - return k; - } - } - return nil; -} - -- (KBKey *)kb_keyFromDef:(KBKeyboardKeyDef *)def identifier:(NSString *)identifier { - KBKeyType type = [self kb_keyTypeForDef:def]; - NSString *title = def.title ?: @""; - if (type == KBKeyTypeShift && self.shiftOn && def.selectedTitle.length > 0) { - title = def.selectedTitle; - } - NSString *output = @""; - switch (type) { - case KBKeyTypeSpace: - output = @" "; - break; - case KBKeyTypeReturn: - output = @"\n"; - break; - default: - output = @""; - break; - } - - NSString *finalId = identifier; - if ([identifier isEqualToString:@"emoji"]) { - finalId = KBKeyIdentifierEmojiPanel; - } else if ([identifier isEqualToString:@"send"]) { - finalId = @"return"; - } - - KBKey *k = [KBKey keyWithIdentifier:finalId title:title output:output type:type]; - if (type == KBKeyTypeShift) { - k.caseVariant = self.shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower; - } else { - k.caseVariant = KBKeyCaseVariantNone; - } - return k; -} - -- (KBKeyType)kb_keyTypeForDef:(KBKeyboardKeyDef *)def { - NSString *type = def.type.lowercaseString; - if ([type isEqualToString:@"shift"]) return KBKeyTypeShift; - if ([type isEqualToString:@"backspace"]) return KBKeyTypeBackspace; - if ([type isEqualToString:@"mode"]) return KBKeyTypeModeChange; - if ([type isEqualToString:@"symbolstoggle"]) return KBKeyTypeSymbolsToggle; - if ([type isEqualToString:@"space"]) return KBKeyTypeSpace; - if ([type isEqualToString:@"return"]) return KBKeyTypeReturn; - if ([type isEqualToString:@"globe"]) return KBKeyTypeGlobe; - return KBKeyTypeCustom; -} - -- (NSString *)kb_identifierForSymbol:(NSString *)symbol { - if (symbol.length == 0) { return nil; } - static NSDictionary *map = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - map = @{ - @"-": @"sym_minus", - @"/": @"sym_slash", - @":": @"sym_colon", - @";": @"sym_semicolon", - @"(": @"sym_paren_l", - @")": @"sym_paren_r", - @"¥": @"sym_money", - @"¥": @"sym_money", - @"&": @"sym_amp", - @"@": @"sym_at", - @"\"": @"sym_quote_double", - @"“": @"sym_quote_double", - @"”": @"sym_quote_double", - @".": @"sym_dot", - @"。": @"sym_chinese_dot", - @",": @"sym_comma", - @"、": @"sym_dun", - @"?": @"sym_question", - @"!": @"sym_exclam", - @"'": @"sym_quote_single", - @"‘": @"sym_quote_single", - @"’": @"sym_quote_single", - @"[": @"sym_bracket_l", - @"]": @"sym_bracket_r", - @"{": @"sym_brace_l", - @"}": @"sym_brace_r", - @"「": @"sym_corner_l", - @"」": @"sym_corner_r", - @"#": @"sym_hash", - @"%": @"sym_percent", - @"^": @"sym_caret", - @"*": @"sym_asterisk", - @"+": @"sym_plus", - @"=": @"sym_equal", - @"_": @"sym_underscore", - @"\\": @"sym_backslash", - @"|": @"sym_pipe", - @"~": @"sym_tilde", - @"<": @"sym_lt", - @">": @"sym_gt", - @"€": @"sym_euro", - @"$": @"sym_dollar", - @"£": @"sym_pound", - @"·": @"sym_bullet", - @"^_^": @"sym_face", - @"—": @"sym_emdash", - @"«": @"sym_guillemet_l", - @"»": @"sym_guillemet_r", - @"《": @"sym_book_title_l", - @"》": @"sym_book_title_r", - @"...": @"sym_ellipsis" - }; - }); - return map[symbol]; -} - -#pragma mark - Actions - -- (void)onKeyTapped:(KBKeyButton *)sender { - KBKey *key = sender.key; - if (key.type == KBKeyTypeShift) { - self.shiftOn = !self.shiftOn; - [self reloadKeys]; - return; - } - if (key.type == KBKeyTypeSymbolsToggle) { - // 在数字布局内切换 123 <-> #+= - self.symbolsMoreOn = !self.symbolsMoreOn; - [self reloadKeys]; - return; - } - if ([self.delegate respondsToSelector:@selector(keyboardView:didTapKey:)]) { - [self.delegate keyboardView:self didTapKey:key]; - } -} - -// 在字符键按下时,显示一个上方气泡预览(类似系统键盘)。 -- (void)showPreviewForButton:(KBKeyButton *)button { - KBKey *key = button.key; - if (key.type != KBKeyTypeCharacter) return; - - if (!self.previewView) { - self.previewView = [[KBKeyPreviewView alloc] initWithFrame:CGRectZero]; - self.previewView.hidden = YES; - [self addSubview:self.previewView]; - } - - [self.previewView configureWithKey:key icon:button.iconView.image]; - - // 计算预览视图位置:在按钮上方稍微偏上 - CGRect btnFrameInSelf = [button convertRect:button.bounds toView:self]; -// CGFloat previewWidth = MAX(CGRectGetWidth(btnFrameInSelf) * 1.4, 42.0); - CGFloat previewWidth = 42; - CGFloat previewHeight = CGRectGetHeight(btnFrameInSelf) * 1.2; - CGFloat centerX = CGRectGetMidX(btnFrameInSelf); - CGFloat centerY = CGRectGetMinY(btnFrameInSelf) - previewHeight * 0.6; - - // 修复:原来写死 40,这里用真正计算出的 previewWidth - self.previewView.frame = CGRectMake(0, 0, previewWidth, previewHeight); - self.previewView.center = CGPointMake(centerX, centerY); - self.previewView.alpha = 0.0; - self.previewView.hidden = NO; - - [UIView animateWithDuration:kKBPreviewShowDuration - delay:0 - options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseOut - animations:^{ - self.previewView.alpha = 1.0; - } - completion:nil]; -} - -- (void)hidePreview { - if (!self.previewView || self.previewView.isHidden) return; - [UIView animateWithDuration:kKBPreviewHideDuration - delay:0 - options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn - animations:^{ - self.previewView.alpha = 0.0; - } - completion:^(BOOL finished) { - self.previewView.hidden = YES; - }]; -} - -#pragma mark - Lazy - -- (NSMutableArray *)rowViews { - if (!_rowViews) _rowViews = [NSMutableArray array]; - return _rowViews; -} - -@end diff --git a/CustomKeyboard/View/KBKeyboardView/KBKeyboardInputHandler.h b/CustomKeyboard/View/KBKeyboardView/KBKeyboardInputHandler.h new file mode 100644 index 0000000..7a69739 --- /dev/null +++ b/CustomKeyboard/View/KBKeyboardView/KBKeyboardInputHandler.h @@ -0,0 +1,27 @@ +// +// KBKeyboardInputHandler.h +// CustomKeyboard +// +// Key tap handling helper. +// + +#import + +@class KBKey; + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^KBKeyboardActionHandler)(void); +typedef void (^KBKeyboardKeyTapHandler)(KBKey *key); + +@interface KBKeyboardInputHandler : NSObject + +@property (nonatomic, copy, nullable) KBKeyboardActionHandler onToggleShift; +@property (nonatomic, copy, nullable) KBKeyboardActionHandler onToggleSymbols; +@property (nonatomic, copy, nullable) KBKeyboardKeyTapHandler onKeyTapped; + +- (BOOL)handleKeyTap:(KBKey *)key; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CustomKeyboard/View/KBKeyboardView/KBKeyboardInputHandler.m b/CustomKeyboard/View/KBKeyboardView/KBKeyboardInputHandler.m new file mode 100644 index 0000000..6f68832 --- /dev/null +++ b/CustomKeyboard/View/KBKeyboardView/KBKeyboardInputHandler.m @@ -0,0 +1,26 @@ +// +// KBKeyboardInputHandler.m +// CustomKeyboard +// + +#import "KBKeyboardInputHandler.h" +#import "KBKey.h" + +@implementation KBKeyboardInputHandler + +- (BOOL)handleKeyTap:(KBKey *)key { + if (!key) { return NO; } + switch (key.type) { + case KBKeyTypeShift: + if (self.onToggleShift) { self.onToggleShift(); } + return YES; + case KBKeyTypeSymbolsToggle: + if (self.onToggleSymbols) { self.onToggleSymbols(); } + return YES; + default: + if (self.onKeyTapped) { self.onKeyTapped(key); } + return YES; + } +} + +@end diff --git a/CustomKeyboard/View/KBKeyboardView/KBKeyboardInteractionHandler.h b/CustomKeyboard/View/KBKeyboardView/KBKeyboardInteractionHandler.h new file mode 100644 index 0000000..8a80565 --- /dev/null +++ b/CustomKeyboard/View/KBKeyboardView/KBKeyboardInteractionHandler.h @@ -0,0 +1,28 @@ +// +// KBKeyboardInteractionHandler.h +// CustomKeyboard +// + +#import +#import + +@class KBKeyButton; + +NS_ASSUME_NONNULL_BEGIN + +@interface KBKeyboardInteractionHandler : NSObject + +- (UIView *)resolveHitView:(UIView *)hitView + point:(CGPoint)point + container:(UIView *)container + rowViews:(NSArray *)rowViews; + +- (NSArray *)collectKeyButtonsInView:(UIView *)view; + +- (void)showPreviewForButton:(KBKeyButton *)button inContainer:(UIView *)container; +- (void)hidePreview; +- (void)bringPreviewToFrontIfNeededInContainer:(UIView *)container; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CustomKeyboard/View/KBKeyboardView/KBKeyboardInteractionHandler.m b/CustomKeyboard/View/KBKeyboardView/KBKeyboardInteractionHandler.m new file mode 100644 index 0000000..0307d37 --- /dev/null +++ b/CustomKeyboard/View/KBKeyboardView/KBKeyboardInteractionHandler.m @@ -0,0 +1,156 @@ +// +// KBKeyboardInteractionHandler.m +// CustomKeyboard +// + +#import "KBKeyboardInteractionHandler.h" +#import "KBKeyButton.h" +#import "KBKey.h" +#import "KBKeyPreviewView.h" + +static const NSTimeInterval kKBPreviewShowDuration = 0.08; +static const NSTimeInterval kKBPreviewHideDuration = 0.06; + +@interface KBKeyboardInteractionHandler () +@property (nonatomic, strong) KBKeyPreviewView *previewView; +@end + +@implementation KBKeyboardInteractionHandler + +- (UIView *)resolveHitView:(UIView *)hitView + point:(CGPoint)point + container:(UIView *)container + rowViews:(NSArray *)rowViews { + if ([hitView isKindOfClass:[KBKeyButton class]]) { + return hitView; + } + if ([self isHitInsideKeyRows:hitView rowViews:rowViews]) { + KBKeyButton *btn = [self nearestKeyButtonForPoint:point + container:container + rowViews:rowViews]; + if (btn) { return btn; } + } + return hitView; +} + +- (NSArray *)collectKeyButtonsInView:(UIView *)view { + if (!view) { return @[]; } + NSMutableArray *buttons = [NSMutableArray array]; + [self collectKeyButtonsInView:view into:buttons]; + return buttons.copy; +} + +- (void)showPreviewForButton:(KBKeyButton *)button inContainer:(UIView *)container { + if (!button || !container) { return; } + KBKey *key = button.key; + if (key.type != KBKeyTypeCharacter) return; + + if (!self.previewView) { + self.previewView = [[KBKeyPreviewView alloc] initWithFrame:CGRectZero]; + self.previewView.hidden = YES; + [container addSubview:self.previewView]; + } else if (self.previewView.superview != container) { + [container addSubview:self.previewView]; + } + + [self.previewView configureWithKey:key icon:button.iconView.image]; + + // 计算预览视图位置:在按钮上方稍微偏上 + CGRect btnFrameInSelf = [button convertRect:button.bounds toView:container]; + CGFloat previewWidth = 42; + CGFloat previewHeight = CGRectGetHeight(btnFrameInSelf) * 1.2; + CGFloat centerX = CGRectGetMidX(btnFrameInSelf); + CGFloat centerY = CGRectGetMinY(btnFrameInSelf) - previewHeight * 0.6; + + self.previewView.frame = CGRectMake(0, 0, previewWidth, previewHeight); + self.previewView.center = CGPointMake(centerX, centerY); + self.previewView.alpha = 0.0; + self.previewView.hidden = NO; + + [UIView animateWithDuration:kKBPreviewShowDuration + delay:0 + options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseOut + animations:^{ + self.previewView.alpha = 1.0; + } + completion:nil]; +} + +- (void)hidePreview { + if (!self.previewView || self.previewView.isHidden) return; + [UIView animateWithDuration:kKBPreviewHideDuration + delay:0 + options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn + animations:^{ + self.previewView.alpha = 0.0; + } + completion:^(BOOL finished) { + self.previewView.hidden = YES; + }]; +} + +- (void)bringPreviewToFrontIfNeededInContainer:(UIView *)container { + if (!container) { return; } + if (self.previewView && self.previewView.superview == container) { + [container bringSubviewToFront:self.previewView]; + } +} + +#pragma mark - Private + +- (BOOL)isHitInsideKeyRows:(UIView *)hitView rowViews:(NSArray *)rowViews { + if (!hitView) { return NO; } + if ([rowViews containsObject:hitView]) { return YES; } + for (UIView *row in rowViews) { + if ([hitView isDescendantOfView:row]) { return YES; } + } + return NO; +} + +- (KBKeyButton *)nearestKeyButtonForPoint:(CGPoint)point + container:(UIView *)container + rowViews:(NSArray *)rowViews { + if (!container) { return nil; } + KBKeyButton *best = nil; + CGFloat bestDistance = CGFLOAT_MAX; + + UIView *targetRow = nil; + for (UIView *row in rowViews) { + CGRect rowFrame = [container convertRect:row.bounds fromView:row]; + if (CGRectContainsPoint(rowFrame, point)) { + targetRow = row; + break; + } + } + + NSArray *candidateRows = targetRow ? @[targetRow] : rowViews; + for (UIView *row in candidateRows) { + NSArray *buttons = [self collectKeyButtonsInView:row]; + for (KBKeyButton *btn in buttons) { + CGRect frame = [container convertRect:btn.frame fromView:btn.superview]; + CGFloat dx = point.x - CGRectGetMidX(frame); + CGFloat dy = point.y - CGRectGetMidY(frame); + CGFloat dist = (dx * dx) + (dy * dy); + if (dist < bestDistance) { + bestDistance = dist; + best = btn; + } + } + } + return best; +} + +- (void)collectKeyButtonsInView:(UIView *)view + into:(NSMutableArray *)buttons { + for (UIView *sub in view.subviews) { + if ([sub isKindOfClass:[KBKeyButton class]]) { + [buttons addObject:(KBKeyButton *)sub]; + continue; + } + if (sub.subviews.count > 0) { + [self collectKeyButtonsInView:sub into:buttons]; + } + } +} + +@end diff --git a/CustomKeyboard/View/KBKeyboardView/KBKeyboardKeyFactory.h b/CustomKeyboard/View/KBKeyboardView/KBKeyboardKeyFactory.h new file mode 100644 index 0000000..3466548 --- /dev/null +++ b/CustomKeyboard/View/KBKeyboardView/KBKeyboardKeyFactory.h @@ -0,0 +1,28 @@ +// +// KBKeyboardKeyFactory.h +// CustomKeyboard +// +// Key creation helper for keyboard layouts. +// + +#import + +@class KBKeyboardLayoutConfig; +@class KBKeyboardKeyDef; +@class KBKey; + +NS_ASSUME_NONNULL_BEGIN + +@interface KBKeyboardKeyFactory : NSObject + +- (instancetype)initWithLayoutConfig:(KBKeyboardLayoutConfig *)layoutConfig; + +- (nullable KBKey *)keyForItemId:(NSString *)itemId shiftOn:(BOOL)shiftOn; +- (nullable KBKey *)keyFromDef:(KBKeyboardKeyDef *)def + identifier:(NSString *)identifier + shiftOn:(BOOL)shiftOn; +- (nullable KBKey *)letterKeyWithChar:(NSString *)charString shiftOn:(BOOL)shiftOn; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CustomKeyboard/View/KBKeyboardView/KBKeyboardKeyFactory.m b/CustomKeyboard/View/KBKeyboardView/KBKeyboardKeyFactory.m new file mode 100644 index 0000000..5c4e8be --- /dev/null +++ b/CustomKeyboard/View/KBKeyboardView/KBKeyboardKeyFactory.m @@ -0,0 +1,186 @@ +// +// KBKeyboardKeyFactory.m +// CustomKeyboard +// + +#import "KBKeyboardKeyFactory.h" +#import "KBKeyboardLayoutConfig.h" +#import "KBKey.h" + +@interface KBKeyboardKeyFactory () +@property (nonatomic, strong) KBKeyboardLayoutConfig *layoutConfig; +@end + +@implementation KBKeyboardKeyFactory + +- (instancetype)initWithLayoutConfig:(KBKeyboardLayoutConfig *)layoutConfig { + self = [super init]; + if (self) { + _layoutConfig = layoutConfig; + } + return self; +} + +- (KBKey *)keyForItemId:(NSString *)itemId shiftOn:(BOOL)shiftOn { + if (itemId.length == 0) { return nil; } + KBKeyboardKeyDef *def = [self.layoutConfig keyDefForIdentifier:itemId]; + if (def) { + return [self keyFromDef:def identifier:itemId shiftOn:shiftOn]; + } + + NSRange range = [itemId rangeOfString:@":"]; + if (range.location != NSNotFound) { + NSString *prefix = [itemId substringToIndex:range.location]; + NSString *value = [itemId substringFromIndex:range.location + 1]; + if ([prefix isEqualToString:@"letter"]) { + if (value.length >= 1) { + return [self letterKeyWithChar:value shiftOn:shiftOn]; + } + return nil; + } + if ([prefix isEqualToString:@"digit"]) { + NSString *identifier = [NSString stringWithFormat:@"digit_%@", value]; + KBKey *k = [KBKey keyWithIdentifier:identifier title:value output:value type:KBKeyTypeCharacter]; + k.caseVariant = KBKeyCaseVariantNone; + return k; + } + if ([prefix isEqualToString:@"sym"]) { + NSString *identifier = [self kb_identifierForSymbol:value]; + KBKey *k = [KBKey keyWithIdentifier:identifier title:value output:value type:KBKeyTypeCharacter]; + k.caseVariant = KBKeyCaseVariantNone; + return k; + } + } + return nil; +} + +- (KBKey *)keyFromDef:(KBKeyboardKeyDef *)def + identifier:(NSString *)identifier + shiftOn:(BOOL)shiftOn { + KBKeyType type = [self kb_keyTypeForDef:def]; + NSString *title = def.title ?: @""; + if (type == KBKeyTypeShift && shiftOn && def.selectedTitle.length > 0) { + title = def.selectedTitle; + } + NSString *output = @""; + switch (type) { + case KBKeyTypeSpace: + output = @" "; + break; + case KBKeyTypeReturn: + output = @"\n"; + break; + default: + output = @""; + break; + } + + NSString *finalId = identifier; + if ([identifier isEqualToString:@"emoji"]) { + finalId = KBKeyIdentifierEmojiPanel; + } else if ([identifier isEqualToString:@"send"]) { + finalId = @"return"; + } + + KBKey *k = [KBKey keyWithIdentifier:finalId title:title output:output type:type]; + if (type == KBKeyTypeShift) { + k.caseVariant = shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower; + } else { + k.caseVariant = KBKeyCaseVariantNone; + } + return k; +} + +- (KBKey *)letterKeyWithChar:(NSString *)charString shiftOn:(BOOL)shiftOn { + if (charString.length == 0) { return nil; } + NSString *lower = charString.lowercaseString; + NSString *upper = charString.uppercaseString; + + NSString *shown = shiftOn ? upper : lower; + NSString *identifier = [NSString stringWithFormat:@"letter_%@", lower]; + + KBKey *k = [KBKey keyWithIdentifier:identifier + title:shown + output:shown + type:KBKeyTypeCharacter]; + k.caseVariant = shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower; + return k; +} + +#pragma mark - Private + +- (KBKeyType)kb_keyTypeForDef:(KBKeyboardKeyDef *)def { + NSString *type = def.type.lowercaseString; + if ([type isEqualToString:@"shift"]) return KBKeyTypeShift; + if ([type isEqualToString:@"backspace"]) return KBKeyTypeBackspace; + if ([type isEqualToString:@"mode"]) return KBKeyTypeModeChange; + if ([type isEqualToString:@"symbolstoggle"]) return KBKeyTypeSymbolsToggle; + if ([type isEqualToString:@"space"]) return KBKeyTypeSpace; + if ([type isEqualToString:@"return"]) return KBKeyTypeReturn; + if ([type isEqualToString:@"globe"]) return KBKeyTypeGlobe; + return KBKeyTypeCustom; +} + +- (NSString *)kb_identifierForSymbol:(NSString *)symbol { + if (symbol.length == 0) { return nil; } + static NSDictionary *map = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + map = @{ + @"-": @"sym_minus", + @"/": @"sym_slash", + @":": @"sym_colon", + @";": @"sym_semicolon", + @"(": @"sym_paren_l", + @")": @"sym_paren_r", + @"¥": @"sym_money", + @"¥": @"sym_money", + @"&": @"sym_amp", + @"@": @"sym_at", + @"\"": @"sym_quote_double", + @"“": @"sym_quote_double", + @"”": @"sym_quote_double", + @".": @"sym_dot", + @"。": @"sym_chinese_dot", + @",": @"sym_comma", + @"、": @"sym_dun", + @"?": @"sym_question", + @"!": @"sym_exclam", + @"'": @"sym_quote_single", + @"‘": @"sym_quote_single", + @"’": @"sym_quote_single", + @"[": @"sym_bracket_l", + @"]": @"sym_bracket_r", + @"{": @"sym_brace_l", + @"}": @"sym_brace_r", + @"「": @"sym_corner_l", + @"」": @"sym_corner_r", + @"#": @"sym_hash", + @"%": @"sym_percent", + @"^": @"sym_caret", + @"*": @"sym_asterisk", + @"+": @"sym_plus", + @"=": @"sym_equal", + @"_": @"sym_underscore", + @"\\": @"sym_backslash", + @"|": @"sym_pipe", + @"~": @"sym_tilde", + @"<": @"sym_lt", + @">": @"sym_gt", + @"€": @"sym_euro", + @"$": @"sym_dollar", + @"£": @"sym_pound", + @"·": @"sym_bullet", + @"^_^": @"sym_face", + @"—": @"sym_emdash", + @"«": @"sym_guillemet_l", + @"»": @"sym_guillemet_r", + @"《": @"sym_book_title_l", + @"》": @"sym_book_title_r", + @"...": @"sym_ellipsis" + }; + }); + return map[symbol]; +} + +@end diff --git a/CustomKeyboard/View/KBKeyboardView/KBKeyboardLayoutEngine.h b/CustomKeyboard/View/KBKeyboardView/KBKeyboardLayoutEngine.h new file mode 100644 index 0000000..c9659ae --- /dev/null +++ b/CustomKeyboard/View/KBKeyboardView/KBKeyboardLayoutEngine.h @@ -0,0 +1,42 @@ +// +// KBKeyboardLayoutEngine.h +// CustomKeyboard +// +// Layout metrics calculation helper. +// + +#import + +@class KBKeyboardLayoutConfig; +@class KBKeyboardLayout; +@class KBKeyboardRowConfig; +@class KBKeyboardRowItem; +@class KBKeyboardKeyFactory; +@class KBKey; + +NS_ASSUME_NONNULL_BEGIN + +@interface KBKeyboardLayoutEngine : NSObject + +- (instancetype)initWithLayoutConfig:(KBKeyboardLayoutConfig *)layoutConfig; + +- (CGFloat)rowSpacingForLayout:(KBKeyboardLayout *)layout; +- (CGFloat)topInsetForLayout:(KBKeyboardLayout *)layout; +- (CGFloat)bottomInsetForLayout:(KBKeyboardLayout *)layout; + +- (CGFloat)rowHeightForRow:(KBKeyboardRowConfig *)row; +- (CGFloat)gapForRow:(KBKeyboardRowConfig *)row; +- (CGFloat)insetLeftForRow:(KBKeyboardRowConfig *)row; +- (CGFloat)insetRightForRow:(KBKeyboardRowConfig *)row; + +- (CGFloat)widthForItem:(KBKeyboardRowItem *)item key:(KBKey *)key; +- (CGFloat)fontSizeForItem:(KBKeyboardRowItem *)item key:(KBKey *)key; +- (CGFloat)fontSizeForFontKey:(NSString *)fontKey; + +- (CGFloat)calculateUniformCharKeyWidthForRows:(NSArray *)rows + keyFactory:(KBKeyboardKeyFactory *)keyFactory + shiftOn:(BOOL)shiftOn; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CustomKeyboard/View/KBKeyboardView/KBKeyboardLayoutEngine.m b/CustomKeyboard/View/KBKeyboardView/KBKeyboardLayoutEngine.m new file mode 100644 index 0000000..3c4ce6e --- /dev/null +++ b/CustomKeyboard/View/KBKeyboardView/KBKeyboardLayoutEngine.m @@ -0,0 +1,245 @@ +// +// KBKeyboardLayoutEngine.m +// CustomKeyboard +// + +#import "KBKeyboardLayoutEngine.h" +#import "KBKeyboardLayoutConfig.h" +#import "KBKeyboardKeyFactory.h" +#import "KBKey.h" +#import "KBConfig.h" + +@interface KBKeyboardLayoutEngine () +@property (nonatomic, strong) KBKeyboardLayoutConfig *layoutConfig; +@end + +@implementation KBKeyboardLayoutEngine + +- (instancetype)initWithLayoutConfig:(KBKeyboardLayoutConfig *)layoutConfig { + self = [super init]; + if (self) { + _layoutConfig = layoutConfig; + } + return self; +} + +- (CGFloat)rowSpacingForLayout:(KBKeyboardLayout *)layout { + KBKeyboardLayoutConfig *config = self.layoutConfig; + NSNumber *layoutSpacing = layout.rowSpacing; + return [self kb_metricValue:layoutSpacing fallback:config.metrics.rowSpacing defaultValue:8.0]; +} + +- (CGFloat)topInsetForLayout:(KBKeyboardLayout *)layout { + KBKeyboardLayoutConfig *config = self.layoutConfig; + NSNumber *layoutInset = layout.topInset; + return [self kb_metricValue:layoutInset fallback:config.metrics.topInset defaultValue:8.0]; +} + +- (CGFloat)bottomInsetForLayout:(KBKeyboardLayout *)layout { + KBKeyboardLayoutConfig *config = self.layoutConfig; + NSNumber *layoutInset = layout.bottomInset; + return [self kb_metricValue:layoutInset fallback:config.metrics.bottomInset defaultValue:6.0]; +} + +- (CGFloat)rowHeightForRow:(KBKeyboardRowConfig *)row { + KBKeyboardLayoutConfig *config = self.layoutConfig; + NSNumber *height = row.height ?: config.metrics.keyHeight; + CGFloat value = [self kb_numberValue:height defaultValue:40.0]; + return [self kb_scaledValue:value]; +} + +- (CGFloat)gapForRow:(KBKeyboardRowConfig *)row { + KBKeyboardLayoutConfig *config = self.layoutConfig; + return [self kb_metricValue:row.gap fallback:config.metrics.gap defaultValue:5.0]; +} + +- (CGFloat)insetLeftForRow:(KBKeyboardRowConfig *)row { + KBKeyboardLayoutConfig *config = self.layoutConfig; + return [self kb_metricValue:row.insetLeft fallback:config.metrics.edgeInset defaultValue:0.0]; +} + +- (CGFloat)insetRightForRow:(KBKeyboardRowConfig *)row { + KBKeyboardLayoutConfig *config = self.layoutConfig; + return [self kb_metricValue:row.insetRight fallback:config.metrics.edgeInset defaultValue:0.0]; +} + +- (CGFloat)widthForItem:(KBKeyboardRowItem *)item key:(KBKey *)key { + CGFloat width = 0.0; + if (item.widthValue.doubleValue > 0.0) { + width = item.widthValue.doubleValue; + } else if (item.width.length > 0) { + if ([item.width.lowercaseString isEqualToString:@"flex"]) { + return 0.0; + } + width = [self kb_metricWidthForKey:item.width]; + if (width <= 0.0) { + width = item.width.doubleValue; + } + } + + if (width <= 0.0) { + KBKeyboardLayoutMetrics *m = self.layoutConfig.metrics; + if ([item.itemId hasPrefix:@"letter:"] || + [item.itemId hasPrefix:@"digit:"] || + [item.itemId hasPrefix:@"sym:"]) { + width = m.letterWidth.doubleValue; + } else if (key.type == KBKeyTypeReturn) { + width = m.sendWidth.doubleValue; + } else if (key.type == KBKeyTypeSpace) { + return 0.0; + } else { + width = m.controlWidth.doubleValue; + } + } + + if (width <= 0.0) { + if ([item.itemId hasPrefix:@"letter:"] || + [item.itemId hasPrefix:@"digit:"] || + [item.itemId hasPrefix:@"sym:"]) { + width = 32.0; + } else if (key.type == KBKeyTypeReturn) { + width = 88.0; + } else if (key.type == KBKeyTypeSpace) { + return 0.0; + } else { + width = 41.0; + } + } + + return width > 0.0 ? [self kb_scaledValue:width] : 0.0; +} + +- (CGFloat)fontSizeForItem:(KBKeyboardRowItem *)item key:(KBKey *)key { + NSString *fontKey = nil; + if ([item.itemId hasPrefix:@"letter:"]) { + fontKey = @"letter"; + } else if ([item.itemId hasPrefix:@"digit:"]) { + fontKey = @"digit"; + } else if ([item.itemId hasPrefix:@"sym:"]) { + fontKey = @"symbol"; + } else { + KBKeyboardKeyDef *def = [self.layoutConfig keyDefForIdentifier:item.itemId]; + fontKey = def.font; + } + + if (fontKey.length == 0) { + switch (key.type) { + case KBKeyTypeModeChange: + case KBKeyTypeSymbolsToggle: + fontKey = @"mode"; + break; + case KBKeyTypeSpace: + fontKey = @"space"; + break; + case KBKeyTypeReturn: + fontKey = @"send"; + break; + default: + fontKey = @"symbol"; + break; + } + } + + return [self fontSizeForFontKey:fontKey]; +} + +- (CGFloat)fontSizeForFontKey:(NSString *)fontKey { + KBKeyboardLayoutFonts *fonts = self.layoutConfig.fonts; + CGFloat size = 0.0; + if ([fontKey isEqualToString:@"letter"]) { size = fonts.letter.doubleValue; } + else if ([fontKey isEqualToString:@"digit"]) { size = fonts.digit.doubleValue; } + else if ([fontKey isEqualToString:@"symbol"]) { size = fonts.symbol.doubleValue; } + else if ([fontKey isEqualToString:@"mode"]) { size = fonts.mode.doubleValue; } + else if ([fontKey isEqualToString:@"space"]) { size = fonts.space.doubleValue; } + else if ([fontKey isEqualToString:@"send"]) { size = fonts.send.doubleValue; } + if (size <= 0.0) { size = 18.0; } + return [self kb_scaledValue:size]; +} + +/// 计算跨行统一字符键宽度:遍历每行各自的 insets/gap/非字符键宽度, +/// 取各行可用字符键宽度的最小值,确保所有行都能容纳。 +/// 当各行有效宽度相同时返回 0(无需统一)。 +- (CGFloat)calculateUniformCharKeyWidthForRows:(NSArray *)rows + keyFactory:(KBKeyboardKeyFactory *)keyFactory + shiftOn:(BOOL)shiftOn { + CGFloat minWidth = CGFLOAT_MAX; + CGFloat maxWidth = 0.0; + BOOL hasCharRow = NO; + CGFloat containerWidth = KBScreenWidth(); + + for (KBKeyboardRowConfig *row in rows) { + if (row.segments) { continue; } // 跳过分段行 + NSArray *items = [row resolvedItems]; + NSUInteger charCount = 0; + CGFloat nonCharWidth = 0.0; + for (KBKeyboardRowItem *item in items) { + BOOL isChar = [item.itemId hasPrefix:@"letter:"] || + [item.itemId hasPrefix:@"digit:"] || + [item.itemId hasPrefix:@"sym:"]; + if (isChar) { + charCount++; + } else { + KBKey *key = [keyFactory keyForItemId:item.itemId shiftOn:shiftOn]; + CGFloat w = [self widthForItem:item key:key]; + nonCharWidth += w; + } + } + if (charCount == 0) { continue; } // 跳过无字符键的行(如底部控制行) + hasCharRow = YES; + + // 使用每行各自配置的 insets 和 gap + CGFloat gap = [self gapForRow:row]; + CGFloat insetLeft = [self insetLeftForRow:row]; + CGFloat insetRight = [self insetRightForRow:row]; + CGFloat totalGaps = (items.count > 1) ? (items.count - 1) * gap : 0.0; + CGFloat available = containerWidth - insetLeft - insetRight - totalGaps - nonCharWidth; + CGFloat width = available / charCount; + + if (width < minWidth) { minWidth = width; } + if (width > maxWidth) { maxWidth = width; } + } + + if (!hasCharRow || minWidth <= 0.0 || minWidth >= CGFLOAT_MAX) { return 0.0; } + // 各行有效宽度相同时无需统一 + if (fabs(maxWidth - minWidth) < 0.5) { return 0.0; } + return minWidth; +} + +#pragma mark - Helpers + +- (CGFloat)kb_scaledValue:(CGFloat)designValue { + if (self.layoutConfig) { + return [self.layoutConfig scaledValue:designValue]; + } + return KBFit(designValue); +} + +- (CGFloat)kb_numberValue:(NSNumber *)value defaultValue:(CGFloat)defaultValue { + if ([value isKindOfClass:[NSNumber class]]) { + return value.doubleValue; + } + return defaultValue; +} + +- (CGFloat)kb_metricValue:(NSNumber *)value fallback:(NSNumber *)fallback defaultValue:(CGFloat)defaultValue { + CGFloat v = [self kb_numberValue:value defaultValue:-1.0]; + if (v < 0.0) { + v = [self kb_numberValue:fallback defaultValue:defaultValue]; + } + if (v < 0.0) { + v = defaultValue; + } + return [self kb_scaledValue:v]; +} + +- (CGFloat)kb_metricWidthForKey:(NSString *)key { + KBKeyboardLayoutMetrics *m = self.layoutConfig.metrics; + if ([key isEqualToString:@"letterWidth"]) { return m.letterWidth.doubleValue; } + if ([key isEqualToString:@"controlWidth"]) { return m.controlWidth.doubleValue; } + if ([key isEqualToString:@"sendWidth"]) { return m.sendWidth.doubleValue; } + if ([key isEqualToString:@"symbolsWideWidth"]) { return m.symbolsWideWidth.doubleValue; } + if ([key isEqualToString:@"symbolsSideWidth"]) { return m.symbolsSideWidth.doubleValue; } + return 0.0; +} + +@end diff --git a/CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyBuilder.h b/CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyBuilder.h new file mode 100644 index 0000000..45e6f34 --- /dev/null +++ b/CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyBuilder.h @@ -0,0 +1,28 @@ +// +// KBKeyboardLegacyBuilder.h +// CustomKeyboard +// +// Legacy layout builder (non-config layout). +// + +#import + +@class KBKey; +@class KBBackspaceLongPressHandler; +@class UIView; + +NS_ASSUME_NONNULL_BEGIN + +@interface KBKeyboardLegacyBuilder : NSObject + +- (void)buildRow:(UIView *)row + withKeys:(NSArray *)keys +edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier + shiftOn:(BOOL)shiftOn + backspaceHandler:(KBBackspaceLongPressHandler *)backspaceHandler + target:(id)target + action:(SEL)action; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyBuilder.m b/CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyBuilder.m new file mode 100644 index 0000000..e18b150 --- /dev/null +++ b/CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyBuilder.m @@ -0,0 +1,291 @@ +// +// KBKeyboardLegacyBuilder.m +// CustomKeyboard +// + +#import "KBKeyboardLegacyBuilder.h" +#import "KBKey.h" +#import "KBKeyButton.h" +#import "KBBackspaceLongPressHandler.h" +#import "KBConfig.h" +#import + +static const CGFloat kKBSpecialKeySquareMultiplier = 1.2; +static const CGFloat kKBReturnWidthMultiplier = 2.4; +static const CGFloat kKBSpaceWidthMultiplier = 3.0; +static inline CGFloat KBLegacyRowHorizontalInset(void) { + return KBFit(6.0f); +} + +@implementation KBKeyboardLegacyBuilder + +- (void)buildRow:(UIView *)row + withKeys:(NSArray *)keys +edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier + shiftOn:(BOOL)shiftOn + backspaceHandler:(KBBackspaceLongPressHandler *)backspaceHandler + target:(id)target + action:(SEL)action { + if (!row || keys.count == 0) { return; } + + // 第 4 行(底部控制行)使用单独的布局规则: + // 123/ABC、Emoji、Send 给定尺寸,Space 自动吃掉剩余宽度。 + BOOL isBottomControlRow = [self kb_isBottomControlRowWithKeys:keys]; + + CGFloat spacing = 0; // 键与键之间的间距 + UIView *previous = nil; + UIView *leftSpacer = nil; + UIView *rightSpacer = nil; + + if (edgeSpacerMultiplier > 0.0) { + leftSpacer = [UIView new]; + rightSpacer = [UIView new]; + leftSpacer.backgroundColor = [UIColor clearColor]; + rightSpacer.backgroundColor = [UIColor clearColor]; + [row addSubview:leftSpacer]; + [row addSubview:rightSpacer]; + [leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(row.mas_left).offset(KBLegacyRowHorizontalInset()); + make.centerY.equalTo(row); + make.height.mas_equalTo(1); + }]; + [rightSpacer mas_makeConstraints:^(MASConstraintMaker *make) { + make.right.equalTo(row.mas_right).offset(-KBLegacyRowHorizontalInset()); + make.centerY.equalTo(row); + make.height.mas_equalTo(1); + }]; + } + + for (NSInteger i = 0; i < keys.count; i++) { + KBKey *key = keys[i]; + KBKeyButton *btn = [[KBKeyButton alloc] init]; + btn.key = key; + [btn setTitle:key.title forState:UIControlStateNormal]; + // 在设置完标题后,按当前皮肤应用图标与文字显隐 + [btn applyThemeForCurrentKey]; + if (target && action) { + [btn addTarget:target action:action forControlEvents:UIControlEventTouchDown]; + } + [row addSubview:btn]; + + if (key.type == KBKeyTypeBackspace) { + [backspaceHandler bindDeleteButton:btn showClearLabel:YES]; + } + + // Shift 按钮选中态随大小写状态变化 + if (key.type == KBKeyTypeShift) { + btn.selected = shiftOn; + } + + [btn mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.bottom.equalTo(row); + if (previous) { + make.left.equalTo(previous.mas_right).offset(spacing); + } else { + if (leftSpacer) { + make.left.equalTo(leftSpacer.mas_right).offset(spacing); + } else { + make.left.equalTo(row.mas_left).offset(KBLegacyRowHorizontalInset()); + } + } + }]; + + // 字符键:等宽 + if (key.type == KBKeyTypeCharacter) { + if (previous && [previous isKindOfClass:[KBKeyButton class]]) { + KBKeyButton *prevBtn = (KBKeyButton *)previous; + if (prevBtn.key.type == KBKeyTypeCharacter) { + [btn mas_makeConstraints:^(MASConstraintMaker *make) { + make.width.equalTo(previous); + }]; + } + } + } else { + // special keys: 宽度在第二遍统一设置 + } + + previous = btn; + } + + // 右侧使用内边距或右占位 + if (previous) { + [previous mas_makeConstraints:^(MASConstraintMaker *make) { + if (rightSpacer) { + make.right.equalTo(rightSpacer.mas_left).offset(-spacing); + } else { + make.right.equalTo(row.mas_right).offset(-KBLegacyRowHorizontalInset()); + } + }]; + } + + // 底部控制行:在第一轮已完成左右约束的前提下,仅给 123/ABC、Emoji、Send 指定宽度, + // Space 不加宽度约束,让其自动填充剩余空间。 + if (isBottomControlRow) { + [self kb_applyBottomControlRowWidthInRow:row]; + return; + } + + // 第二遍:以首个字符键为基准,统一设置特殊键宽度倍数 + KBKeyButton *firstChar = nil; + BOOL hasCharacterInRow = NO; + for (UIView *v in row.subviews) { + if (![v isKindOfClass:[KBKeyButton class]]) continue; + KBKeyButton *b = (KBKeyButton *)v; + if (b.key.type == KBKeyTypeCharacter) { + firstChar = b; + hasCharacterInRow = YES; + break; + } + } + // 若该行没有字符键(例如底部控制行之外的特殊行),则使用行内第一个按钮作为基准宽度 + if (!firstChar) { + for (UIView *v in row.subviews) { + if ([v isKindOfClass:[KBKeyButton class]]) { + firstChar = (KBKeyButton *)v; + break; + } + } + } + if (firstChar) { + // 如果该行本身没有字符键(如底部控制行),且基准按钮是 123/ABC/#+= 等, + // 也将其约束为 1:1,避免 123/ABC 不是正方形。 + if (!hasCharacterInRow && + (firstChar.key.type == KBKeyTypeModeChange || + firstChar.key.type == KBKeyTypeSymbolsToggle || + firstChar.key.type == KBKeyTypeCustom)) { + [firstChar mas_makeConstraints:^(MASConstraintMaker *make) { + make.width.equalTo(firstChar.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier); + }]; + } + + for (UIView *v in row.subviews) { + if (![v isKindOfClass:[KBKeyButton class]]) continue; + KBKeyButton *b = (KBKeyButton *)v; + + // 避免对基准按钮自身添加 self == self * k 的无效约束 + if (b == firstChar) continue; + if (b.key.type == KBKeyTypeCharacter) continue; + + BOOL isBottomModeKey = (b.key.type == KBKeyTypeModeChange) || + (b.key.type == KBKeyTypeSymbolsToggle) || + (b.key.type == KBKeyTypeCustom); + // 一类键强制近似正方形(宽 ~ 高) + if (b.key.type == KBKeyTypeShift || + b.key.type == KBKeyTypeBackspace || + isBottomModeKey) { + [b mas_makeConstraints:^(MASConstraintMaker *make) { + make.width.equalTo(b.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier); + }]; + continue; + } + + CGFloat multiplier = 1.5; + + // Space:宽度更大 + if (b.key.type == KBKeyTypeSpace) { + multiplier = kKBSpaceWidthMultiplier; + } + // Send 按钮:宽度为基准键的 2.4 倍 + else if (b.key.type == KBKeyTypeReturn) { + multiplier = kKBReturnWidthMultiplier; + } + // 其它特殊键(如 Globe)保持适度放大 + else if (b.key.type == KBKeyTypeGlobe) { + multiplier = 1.5; + } + + [b mas_makeConstraints:^(MASConstraintMaker *make) { + make.width.equalTo(firstChar).multipliedBy(multiplier); + }]; + } + + // 如果有左右占位,则把占位宽度设置为字符键宽度的一定倍数,以实现整体居中; + // 同时强约束左右占位宽度相等,避免在某些系统上由于布局冲突导致只压缩一侧, + // 出现“左侧有空隙,右侧无空隙”的情况。 + if (leftSpacer && rightSpacer) { + // 1) 左右占位宽度必须相等 + [leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) { + make.width.equalTo(rightSpacer); + }]; + // 2) 同时都接近字符键宽度的 edgeSpacerMultiplier 倍数 + [rightSpacer mas_makeConstraints:^(MASConstraintMaker *make) { + make.width.equalTo(firstChar).multipliedBy(edgeSpacerMultiplier); + }]; + } + } +} + +#pragma mark - Row Helpers (Bottom Control Row) + +// 判断是否为底部控制行:包含 Space + Return,且有 ModeChange/SymbolsToggle, +// 并且不再含字符键。 +- (BOOL)kb_isBottomControlRowWithKeys:(NSArray *)keys { + BOOL hasSpace = NO; + BOOL hasReturn = NO; + BOOL hasModeOrSymbols = NO; + BOOL hasCharacters = NO; + + for (KBKey *k in keys) { + if (k.type == KBKeyTypeSpace) { + hasSpace = YES; + } else if (k.type == KBKeyTypeReturn) { + hasReturn = YES; + } else if (k.type == KBKeyTypeModeChange || k.type == KBKeyTypeSymbolsToggle) { + hasModeOrSymbols = YES; + } else if (k.type == KBKeyTypeCharacter) { + hasCharacters = YES; + } + } + return (hasSpace && hasReturn && hasModeOrSymbols && !hasCharacters); +} + +- (void)kb_applyBottomControlRowWidthInRow:(UIView *)row { + if (!row) { return; } + + KBKeyButton *modeBtn = nil; + KBKeyButton *spaceBtn = nil; + KBKeyButton *retBtn = nil; + NSMutableArray *customButtons = [NSMutableArray array]; + + for (UIView *v in row.subviews) { + if (![v isKindOfClass:[KBKeyButton class]]) continue; + KBKeyButton *b = (KBKeyButton *)v; + switch (b.key.type) { + case KBKeyTypeModeChange: + case KBKeyTypeSymbolsToggle: + modeBtn = b; + break; + case KBKeyTypeCustom: + [customButtons addObject:b]; + break; + case KBKeyTypeSpace: + spaceBtn = b; + break; + case KBKeyTypeReturn: + retBtn = b; + break; + default: + break; + } + } + + if (!modeBtn || customButtons.count == 0 || !spaceBtn || !retBtn) { + return; + } + + [modeBtn mas_makeConstraints:^(MASConstraintMaker *make) { + make.width.equalTo(row.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier); + }]; + for (KBKeyButton *custom in customButtons) { + [custom mas_makeConstraints:^(MASConstraintMaker *make) { + make.width.equalTo(row.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier); + }]; + } + [retBtn mas_makeConstraints:^(MASConstraintMaker *make) { + make.width.equalTo(modeBtn.mas_width).multipliedBy(2.0); + }]; + // Space 不设置宽度;通过此前已建立的左右约束自动占满剩余宽度。 + (void)spaceBtn; +} + +@end diff --git a/CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyLayoutProvider.h b/CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyLayoutProvider.h new file mode 100644 index 0000000..977a881 --- /dev/null +++ b/CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyLayoutProvider.h @@ -0,0 +1,22 @@ +// +// KBKeyboardLegacyLayoutProvider.h +// CustomKeyboard +// + +#import + +@class KBKey; +@class KBKeyboardKeyFactory; + +NS_ASSUME_NONNULL_BEGIN + +@interface KBKeyboardLegacyLayoutProvider : NSObject + +- (NSArray *> *)keysForLayoutStyleIsNumbers:(BOOL)isNumbersLayout + shiftOn:(BOOL)shiftOn + symbolsMoreOn:(BOOL)symbolsMoreOn + keyFactory:(KBKeyboardKeyFactory *)keyFactory; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyLayoutProvider.m b/CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyLayoutProvider.m new file mode 100644 index 0000000..6137dda --- /dev/null +++ b/CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyLayoutProvider.m @@ -0,0 +1,193 @@ +// +// KBKeyboardLegacyLayoutProvider.m +// CustomKeyboard +// + +#import "KBKeyboardLegacyLayoutProvider.h" +#import "KBKeyboardKeyFactory.h" +#import "KBKey.h" + +@implementation KBKeyboardLegacyLayoutProvider + +- (NSArray *> *)keysForLayoutStyleIsNumbers:(BOOL)isNumbersLayout + shiftOn:(BOOL)shiftOn + symbolsMoreOn:(BOOL)symbolsMoreOn + keyFactory:(KBKeyboardKeyFactory *)keyFactory { + if (isNumbersLayout) { + return [self buildKeysForNumbersLayoutWithSymbolsMoreOn:symbolsMoreOn]; + } + return [self buildKeysForLettersLayoutWithShiftOn:shiftOn keyFactory:keyFactory]; +} + +#pragma mark - Letters Layout + +- (NSArray *> *)buildKeysForLettersLayoutWithShiftOn:(BOOL)shiftOn + keyFactory:(KBKeyboardKeyFactory *)keyFactory { + // 字母布局(QWERTY) + NSArray *r1Letters = @[ @"q", @"w", @"e", @"r", @"t", @"y", @"u", @"i", @"o", @"p" ]; + NSArray *r2Letters = @[ @"a", @"s", @"d", @"f", @"g", @"h", @"j", @"k", @"l" ]; + NSArray *r3Letters = @[ @"z", @"x", @"c", @"v", @"b", @"n", @"m" ]; + + NSMutableArray *row1 = [NSMutableArray arrayWithCapacity:r1Letters.count]; + for (NSString *s in r1Letters) { + KBKey *key = [keyFactory letterKeyWithChar:s shiftOn:shiftOn]; + if (key) { [row1 addObject:key]; } + } + + NSMutableArray *row2 = [NSMutableArray arrayWithCapacity:r2Letters.count]; + for (NSString *s in r2Letters) { + KBKey *key = [keyFactory letterKeyWithChar:s shiftOn:shiftOn]; + if (key) { [row2 addObject:key]; } + } + + // 第三行:Shift + Z...M + Backspace + NSMutableArray *row3 = [NSMutableArray array]; + KBKey *shift = [KBKey keyWithIdentifier:@"shift" + title:@"⇧" + output:@"" + type:KBKeyTypeShift]; + // Shift 键也支持大小写两套皮肤图 + shift.caseVariant = shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower; + [row3 addObject:shift]; + + for (NSString *s in r3Letters) { + KBKey *key = [keyFactory letterKeyWithChar:s shiftOn:shiftOn]; + if (key) { [row3 addObject:key]; } + } + + KBKey *backspace = [KBKey keyWithIdentifier:@"backspace" + title:@"⌫" + output:@"" + type:KBKeyTypeBackspace]; + [row3 addObject:backspace]; + + NSArray *row4 = [self bottomControlRowKeysForLettersLayout]; + + return @[row1.copy, row2.copy, row3.copy, row4]; +} + +#pragma mark - Numbers / Symbols Layout + +- (NSArray *> *)buildKeysForNumbersLayoutWithSymbolsMoreOn:(BOOL)symbolsMoreOn { + // 数字/符号布局:3 行主键 + 底部控制行 + NSArray *r1 = nil; + NSArray *r2 = nil; + NSArray *r3 = nil; + + if (!symbolsMoreOn) { + // 数字第一页(123) + r1 = @[ [KBKey keyWithIdentifier:@"digit_1" title:@"1" output:@"1" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"digit_2" title:@"2" output:@"2" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"digit_3" title:@"3" output:@"3" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"digit_4" title:@"4" output:@"4" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"digit_5" title:@"5" output:@"5" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"digit_6" title:@"6" output:@"6" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"digit_7" title:@"7" output:@"7" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"digit_8" title:@"8" output:@"8" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"digit_9" title:@"9" output:@"9" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"digit_0" title:@"0" output:@"0" type:KBKeyTypeCharacter] ]; + r2 = @[ [KBKey keyWithIdentifier:@"sym_minus" title:@"-" output:@"-" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_slash" title:@"/" output:@"/" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_colon" title:@":" output:@":" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_semicolon" title:@";" output:@";" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_paren_l" title:@"(" output:@"(" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_paren_r" title:@")" output:@")" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_money" title:@"¥" output:@"¥" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_amp" title:@"&" output:@"&" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_at" title:@"@" output:@"@" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_quote_double" title:@"\"" output:@"\"" type:KBKeyTypeCharacter] ]; + + r3 = [self symbolsCommonThirdRowWithToggleIsMore:NO]; + } else { + // 数字第二页(#+=):前两行替换为更多符号,左下角按钮文案改为“123” + r1 = @[ [KBKey keyWithIdentifier:@"sym_bracket_l" title:@"[" output:@"[" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_bracket_r" title:@"]" output:@"]" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_brace_l" title:@"{" output:@"{" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_brace_r" title:@"}" output:@"}" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_hash" title:@"#" output:@"#" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_percent" title:@"%" output:@"%" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_caret" title:@"^" output:@"^" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_asterisk" title:@"*" output:@"*" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_plus" title:@"+" output:@"+" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_equal" title:@"=" output:@"=" type:KBKeyTypeCharacter] ]; + r2 = @[ [KBKey keyWithIdentifier:@"sym_underscore" title:@"_" output:@"_" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_backslash" title:@"\\" output:@"\\" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_pipe" title:@"|" output:@"|" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_tilde" title:@"~" output:@"~" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_lt" title:@"<" output:@"<" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_gt" title:@">" output:@">" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_dollar" title:@"$" output:@"$" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_euro" title:@"€" output:@"€" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_pound" title:@"£" output:@"£" type:KBKeyTypeCharacter], + [KBKey keyWithIdentifier:@"sym_bullet" title:@"•" output:@"•" type:KBKeyTypeCharacter] ]; + + r3 = [self symbolsCommonThirdRowWithToggleIsMore:YES]; + } + + NSArray *r4 = [self bottomControlRowKeysForNumbersLayout]; + return @[r1, r2, r3, r4]; +} + +#pragma mark - Key Factories + +- (NSArray *)symbolsCommonThirdRowWithToggleIsMore:(BOOL)isMorePage { + NSString *identifier = isMorePage ? @"symbols_toggle_123" : @"symbols_toggle_more"; + NSString *title = isMorePage ? @"123" : @"#+="; + + KBKey *toggle = [KBKey keyWithIdentifier:identifier + title:title + output:@"" + type:KBKeyTypeSymbolsToggle]; + KBKey *comma = [KBKey keyWithIdentifier:@"sym_comma" title:@"," output:@"," type:KBKeyTypeCharacter]; + KBKey *dot = [KBKey keyWithIdentifier:@"sym_dot" title:@"." output:@"." type:KBKeyTypeCharacter]; + KBKey *q = [KBKey keyWithIdentifier:@"sym_question" title:@"?" output:@"?" type:KBKeyTypeCharacter]; + KBKey *ex = [KBKey keyWithIdentifier:@"sym_exclam" title:@"!" output:@"!" type:KBKeyTypeCharacter]; + KBKey *quote = [KBKey keyWithIdentifier:@"sym_quote_single" title:@"'" output:@"'" type:KBKeyTypeCharacter]; + KBKey *back = [KBKey keyWithIdentifier:@"backspace" + title:@"⌫" + output:@"" + type:KBKeyTypeBackspace]; + return @[ toggle, comma, dot, q, ex, quote, back ]; +} + +- (NSArray *)bottomControlRowKeysForLettersLayout { + KBKey *mode123 = [KBKey keyWithIdentifier:@"mode_123" + title:@"123" + output:@"" + type:KBKeyTypeModeChange]; + KBKey *emoji = [KBKey keyWithIdentifier:KBKeyIdentifierEmojiPanel + title:@"😊" + output:@"" + type:KBKeyTypeCustom]; + KBKey *space = [KBKey keyWithIdentifier:@"space" + title:@"space" + output:@" " + type:KBKeyTypeSpace]; + KBKey *ret = [KBKey keyWithIdentifier:@"return" + title:KBLocalized(@"Send") + output:@"\n" + type:KBKeyTypeReturn]; + return @[ mode123, emoji, space, ret ]; +} + +- (NSArray *)bottomControlRowKeysForNumbersLayout { + KBKey *modeABC = [KBKey keyWithIdentifier:@"mode_abc" + title:@"abc" + output:@"" + type:KBKeyTypeModeChange]; + KBKey *emoji = [KBKey keyWithIdentifier:KBKeyIdentifierEmojiPanel + title:@"😊" + output:@"" + type:KBKeyTypeCustom]; + KBKey *space = [KBKey keyWithIdentifier:@"space" + title:@"space" + output:@" " + type:KBKeyTypeSpace]; + KBKey *ret = [KBKey keyWithIdentifier:@"return" + title:KBLocalized(@"Send") + output:@"\n" + type:KBKeyTypeReturn]; + return @[ modeABC, emoji, space, ret ]; +} + +@end diff --git a/CustomKeyboard/View/KBKeyboardView/KBKeyboardRowBuilder.h b/CustomKeyboard/View/KBKeyboardView/KBKeyboardRowBuilder.h new file mode 100644 index 0000000..6c481f0 --- /dev/null +++ b/CustomKeyboard/View/KBKeyboardView/KBKeyboardRowBuilder.h @@ -0,0 +1,33 @@ +// +// KBKeyboardRowBuilder.h +// CustomKeyboard +// + +#import + +@class KBKeyboardLayoutConfig; +@class KBKeyboardLayoutEngine; +@class KBKeyboardKeyFactory; +@class KBKeyboardRowConfig; +@class KBBackspaceLongPressHandler; +@class UIView; + +NS_ASSUME_NONNULL_BEGIN + +@interface KBKeyboardRowBuilder : NSObject + +- (instancetype)initWithLayoutConfig:(KBKeyboardLayoutConfig *)layoutConfig + layoutEngine:(KBKeyboardLayoutEngine *)layoutEngine + keyFactory:(KBKeyboardKeyFactory *)keyFactory; + +- (void)buildRow:(UIView *)row + withRowConfig:(KBKeyboardRowConfig *)rowConfig +uniformCharWidth:(CGFloat)uniformCharWidth + shiftOn:(BOOL)shiftOn +backspaceHandler:(KBBackspaceLongPressHandler *)backspaceHandler + target:(id)target + action:(SEL)action; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CustomKeyboard/View/KBKeyboardView/KBKeyboardRowBuilder.m b/CustomKeyboard/View/KBKeyboardView/KBKeyboardRowBuilder.m new file mode 100644 index 0000000..2ade425 --- /dev/null +++ b/CustomKeyboard/View/KBKeyboardView/KBKeyboardRowBuilder.m @@ -0,0 +1,330 @@ +// +// KBKeyboardRowBuilder.m +// CustomKeyboard +// + +#import "KBKeyboardRowBuilder.h" +#import "KBKeyboardLayoutConfig.h" +#import "KBKeyboardLayoutEngine.h" +#import "KBKeyboardKeyFactory.h" +#import "KBKeyButton.h" +#import "KBKey.h" +#import "KBBackspaceLongPressHandler.h" +#import "KBSkinManager.h" +#import + +@interface KBKeyboardRowBuilder () +@property (nonatomic, strong) KBKeyboardLayoutConfig *layoutConfig; +@property (nonatomic, strong) KBKeyboardLayoutEngine *layoutEngine; +@property (nonatomic, strong) KBKeyboardKeyFactory *keyFactory; +@end + +@implementation KBKeyboardRowBuilder + +- (instancetype)initWithLayoutConfig:(KBKeyboardLayoutConfig *)layoutConfig + layoutEngine:(KBKeyboardLayoutEngine *)layoutEngine + keyFactory:(KBKeyboardKeyFactory *)keyFactory { + self = [super init]; + if (self) { + _layoutConfig = layoutConfig; + _layoutEngine = layoutEngine; + _keyFactory = keyFactory; + } + return self; +} + +- (void)buildRow:(UIView *)row + withRowConfig:(KBKeyboardRowConfig *)rowConfig +uniformCharWidth:(CGFloat)uniformCharWidth + shiftOn:(BOOL)shiftOn +backspaceHandler:(KBBackspaceLongPressHandler *)backspaceHandler + target:(id)target + action:(SEL)action { + if (!row || !rowConfig) { return; } + CGFloat gap = [self.layoutEngine gapForRow:rowConfig]; + CGFloat insetLeft = [self.layoutEngine insetLeftForRow:rowConfig]; + CGFloat insetRight = [self.layoutEngine insetRightForRow:rowConfig]; + + if (rowConfig.segments) { + KBKeyboardRowSegments *segments = rowConfig.segments; + NSArray *leftItems = [segments leftItems]; + NSArray *centerItems = [segments centerItems]; + NSArray *rightItems = [segments rightItems]; + UIView *leftContainer = [UIView new]; + UIView *centerContainer = [UIView new]; + UIView *rightContainer = [UIView new]; + [row addSubview:leftContainer]; + [row addSubview:centerContainer]; + [row addSubview:rightContainer]; + + [leftContainer mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(row.mas_left).offset(insetLeft); + make.top.bottom.equalTo(row); + }]; + [rightContainer mas_makeConstraints:^(MASConstraintMaker *make) { + make.right.equalTo(row.mas_right).offset(-insetRight); + make.top.bottom.equalTo(row); + }]; + [centerContainer mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerX.equalTo(row); + make.top.bottom.equalTo(row); + make.left.greaterThanOrEqualTo(leftContainer.mas_right).offset(gap); + make.right.lessThanOrEqualTo(rightContainer.mas_left).offset(-gap); + }]; + + if (leftItems.count == 0) { + [leftContainer mas_makeConstraints:^(MASConstraintMaker *make) { + make.width.mas_equalTo(0); + }]; + } + if (centerItems.count == 0) { + [centerContainer mas_makeConstraints:^(MASConstraintMaker *make) { + make.width.mas_equalTo(0); + }]; + } + if (rightItems.count == 0) { + [rightContainer mas_makeConstraints:^(MASConstraintMaker *make) { + make.width.mas_equalTo(0); + }]; + } + + [self kb_buildButtonsInContainer:leftContainer + items:leftItems + gap:gap + insetLeft:0 + insetRight:0 + alignCenter:NO + isTopLevelRow:NO + uniformCharWidth:uniformCharWidth + shiftOn:shiftOn + backspaceHandler:backspaceHandler + target:target + action:action]; + [self kb_buildButtonsInContainer:centerContainer + items:centerItems + gap:gap + insetLeft:0 + insetRight:0 + alignCenter:NO + isTopLevelRow:NO + uniformCharWidth:uniformCharWidth + shiftOn:shiftOn + backspaceHandler:backspaceHandler + target:target + action:action]; + [self kb_buildButtonsInContainer:rightContainer + items:rightItems + gap:gap + insetLeft:0 + insetRight:0 + alignCenter:NO + isTopLevelRow:NO + uniformCharWidth:uniformCharWidth + shiftOn:shiftOn + backspaceHandler:backspaceHandler + target:target + action:action]; + return; + } + + BOOL alignCenter = [rowConfig.align.lowercaseString isEqualToString:@"center"]; + [self kb_buildButtonsInContainer:row + items:[rowConfig resolvedItems] + gap:gap + insetLeft:insetLeft + insetRight:insetRight + alignCenter:alignCenter + isTopLevelRow:YES + uniformCharWidth:uniformCharWidth + shiftOn:shiftOn + backspaceHandler:backspaceHandler + target:target + action:action]; +} + +#pragma mark - Private + +- (void)kb_buildButtonsInContainer:(UIView *)container + items:(NSArray *)items + gap:(CGFloat)gap + insetLeft:(CGFloat)insetLeft + insetRight:(CGFloat)insetRight + alignCenter:(BOOL)alignCenter + isTopLevelRow:(BOOL)isTopLevelRow + uniformCharWidth:(CGFloat)uniformCharWidth + shiftOn:(BOOL)shiftOn + backspaceHandler:(KBBackspaceLongPressHandler *)backspaceHandler + target:(id)target + action:(SEL)action { + if (items.count == 0) { return; } + + UIView *leftSpacer = nil; + UIView *rightSpacer = nil; + if (alignCenter) { + leftSpacer = [UIView new]; + rightSpacer = [UIView new]; + [container addSubview:leftSpacer]; + [container addSubview:rightSpacer]; + [leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(container.mas_left).offset(insetLeft); + make.top.bottom.equalTo(container); + }]; + [rightSpacer mas_makeConstraints:^(MASConstraintMaker *make) { + make.right.equalTo(container.mas_right).offset(-insetRight); + make.top.bottom.equalTo(container); + }]; + [leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) { + make.width.equalTo(rightSpacer); + }]; + } + + BOOL usingUniformWidth = (uniformCharWidth > 0.0); + BOOL allCharacterKeys = YES; // 跟踪该容器内是否全部为字符键 + + KBKeyButton *previous = nil; + KBKeyButton *firstCharBtn = nil; // 用于非统一模式下的行内等宽约束 + for (KBKeyboardRowItem *item in items) { + KBKeyButton *btn = [self kb_buttonForItem:item + shiftOn:shiftOn + backspaceHandler:backspaceHandler + target:target + action:action]; + if (!btn) { continue; } + [container addSubview:btn]; + + [btn mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.bottom.equalTo(container); + if (previous) { + make.left.equalTo(previous.mas_right).offset(gap); + } else { + if (leftSpacer) { + make.left.equalTo(leftSpacer.mas_right).offset(gap); + } else { + make.left.equalTo(container.mas_left).offset(insetLeft); + } + } + }]; + + // 字符键(letter/digit/sym)使用等宽约束,自动分配可用空间; + // 功能键(shift/backspace/mode 等)使用固定宽度。 + BOOL isCharacterKey = [item.itemId hasPrefix:@"letter:"] || + [item.itemId hasPrefix:@"digit:"] || + [item.itemId hasPrefix:@"sym:"]; + + if (!isCharacterKey) { allCharacterKeys = NO; } + + if (isCharacterKey && usingUniformWidth) { + // 使用跨行统一的固定宽度 + CGFloat w = uniformCharWidth; + [btn mas_makeConstraints:^(MASConstraintMaker *make) { + make.width.mas_equalTo(w); + }]; + } else if (isCharacterKey) { + // 行内等宽:所有字符键与第一个字符键等宽 + if (firstCharBtn) { + [btn mas_makeConstraints:^(MASConstraintMaker *make) { + make.width.equalTo(firstCharBtn); + }]; + } else { + firstCharBtn = btn; + } + } else { + CGFloat width = [self.layoutEngine widthForItem:item key:btn.key]; + if (width > 0.0) { + [btn mas_makeConstraints:^(MASConstraintMaker *make) { + make.width.mas_equalTo(width); + }]; + } + } + + previous = btn; + } + + if (!previous) { return; } + + // 当使用统一宽度且顶层行全部为字符键时,跳过右锚约束以实现左对齐(列对齐) + BOOL skipRightAnchor = isTopLevelRow && usingUniformWidth && allCharacterKeys; + + if (!skipRightAnchor) { + [previous mas_makeConstraints:^(MASConstraintMaker *make) { + if (rightSpacer) { + make.right.equalTo(rightSpacer.mas_left).offset(-gap); + } else { + make.right.equalTo(container.mas_right).offset(-insetRight); + } + }]; + } +} + +- (KBKeyButton *)kb_buttonForItem:(KBKeyboardRowItem *)item + shiftOn:(BOOL)shiftOn + backspaceHandler:(KBBackspaceLongPressHandler *)backspaceHandler + target:(id)target + action:(SEL)action { + if (item.itemId.length == 0) { return nil; } + KBKeyboardKeyDef *def = [self.layoutConfig keyDefForIdentifier:item.itemId]; + KBKey *key = [self.keyFactory keyForItemId:item.itemId shiftOn:shiftOn]; + if (!key) { return nil; } + + KBKeyButton *btn = [[KBKeyButton alloc] init]; + btn.key = key; + [btn setTitle:key.title forState:UIControlStateNormal]; + + UIColor *bgColor = [self kb_backgroundColorForItem:item keyDef:def]; + if (bgColor) { + btn.customBackgroundColor = bgColor; + } + + CGFloat fontSize = [self.layoutEngine fontSizeForItem:item key:key]; + if (fontSize > 0.0) { + btn.titleLabel.font = [UIFont systemFontOfSize:fontSize weight:UIFontWeightSemibold]; + } + + [btn applyThemeForCurrentKey]; + if (target && action) { + [btn addTarget:target action:action forControlEvents:UIControlEventTouchDown]; + } + + if (key.type == KBKeyTypeBackspace) { + [backspaceHandler bindDeleteButton:btn showClearLabel:YES]; + } + if (key.type == KBKeyTypeShift) { + btn.selected = shiftOn; + } + [self kb_applySymbolIfNeededForButton:btn keyDef:def fontSize:fontSize]; + return btn; +} + +- (UIColor *)kb_backgroundColorForItem:(KBKeyboardRowItem *)item keyDef:(KBKeyboardKeyDef *)def { + NSString *hex = def.backgroundColor; + if (hex.length == 0) { + hex = self.layoutConfig.defaultKeyBackground; + } + if (hex.length == 0) { return nil; } + return [KBSkinManager colorFromHexString:hex defaultColor:nil]; +} + +- (void)kb_applySymbolIfNeededForButton:(KBKeyButton *)button + keyDef:(KBKeyboardKeyDef *)def + fontSize:(CGFloat)fontSize { + if (!button || !def) { return; } + if (button.iconView.image != nil) { return; } + NSString *symbolName = button.isSelected ? def.selectedSymbolName : def.symbolName; + if (symbolName.length == 0) { return; } + + UIImage *image = [UIImage systemImageNamed:symbolName]; + if (!image) { return; } + + UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:fontSize weight:UIFontWeightSemibold]; + image = [image imageWithConfiguration:config]; + + button.iconView.image = image; + button.iconView.hidden = NO; + button.iconView.contentMode = UIViewContentModeCenter; + button.titleLabel.hidden = YES; + + UIColor *textColor = [KBSkinManager shared].current.keyTextColor ?: [UIColor blackColor]; + button.iconView.tintColor = button.isSelected ? [UIColor blackColor] : textColor; +} + +@end diff --git a/CustomKeyboard/View/KBKeyboardView/KBKeyboardRowContainerBuilder.h b/CustomKeyboard/View/KBKeyboardView/KBKeyboardRowContainerBuilder.h new file mode 100644 index 0000000..46d2b8c --- /dev/null +++ b/CustomKeyboard/View/KBKeyboardView/KBKeyboardRowContainerBuilder.h @@ -0,0 +1,24 @@ +// +// KBKeyboardRowContainerBuilder.h +// CustomKeyboard +// + +#import +#import + +@class KBKeyboardRowConfig; + +NS_ASSUME_NONNULL_BEGIN + +@interface KBKeyboardRowContainerBuilder : NSObject + +- (void)rebuildRowContainersForRows:(NSArray *)rowConfigs + inContainer:(UIView *)container + rowViews:(NSMutableArray *)rowViews + rowSpacing:(CGFloat)rowSpacing + topInset:(CGFloat)topInset + bottomInset:(CGFloat)bottomInset; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CustomKeyboard/View/KBKeyboardView/KBKeyboardRowContainerBuilder.m b/CustomKeyboard/View/KBKeyboardView/KBKeyboardRowContainerBuilder.m new file mode 100644 index 0000000..3c61063 --- /dev/null +++ b/CustomKeyboard/View/KBKeyboardView/KBKeyboardRowContainerBuilder.m @@ -0,0 +1,59 @@ +// +// KBKeyboardRowContainerBuilder.m +// CustomKeyboard +// + +#import "KBKeyboardRowContainerBuilder.h" +#import + +@implementation KBKeyboardRowContainerBuilder + +- (void)rebuildRowContainersForRows:(NSArray *)rowConfigs + inContainer:(UIView *)container + rowViews:(NSMutableArray *)rowViews + rowSpacing:(CGFloat)rowSpacing + topInset:(CGFloat)topInset + bottomInset:(CGFloat)bottomInset { + if (!container || !rowViews) { return; } + + for (UIView *row in rowViews) { + [row removeFromSuperview]; + } + [rowViews removeAllObjects]; + + NSUInteger rowCount = rowConfigs.count; + if (rowCount == 0) { return; } + + UIView *firstRow = nil; + UIView *previousRow = nil; + for (NSUInteger i = 0; i < rowCount; i++) { + UIView *rowView = [UIView new]; + [container addSubview:rowView]; + [rowViews addObject:rowView]; + + [rowView mas_makeConstraints:^(MASConstraintMaker *make) { + if (previousRow) { + make.top.equalTo(previousRow.mas_bottom).offset(rowSpacing); + } else { + make.top.equalTo(container.mas_top).offset(topInset); + } + make.left.right.equalTo(container); + // 所有行等高,自动根据可用空间分配行高 + if (firstRow) { + make.height.equalTo(firstRow); + } + }]; + + // 最后一行锚定到底部 + if (i == rowCount - 1) { + [rowView mas_makeConstraints:^(MASConstraintMaker *make) { + make.bottom.equalTo(container.mas_bottom).offset(-bottomInset); + }]; + } + + if (!firstRow) { firstRow = rowView; } + previousRow = rowView; + } +} + +@end diff --git a/CustomKeyboard/View/KBKeyboardView.h b/CustomKeyboard/View/KBKeyboardView/KBKeyboardView.h similarity index 100% rename from CustomKeyboard/View/KBKeyboardView.h rename to CustomKeyboard/View/KBKeyboardView/KBKeyboardView.h diff --git a/CustomKeyboard/View/KBKeyboardView/KBKeyboardView.m b/CustomKeyboard/View/KBKeyboardView/KBKeyboardView.m new file mode 100644 index 0000000..e574bc3 --- /dev/null +++ b/CustomKeyboard/View/KBKeyboardView/KBKeyboardView.m @@ -0,0 +1,414 @@ +// +// 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 diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index f28009c..8c78ad1 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -325,6 +325,14 @@ A1F0C1C32FABCDEF12345678 /* KBInviteCodeModel.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1C12FABCDEF12345678 /* KBInviteCodeModel.m */; }; A1F0C1D22FACAD0012345678 /* KBMaiPointReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1D12FACAD0012345678 /* KBMaiPointReporter.m */; }; A1F0C1D32FACAD0012345678 /* KBMaiPointReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1D12FACAD0012345678 /* KBMaiPointReporter.m */; }; + B7F1A1D02F90000100000001 /* KBKeyboardKeyFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = B7F1A1C52F90000100000001 /* KBKeyboardKeyFactory.m */; }; + B7F1A1D12F90000100000001 /* KBKeyboardLayoutEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = B7F1A1C72F90000100000001 /* KBKeyboardLayoutEngine.m */; }; + B7F1A1D22F90000100000001 /* KBKeyboardRowBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = B7F1A1C92F90000100000001 /* KBKeyboardRowBuilder.m */; }; + B7F1A1D32F90000100000001 /* KBKeyboardInputHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = B7F1A1CB2F90000100000001 /* KBKeyboardInputHandler.m */; }; + B7F1A1D42F90000100000001 /* KBKeyboardLegacyBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = B7F1A1CD2F90000100000001 /* KBKeyboardLegacyBuilder.m */; }; + B7F1A1D52F90000100000001 /* KBKeyboardLegacyLayoutProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = B7F1A1CF2F90000100000001 /* KBKeyboardLegacyLayoutProvider.m */; }; + B7F1A1D62F90000100000001 /* KBKeyboardInteractionHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = B7F1A1D82F90000100000001 /* KBKeyboardInteractionHandler.m */; }; + B7F1A1D92F90000100000001 /* KBKeyboardRowContainerBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = B7F1A1DB2F90000100000001 /* KBKeyboardRowContainerBuilder.m */; }; EB72B60040437E3C0A4890FC /* KBShopThemeDetailModel.m in Sources */ = {isa = PBXBuildFile; fileRef = B9F60894E529C3EDAF6BAC3D /* KBShopThemeDetailModel.m */; }; ECC9EE02174D86E8D792472F /* Pods_keyBoard.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 967065BB5230E43F293B3AF9 /* Pods_keyBoard.framework */; }; /* End PBXBuildFile section */ @@ -898,6 +906,22 @@ A1F0C1D02FACAD0012345678 /* KBMaiPointReporter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMaiPointReporter.h; sourceTree = ""; }; A1F0C1D12FACAD0012345678 /* KBMaiPointReporter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMaiPointReporter.m; sourceTree = ""; }; B12EC429812407B9F0E67565 /* Pods-CustomKeyboard.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CustomKeyboard.release.xcconfig"; path = "Target Support Files/Pods-CustomKeyboard/Pods-CustomKeyboard.release.xcconfig"; sourceTree = ""; }; + B7F1A1C42F90000100000001 /* KBKeyboardKeyFactory.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardKeyFactory.h; sourceTree = ""; }; + B7F1A1C52F90000100000001 /* KBKeyboardKeyFactory.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardKeyFactory.m; sourceTree = ""; }; + B7F1A1C62F90000100000001 /* KBKeyboardLayoutEngine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardLayoutEngine.h; sourceTree = ""; }; + B7F1A1C72F90000100000001 /* KBKeyboardLayoutEngine.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardLayoutEngine.m; sourceTree = ""; }; + B7F1A1C82F90000100000001 /* KBKeyboardRowBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardRowBuilder.h; sourceTree = ""; }; + B7F1A1C92F90000100000001 /* KBKeyboardRowBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardRowBuilder.m; sourceTree = ""; }; + B7F1A1CA2F90000100000001 /* KBKeyboardInputHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardInputHandler.h; sourceTree = ""; }; + B7F1A1CB2F90000100000001 /* KBKeyboardInputHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardInputHandler.m; sourceTree = ""; }; + B7F1A1CC2F90000100000001 /* KBKeyboardLegacyBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardLegacyBuilder.h; sourceTree = ""; }; + B7F1A1CD2F90000100000001 /* KBKeyboardLegacyBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardLegacyBuilder.m; sourceTree = ""; }; + B7F1A1CE2F90000100000001 /* KBKeyboardLegacyLayoutProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardLegacyLayoutProvider.h; sourceTree = ""; }; + B7F1A1CF2F90000100000001 /* KBKeyboardLegacyLayoutProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardLegacyLayoutProvider.m; sourceTree = ""; }; + B7F1A1D72F90000100000001 /* KBKeyboardInteractionHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardInteractionHandler.h; sourceTree = ""; }; + B7F1A1D82F90000100000001 /* KBKeyboardInteractionHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardInteractionHandler.m; sourceTree = ""; }; + B7F1A1DA2F90000100000001 /* KBKeyboardRowContainerBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardRowContainerBuilder.h; sourceTree = ""; }; + B7F1A1DB2F90000100000001 /* KBKeyboardRowContainerBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardRowContainerBuilder.m; sourceTree = ""; }; B8CA018AB878499327504AAD /* Pods-CustomKeyboard.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CustomKeyboard.debug.xcconfig"; path = "Target Support Files/Pods-CustomKeyboard/Pods-CustomKeyboard.debug.xcconfig"; sourceTree = ""; }; B9F60894E529C3EDAF6BAC3D /* KBShopThemeDetailModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBShopThemeDetailModel.m; sourceTree = ""; }; E2A844CD2D8584596DBE6316 /* KBShopThemeTagModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBShopThemeTagModel.m; sourceTree = ""; }; @@ -1084,6 +1108,31 @@ path = Resource; sourceTree = ""; }; + 043213C72F57DD600065C888 /* KBKeyboardView */ = { + isa = PBXGroup; + children = ( + 04FC956B2EB054B7007BD342 /* KBKeyboardView.h */, + 04FC956C2EB054B7007BD342 /* KBKeyboardView.m */, + B7F1A1C42F90000100000001 /* KBKeyboardKeyFactory.h */, + B7F1A1C52F90000100000001 /* KBKeyboardKeyFactory.m */, + B7F1A1C62F90000100000001 /* KBKeyboardLayoutEngine.h */, + B7F1A1C72F90000100000001 /* KBKeyboardLayoutEngine.m */, + B7F1A1C82F90000100000001 /* KBKeyboardRowBuilder.h */, + B7F1A1C92F90000100000001 /* KBKeyboardRowBuilder.m */, + B7F1A1CA2F90000100000001 /* KBKeyboardInputHandler.h */, + B7F1A1CB2F90000100000001 /* KBKeyboardInputHandler.m */, + B7F1A1CC2F90000100000001 /* KBKeyboardLegacyBuilder.h */, + B7F1A1CD2F90000100000001 /* KBKeyboardLegacyBuilder.m */, + B7F1A1CE2F90000100000001 /* KBKeyboardLegacyLayoutProvider.h */, + B7F1A1CF2F90000100000001 /* KBKeyboardLegacyLayoutProvider.m */, + B7F1A1D72F90000100000001 /* KBKeyboardInteractionHandler.h */, + B7F1A1D82F90000100000001 /* KBKeyboardInteractionHandler.m */, + B7F1A1DA2F90000100000001 /* KBKeyboardRowContainerBuilder.h */, + B7F1A1DB2F90000100000001 /* KBKeyboardRowContainerBuilder.m */, + ); + path = KBKeyboardView; + sourceTree = ""; + }; 0450ABFB2EF11E4400B6AF06 /* Converts */ = { isa = PBXGroup; children = ( @@ -1672,8 +1721,7 @@ 04A9A67D2EB9E1690023B8F4 /* KBResponderUtils.h */, 04FC95682EB05497007BD342 /* KBKeyButton.h */, 04FC95692EB05497007BD342 /* KBKeyButton.m */, - 04FC956B2EB054B7007BD342 /* KBKeyboardView.h */, - 04FC956C2EB054B7007BD342 /* KBKeyboardView.m */, + 043213C72F57DD600065C888 /* KBKeyboardView */, 046131122ECF454500A6FADF /* KBKeyPreviewView.h */, 046131132ECF454500A6FADF /* KBKeyPreviewView.m */, 04FC95772EB09BC8007BD342 /* KBKeyBoardMainView.h */, @@ -2490,6 +2538,14 @@ 049FB23F2EC4B6EF00FAB05D /* KBULBridgeNotification.m in Sources */, 04791F992ED49CE7004E8522 /* KBFont.m in Sources */, 04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */, + B7F1A1D02F90000100000001 /* KBKeyboardKeyFactory.m in Sources */, + B7F1A1D12F90000100000001 /* KBKeyboardLayoutEngine.m in Sources */, + B7F1A1D22F90000100000001 /* KBKeyboardRowBuilder.m in Sources */, + B7F1A1D32F90000100000001 /* KBKeyboardInputHandler.m in Sources */, + B7F1A1D42F90000100000001 /* KBKeyboardLegacyBuilder.m in Sources */, + B7F1A1D52F90000100000001 /* KBKeyboardLegacyLayoutProvider.m in Sources */, + B7F1A1D62F90000100000001 /* KBKeyboardInteractionHandler.m in Sources */, + B7F1A1D92F90000100000001 /* KBKeyboardRowContainerBuilder.m in Sources */, 04FC95672EB0546C007BD342 /* KBKey.m in Sources */, A1B2C3F42EB35A9900000001 /* KBFullAccessGuideView.m in Sources */, 0498BD8F2EE6A3BD006CC1D5 /* KBMyMainModel.m in Sources */,