// // KBKeyboardView.m // CustomKeyboard // #import "KBKeyboardView.h" #import "KBKeyButton.h" #import "KBKey.h" #import "KBResponderUtils.h" // 封装的响应链工具 #import "KBSkinManager.h" #import "KBKeyPreviewView.h" // UI 常量统一管理,方便后续调试样式(以 375 宽设计稿为基准,通过 KBFit 做等比缩放) #define kKBRowVerticalSpacing KBFit(8.0f) #define kKBRowHorizontalInset KBFit(6.0f) #define kKBRowHeight KBFit(40.0f) static const NSTimeInterval kKBBackspaceLongPressMinDuration = 0.35; static const NSTimeInterval kKBBackspaceRepeatInterval = 0.06; static const NSTimeInterval kKBBackspaceChunkStartDelay = 0.7; static const NSTimeInterval kKBBackspaceChunkRepeatInterval = 0.1; static const NSTimeInterval kKBBackspaceChunkFastDelay = 1.4; static const NSInteger kKBBackspaceChunkSize = 6; static const NSInteger kKBBackspaceChunkSizeFast = 12; 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; typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { KBBackspaceChunkClassUnknown = 0, KBBackspaceChunkClassWhitespace, KBBackspaceChunkClassASCIIWord, KBBackspaceChunkClassPunctuation, KBBackspaceChunkClassOther }; @interface KBKeyboardView () @property (nonatomic, strong) UIView *row1; @property (nonatomic, strong) UIView *row2; @property (nonatomic, strong) UIView *row3; @property (nonatomic, strong) UIView *row4; @property (nonatomic, strong) NSArray *> *keysForRows; // 长按退格的一次次删除控制标记(不使用 NSTimer,仅用 GCD 递归调度) @property (nonatomic, assign) BOOL backspaceHoldActive; @property (nonatomic, assign) NSTimeInterval backspaceHoldStartTime; @property (nonatomic, strong) KBKeyPreviewView *previewView; @end @implementation KBKeyboardView - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { self.backgroundColor = [KBSkinManager shared].current.keyboardBackground; _layoutStyle = KBKeyboardLayoutStyleLetters; // 默认小写:与需求一致,初始不开启 Shift _shiftOn = NO; _symbolsMoreOn = NO; // 数字面板默认第一页(123) [self buildBase]; [self reloadKeys]; } return self; } // 当切换大布局(字母/数字)时,重置数字二级页状态 - (void)setLayoutStyle:(KBKeyboardLayoutStyle)layoutStyle { _layoutStyle = layoutStyle; if (_layoutStyle != KBKeyboardLayoutStyleNumbers) { _symbolsMoreOn = NO; } } #pragma mark - Base Layout - (void)buildBase { [self addSubview:self.row1]; [self addSubview:self.row2]; [self addSubview:self.row3]; [self addSubview:self.row4]; [self.row1 mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.mas_top).offset(kKBRowVerticalSpacing); make.left.right.equalTo(self); make.height.mas_equalTo(kKBRowHeight); }]; [self.row2 mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.row1.mas_bottom).offset(kKBRowVerticalSpacing); make.left.right.equalTo(self); make.height.equalTo(self.row1); }]; [self.row3 mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.row2.mas_bottom).offset(kKBRowVerticalSpacing); make.left.right.equalTo(self); make.height.equalTo(self.row1); }]; [self.row4 mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.row3.mas_bottom).offset(kKBRowVerticalSpacing); make.left.right.equalTo(self); make.height.equalTo(self.row1); make.bottom.equalTo(self.mas_bottom).offset(-6); }]; } #pragma mark - Public - (void)reloadKeys { // 移除旧按钮 for (UIView *row in @[self.row1, self.row2, self.row3, self.row4]) { [row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; } self.keysForRows = [self buildKeysForCurrentLayout]; if (self.keysForRows.count < 4) return; [self buildRow:self.row1 withKeys:self.keysForRows[0]]; // 第二行:字母布局时通过左右等宽占位让整行居中 CGFloat row2Spacer = (self.layoutStyle == KBKeyboardLayoutStyleLetters) ? kKBLettersRow2EdgeSpacerMultiplier : 0.0; [self buildRow:self.row2 withKeys:self.keysForRows[1] edgeSpacerMultiplier:row2Spacer]; [self buildRow:self.row3 withKeys:self.keysForRows[2]]; [self buildRow:self.row4 withKeys:self.keysForRows[3]]; } #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 { NSParameterAssert(charString.length == 1); 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 *customAI = [KBKey keyWithIdentifier:@"ai" title:@"AI" output:@"" type:KBKeyTypeCustom]; 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, customAI, emoji, space, ret ]; } // 底部控制行(数字布局) - (NSArray *)kb_bottomControlRowKeysForNumbersLayout { KBKey *modeABC = [KBKey keyWithIdentifier:@"mode_abc" title:@"abc" output:@"" type:KBKeyTypeModeChange]; KBKey *customAI = [KBKey keyWithIdentifier:@"ai" title:@"AI" output:@"" type:KBKeyTypeCustom]; 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, customAI, emoji, space, ret ]; } #pragma mark - Row Building - (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、AI、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:UIControlEventTouchUpInside]; [row addSubview:btn]; // ⌫ 长按:开始连续逐个删除(无需 NSTimer)。使用 UILongPressGestureRecognizer 识别长按 if (key.type == KBKeyTypeBackspace) { UILongPressGestureRecognizer *lp = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onBackspaceLongPress:)]; lp.minimumPressDuration = kKBBackspaceLongPressMinDuration; lp.cancelsTouchesInView = YES; // 被识别为长按时,取消普通点击 [btn addGestureRecognizer:lp]; } // 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、AI、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/AI/#+= 等, // 也将其约束为 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、AI:正方形(宽 = 行高 * 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 - 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; }]; } // 长按退格:先连续单删,稍后切换为按段删除;松手停止。(不使用 NSTimer/DisplayLink) - (void)onBackspaceLongPress:(UILongPressGestureRecognizer *)gr { switch (gr.state) { case UIGestureRecognizerStateBegan: { self.backspaceHoldActive = YES; self.backspaceHoldStartTime = [NSDate date].timeIntervalSinceReferenceDate; [self kb_backspaceStep]; } break; case UIGestureRecognizerStateEnded: case UIGestureRecognizerStateCancelled: case UIGestureRecognizerStateFailed: { self.backspaceHoldActive = NO; } break; default: break; } } #pragma mark - Helpers // 单步删除并在需要时安排下一次,直到松手或无内容 - (void)kb_backspaceStep { if (!self.backspaceHoldActive) { return; } UIInputViewController *ivc = KBFindInputViewController(self); if (!ivc) { self.backspaceHoldActive = NO; return; } id proxy = ivc.textDocumentProxy; NSString *before = proxy.documentContextBeforeInput ?: @""; if (before.length <= 0) { self.backspaceHoldActive = NO; return; } NSTimeInterval elapsed = [NSDate date].timeIntervalSinceReferenceDate - self.backspaceHoldStartTime; NSInteger deleteCount = [self kb_backspaceDeleteCountForContext:before elapsed:elapsed]; for (NSInteger i = 0; i < deleteCount; i++) { [proxy deleteBackward]; } NSTimeInterval interval = [self kb_backspaceRepeatIntervalForElapsed:elapsed]; __weak typeof(self) weakSelf = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(interval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ __strong typeof(weakSelf) selfStrong = weakSelf; [selfStrong kb_backspaceStep]; }); } - (NSTimeInterval)kb_backspaceRepeatIntervalForElapsed:(NSTimeInterval)elapsed { if (elapsed >= kKBBackspaceChunkStartDelay) { return kKBBackspaceChunkRepeatInterval; } return kKBBackspaceRepeatInterval; } - (NSInteger)kb_backspaceDeleteCountForContext:(NSString *)context elapsed:(NSTimeInterval)elapsed { if (elapsed < kKBBackspaceChunkStartDelay) { return 1; } NSInteger maxCount = (elapsed >= kKBBackspaceChunkFastDelay) ? kKBBackspaceChunkSizeFast : kKBBackspaceChunkSize; return [self kb_backspaceChunkDeleteCountForContext:context maxCount:maxCount]; } - (NSInteger)kb_backspaceChunkDeleteCountForContext:(NSString *)context maxCount:(NSInteger)maxCount { if (context.length == 0) { return 1; } static NSCharacterSet *whitespaceSet = nil; static NSCharacterSet *asciiWordSet = nil; static NSCharacterSet *punctuationSet = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet]; asciiWordSet = [NSCharacterSet characterSetWithCharactersInString: @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"]; punctuationSet = [NSCharacterSet punctuationCharacterSet]; }); __block NSInteger deleteCount = 0; __block KBBackspaceChunkClass chunkClass = KBBackspaceChunkClassUnknown; [context enumerateSubstringsInRange:NSMakeRange(0, context.length) options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) { if (substring.length == 0) { return; } KBBackspaceChunkClass currentClass = KBBackspaceChunkClassOther; if ([substring rangeOfCharacterFromSet:whitespaceSet].location != NSNotFound) { currentClass = KBBackspaceChunkClassWhitespace; } else if ([substring rangeOfCharacterFromSet:asciiWordSet].location != NSNotFound) { currentClass = KBBackspaceChunkClassASCIIWord; } else if ([substring rangeOfCharacterFromSet:punctuationSet].location != NSNotFound) { currentClass = KBBackspaceChunkClassPunctuation; } if (chunkClass == KBBackspaceChunkClassUnknown) { chunkClass = currentClass; } else if (chunkClass != currentClass) { *stop = YES; return; } deleteCount += 1; if (deleteCount >= maxCount) { *stop = YES; } }]; return MAX(deleteCount, 1); } #pragma mark - Lazy - (UIView *)row1 { if (!_row1) _row1 = [UIView new]; return _row1; } - (UIView *)row2 { if (!_row2) _row2 = [UIView new]; return _row2; } - (UIView *)row3 { if (!_row3) _row3 = [UIView new]; return _row3; } - (UIView *)row4 { if (!_row4) _row4 = [UIView new]; return _row4; } @end