Files
keyboard/CustomKeyboard/View/KBKeyboardView.m
2025-11-21 21:50:40 +08:00

706 lines
30 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"
#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 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) 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;
@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<NSArray<KBKey *> *> *)buildKeysForCurrentLayout {
if (self.layoutStyle == KBKeyboardLayoutStyleNumbers) {
return [self buildKeysForNumbersLayout];
} else {
return [self buildKeysForLettersLayout];
}
}
#pragma mark - Letters Layout
- (NSArray<NSArray<KBKey *> *> *)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<NSArray<KBKey *> *> *)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_dollar" 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<KBKey *> *)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<KBKey *> *)kb_bottomControlRowKeysForLettersLayout {
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];
return @[ mode123, customAI, space, ret ];
}
// 底部控制行(数字布局)
- (NSArray<KBKey *> *)kb_bottomControlRowKeysForNumbersLayout {
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];
return @[ modeABC, customAI, space, ret ];
}
#pragma mark - Row Building
- (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 {
// 第 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<KBKey *> *)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;
KBKeyButton *aiBtn = nil;
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:
aiBtn = b;
break;
case KBKeyTypeSpace:
spaceBtn = b;
break;
case KBKeyTypeReturn:
retBtn = b;
break;
default:
break;
}
}
if (!modeBtn || !aiBtn || !spaceBtn || !retBtn) {
return;
}
// 行高由外部约束为固定值(等于 row1 高度),这里用行高作为“正方形”的边长。
[modeBtn mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(row.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier);
}];
[aiBtn mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(row.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier);
}];
[retBtn mas_makeConstraints:^(MASConstraintMaker *make) {
// Send 按钮:宽度为模式键的 2 倍
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 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)(kKBBackspaceRepeatInterval * 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