Files
keyboard/CustomKeyboard/View/KBKeyboardView.m
2025-11-17 20:07:39 +08:00

356 lines
16 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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<NSArray<KBKey *> *> *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<NSArray<KBKey *> *> *)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:@"\""] ];
r3 = @[ [KBKey keyWithTitle:@"#+=" type:KBKeyTypeSymbolsToggle],
[KBKey keyWithTitle:@"," output:@","], [KBKey keyWithTitle:@"." output:@"."], [KBKey keyWithTitle:@"?" output:@"?"],
[KBKey keyWithTitle:@"!" output:@"!"], [KBKey keyWithTitle:@"'" output:@"'"],
[KBKey keyWithTitle:@"" type:KBKeyTypeBackspace] ];
} 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:@""] ];
r3 = @[ [KBKey keyWithTitle:@"123" type:KBKeyTypeSymbolsToggle],
[KBKey keyWithTitle:@"," output:@","], [KBKey keyWithTitle:@"." output:@"."], [KBKey keyWithTitle:@"?" output:@"?"],
[KBKey keyWithTitle:@"!" output:@"!"], [KBKey keyWithTitle:@"'" output:@"'"],
[KBKey keyWithTitle:@"" type:KBKeyTypeBackspace] ];
}
NSArray *r4 = @[ [KBKey keyWithTitle:@"abc" type:KBKeyTypeModeChange],
[KBKey keyWithTitle:@"AI" type:KBKeyTypeCustom],
[KBKey keyWithTitle:@"space" type:KBKeyTypeSpace],
[KBKey keyWithTitle:KBLocalized(@"Send") type:KBKeyTypeReturn] ];
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;
[row1 addObject:[KBKey keyWithTitle:shown output:shown]];
}
NSMutableArray *row2 = [NSMutableArray arrayWithCapacity:r2.count];
for (NSString *s in r2) {
NSString *shown = self.shiftOn ? s : s.lowercaseString;
[row2 addObject:[KBKey keyWithTitle:shown output:shown]];
}
NSMutableArray *row3 = [NSMutableArray array];
[row3 addObject:[KBKey keyWithTitle:@"" type:KBKeyTypeShift]];
for (NSString *s in r3chars) {
NSString *shown = self.shiftOn ? s : s.lowercaseString;
[row3 addObject:[KBKey keyWithTitle:shown output:shown]];
}
[row3 addObject:[KBKey keyWithTitle:@"" type:KBKeyTypeBackspace]];
NSArray *row4 = @[ [KBKey keyWithTitle:@"123" type:KBKeyTypeModeChange],
[KBKey keyWithTitle:@"AI" type:KBKeyTypeCustom],
[KBKey keyWithTitle:@"space" type:KBKeyTypeSpace],
[KBKey keyWithTitle:KBLocalized(@"Send") type:KBKeyTypeReturn] ];
return @[row1.copy, row2.copy, row3.copy, row4];
}
- (void)buildRow:(UIView *)row withKeys:(NSArray<KBKey *> *)keys {
[self buildRow:row withKeys:keys edgeSpacerMultiplier:0.0];
}
- (void)buildRow:(UIView *)row withKeys:(NSArray<KBKey *> *)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<UITextDocumentProxy> 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