// // KBKeyboardView.m // CustomKeyboard // #import "KBKeyboardView.h" #import "KBKeyButton.h" #import "KBKey.h" #import "KBResponderUtils.h" // 封装的响应链工具 #import "KBSkinManager.h" @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; @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; } } - (void)buildBase { [self addSubview:self.row1]; [self addSubview:self.row2]; [self addSubview:self.row3]; [self addSubview:self.row4]; CGFloat vSpacing = 8; [self.row1 mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.mas_top).offset(8); make.left.right.equalTo(self); make.height.mas_equalTo(44); }]; [self.row2 mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.row1.mas_bottom).offset(vSpacing); make.left.right.equalTo(self); make.height.equalTo(self.row1); }]; [self.row3 mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.row2.mas_bottom).offset(vSpacing); make.left.right.equalTo(self); make.height.equalTo(self.row1); }]; [self.row4 mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.row3.mas_bottom).offset(vSpacing); make.left.right.equalTo(self); make.height.equalTo(self.row1); make.bottom.equalTo(self.mas_bottom).offset(-6); }]; } - (void)reloadKeys { // 移除旧按钮 for (UIView *row in @[self.row1, self.row2, self.row3, self.row4]) { [row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; } self.keysForRows = [self buildKeysForCurrentLayout]; [self buildRow:self.row1 withKeys:self.keysForRows[0]]; // 第二行:字母布局时通过左右等宽占位让整行居中 CGFloat row2Spacer = (self.layoutStyle == KBKeyboardLayoutStyleLetters) ? 0.5 : 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]]; } - (NSArray *> *)buildKeysForCurrentLayout { if (self.layoutStyle == KBKeyboardLayoutStyleNumbers) { // 数字/符号布局:3 行主键 + 底部控制行 NSArray *r1 = nil; NSArray *r2 = nil; NSArray *r3 = nil; if (!self.symbolsMoreOn) { // 数字第一页(123) r1 = @[ [KBKey keyWithTitle:@"1" output:@"1"], [KBKey keyWithTitle:@"2" output:@"2"], [KBKey keyWithTitle:@"3" output:@"3"], [KBKey keyWithTitle:@"4" output:@"4"], [KBKey keyWithTitle:@"5" output:@"5"], [KBKey keyWithTitle:@"6" output:@"6"], [KBKey keyWithTitle:@"7" output:@"7"], [KBKey keyWithTitle:@"8" output:@"8"], [KBKey keyWithTitle:@"9" output:@"9"], [KBKey keyWithTitle:@"0" output:@"0"] ]; r2 = @[ [KBKey keyWithTitle:@"-" output:@"-"], [KBKey keyWithTitle:@"/" output:@"/"], [KBKey keyWithTitle:@":" output:@":"], [KBKey keyWithTitle:@";" output:@";"], [KBKey keyWithTitle:@"(" output:@"("], [KBKey keyWithTitle:@")" output:@")"], [KBKey keyWithTitle:@"$" output:@"$"], [KBKey keyWithTitle:@"&" output:@"&"], [KBKey keyWithTitle:@"@" output:@"@"], [KBKey keyWithTitle:@"\"" output:@"\""] ]; // 第三行:左下角是“#+=”切换键,右下角是退格键 KBKey *toggle = [KBKey keyWithIdentifier:@"symbols_toggle_more" title:@"#+=" output:@"" type:KBKeyTypeSymbolsToggle]; KBKey *comma = [KBKey keyWithTitle:@"," output:@","]; KBKey *dot = [KBKey keyWithTitle:@"." output:@"."]; KBKey *q = [KBKey keyWithTitle:@"?" output:@"?"]; KBKey *ex = [KBKey keyWithTitle:@"!" output:@"!"]; KBKey *quote = [KBKey keyWithTitle:@"'" output:@"'"]; KBKey *back = [KBKey keyWithIdentifier:@"backspace" title:@"⌫" output:@"" type:KBKeyTypeBackspace]; r3 = @[ toggle, comma, dot, q, ex, quote, back ]; } else { // 数字第二页(#+=):前两行替换为更多符号,左下角按钮文案改为“123” r1 = @[ [KBKey keyWithTitle:@"[" output:@"["], [KBKey keyWithTitle:@"]" output:@"]"], [KBKey keyWithTitle:@"{" output:@"{"], [KBKey keyWithTitle:@"}" output:@"}"], [KBKey keyWithTitle:@"#" output:@"#"], [KBKey keyWithTitle:@"%" output:@"%"], [KBKey keyWithTitle:@"^" output:@"^"], [KBKey keyWithTitle:@"*" output:@"*"], [KBKey keyWithTitle:@"+" output:@"+"], [KBKey keyWithTitle:@"=" output:@"="] ]; r2 = @[ [KBKey keyWithTitle:@"_" output:@"_"], [KBKey keyWithTitle:@"\\" output:@"\\"], [KBKey keyWithTitle:@"|" output:@"|"], [KBKey keyWithTitle:@"~" output:@"~"], [KBKey keyWithTitle:@"<" output:@"<"], [KBKey keyWithTitle:@">" output:@">"], [KBKey keyWithTitle:@"$" output:@"$"], [KBKey keyWithTitle:@"€" output:@"€"], [KBKey keyWithTitle:@"£" output:@"£"], [KBKey keyWithTitle:@"•" output:@"•"] ]; KBKey *toggle = [KBKey keyWithIdentifier:@"symbols_toggle_123" title:@"123" output:@"" type:KBKeyTypeSymbolsToggle]; KBKey *comma = [KBKey keyWithTitle:@"," output:@","]; KBKey *dot = [KBKey keyWithTitle:@"." output:@"."]; KBKey *q = [KBKey keyWithTitle:@"?" output:@"?"]; KBKey *ex = [KBKey keyWithTitle:@"!" output:@"!"]; KBKey *quote = [KBKey keyWithTitle:@"'" output:@"'"]; KBKey *back = [KBKey keyWithIdentifier:@"backspace" title:@"⌫" output:@"" type:KBKeyTypeBackspace]; r3 = @[ toggle, comma, dot, q, ex, quote, back ]; } KBKey *modeABC = [KBKey keyWithIdentifier:@"mode_abc" title:@"abc" output:@"" type:KBKeyTypeModeChange]; KBKey *customAI = [KBKey keyWithIdentifier:@"ai" title:@"AI" 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]; NSArray *r4 = @[ modeABC, customAI, space, ret ]; return @[r1, r2, r3, r4]; } // 字母布局(QWERTY) NSArray *r1 = @[ @"Q", @"W", @"E", @"R", @"T", @"Y", @"U", @"I", @"O", @"P" ]; NSArray *r2 = @[ @"A", @"S", @"D", @"F", @"G", @"H", @"J", @"K", @"L" ]; NSArray *r3chars = @[ @"Z", @"X", @"C", @"V", @"B", @"N", @"M" ]; NSMutableArray *row1 = [NSMutableArray arrayWithCapacity:r1.count]; // 字母键标题与输出同时随 Shift 切换大小写,界面与输入保持一致 for (NSString *s in r1) { NSString *shown = self.shiftOn ? s : s.lowercaseString; NSString *identifier = [NSString stringWithFormat:@"letter_%@", s.lowercaseString]; KBKey *k = [KBKey keyWithIdentifier:identifier title:shown output:shown type:KBKeyTypeCharacter]; k.caseVariant = self.shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower; [row1 addObject:k]; } NSMutableArray *row2 = [NSMutableArray arrayWithCapacity:r2.count]; for (NSString *s in r2) { NSString *shown = self.shiftOn ? s : s.lowercaseString; NSString *identifier = [NSString stringWithFormat:@"letter_%@", s.lowercaseString]; KBKey *k = [KBKey keyWithIdentifier:identifier title:shown output:shown type:KBKeyTypeCharacter]; k.caseVariant = self.shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower; [row2 addObject:k]; } NSMutableArray *row3 = [NSMutableArray array]; KBKey *shift = [KBKey keyWithIdentifier:@"shift" title:@"⇧" output:@"" type:KBKeyTypeShift]; [row3 addObject:shift]; for (NSString *s in r3chars) { NSString *shown = self.shiftOn ? s : s.lowercaseString; NSString *identifier = [NSString stringWithFormat:@"letter_%@", s.lowercaseString]; KBKey *k = [KBKey keyWithIdentifier:identifier title:shown output:shown type:KBKeyTypeCharacter]; k.caseVariant = self.shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower; [row3 addObject:k]; } KBKey *backspace = [KBKey keyWithIdentifier:@"backspace" title:@"⌫" output:@"" type:KBKeyTypeBackspace]; [row3 addObject:backspace]; KBKey *mode123 = [KBKey keyWithIdentifier:@"mode_123" title:@"123" output:@"" type:KBKeyTypeModeChange]; KBKey *customAI = [KBKey keyWithIdentifier:@"ai" title:@"AI" 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]; NSArray *row4 = @[ mode123, customAI, space, ret ]; return @[row1.copy, row2.copy, row3.copy, row4]; } - (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 { CGFloat hInset = 6; // 行左右内边距 CGFloat spacing = 6; // 键与键之间的间距 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(hInset); make.centerY.equalTo(row); make.height.mas_equalTo(1); }]; [rightSpacer mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(row.mas_right).offset(-hInset); 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 addTarget:self action:@selector(onKeyTapped:) forControlEvents:UIControlEventTouchUpInside]; [row addSubview:btn]; // ⌫ 长按:开始连续逐个删除(无需 NSTimer)。使用 UILongPressGestureRecognizer 识别长按, // 在开始态触发递归的轻量调度,每次删除 1 个字符,直到松手或无内容。 if (key.type == KBKeyTypeBackspace) { UILongPressGestureRecognizer *lp = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onBackspaceLongPress:)]; // 稍短的判定时间,提升响应(默认约 0.5s)。 lp.minimumPressDuration = 0.35; 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(hInset); } } }]; // 宽度规则:字符键等宽;特殊键按倍数放大 if (key.type == KBKeyTypeCharacter) { if (previous && previous != nil) { if (((KBKeyButton *)previous).key.type == KBKeyTypeCharacter) { [btn mas_makeConstraints:^(MASConstraintMaker *make) { make.width.equalTo(previous); }]; } } } else { // special keys: give 1.5x of a character key by deferring constraint equalities after loop } previous = btn; } // 右侧使用内边距或右占位 [previous mas_makeConstraints:^(MASConstraintMaker *make) { if (rightSpacer) { make.right.equalTo(rightSpacer.mas_left).offset(-spacing); } else { make.right.equalTo(row.mas_right).offset(-hInset); } }]; // 第二遍:以首个字符键为基准,统一设置特殊键宽度倍数 KBKeyButton *firstChar = nil; for (KBKeyButton *b in row.subviews) { if ([b isKindOfClass:[KBKeyButton class]] && b.key.type == KBKeyTypeCharacter) { firstChar = b; break; } } // 若该行没有字符键(例如底部控制行),则使用行内第一个按钮作为基准宽度 if (!firstChar) { for (KBKeyButton *b in row.subviews) { if ([b isKindOfClass:[KBKeyButton class]]) { firstChar = b; break; } } } if (firstChar) { for (KBKeyButton *b in row.subviews) { if (![b isKindOfClass:[KBKeyButton class]]) continue; // 当本行没有字符键时,firstChar 可能是一个“特殊键”, // 避免对基准按钮自身添加 self == self * k 的无效约束 if (b == firstChar) continue; if (b.key.type == KBKeyTypeCharacter) continue; CGFloat multiplier = 1.5; if (b.key.type == KBKeyTypeSpace) multiplier = 4.0; if (b.key.type == KBKeyTypeReturn) multiplier = 1.8; if (b.key.type == KBKeyTypeModeChange || b.key.type == KBKeyTypeGlobe || b.key.type == KBKeyTypeShift || b.key.type == KBKeyTypeBackspace) { multiplier = 1.5; } [b mas_makeConstraints:^(MASConstraintMaker *make) { make.width.equalTo(firstChar).multipliedBy(multiplier); }]; } // 如果有左右占位,则把占位宽度设置为字符键宽度的一定倍数,以实现整体居中 if (leftSpacer && rightSpacer) { [leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) { make.width.equalTo(firstChar).multipliedBy(edgeSpacerMultiplier); }]; [rightSpacer mas_makeConstraints:^(MASConstraintMaker *make) { make.width.equalTo(firstChar).multipliedBy(edgeSpacerMultiplier); }]; } } } #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]; } } // 长按退格:按住时以小间隔逐个删除;松手停止。(不使用 NSTimer/DisplayLink) - (void)onBackspaceLongPress:(UILongPressGestureRecognizer *)gr { switch (gr.state) { case UIGestureRecognizerStateBegan: { self.backspaceHoldActive = YES; [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; } [proxy deleteBackward]; // 每次仅删 1 个 __weak typeof(self) weakSelf = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.06 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ __strong typeof(weakSelf) selfStrong = weakSelf; [selfStrong kb_backspaceStep]; }); } #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