Files
keyboard/CustomKeyboard/View/KBKeyboardView.m

702 lines
30 KiB
Mathematica
Raw Normal View History

2025-10-28 10:18:10 +08:00
//
// KBKeyboardView.m
// CustomKeyboard
//
#import "KBKeyboardView.h"
#import "KBKeyButton.h"
#import "KBKey.h"
2025-11-04 16:37:24 +08:00
#import "KBResponderUtils.h" //
2025-11-04 21:01:46 +08:00
#import "KBSkinManager.h"
2025-11-20 21:11:27 +08:00
#import "KBKeyPreviewView.h"
2025-10-28 10:18:10 +08:00
// UI 便
static const CGFloat kKBRowVerticalSpacing = 8.0;
static const CGFloat kKBRowHorizontalInset = 6.0;
static const CGFloat kKBRowHeight = 40.0;
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;
2025-10-28 10:18:10 +08:00
@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;
2025-11-04 16:37:24 +08:00
// 退使 NSTimer GCD
@property (nonatomic, assign) BOOL backspaceHoldActive;
2025-11-20 21:11:27 +08:00
@property (nonatomic, strong) KBKeyPreviewView *previewView;
2025-10-28 10:18:10 +08:00
@end
@implementation KBKeyboardView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
2025-11-04 21:01:46 +08:00
self.backgroundColor = [KBSkinManager shared].current.keyboardBackground;
2025-10-28 10:18:10 +08:00
_layoutStyle = KBKeyboardLayoutStyleLetters;
2025-10-28 19:24:35 +08:00
// Shift
_shiftOn = NO;
2025-10-28 20:03:43 +08:00
_symbolsMoreOn = NO; // 123
2025-10-28 10:18:10 +08:00
[self buildBase];
[self reloadKeys];
}
return self;
}
2025-10-28 20:03:43 +08:00
// /
- (void)setLayoutStyle:(KBKeyboardLayoutStyle)layoutStyle {
_layoutStyle = layoutStyle;
if (_layoutStyle != KBKeyboardLayoutStyleNumbers) {
_symbolsMoreOn = NO;
}
}
#pragma mark - Base Layout
2025-10-28 10:18:10 +08:00
- (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);
2025-10-28 10:18:10 +08:00
make.left.right.equalTo(self);
make.height.mas_equalTo(kKBRowHeight);
2025-10-28 10:18:10 +08:00
}];
[self.row2 mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.row1.mas_bottom).offset(kKBRowVerticalSpacing);
2025-10-28 10:18:10 +08:00
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);
2025-10-28 10:18:10 +08:00
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);
2025-10-28 10:18:10 +08:00
make.left.right.equalTo(self);
make.height.equalTo(self.row1);
make.bottom.equalTo(self.mas_bottom).offset(-6);
}];
}
#pragma mark - Public
2025-10-28 10:18:10 +08:00
- (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;
2025-10-28 10:18:10 +08:00
[self buildRow:self.row1 withKeys:self.keysForRows[0]];
2025-10-28 10:18:10 +08:00
//
CGFloat row2Spacer = (self.layoutStyle == KBKeyboardLayoutStyleLetters)
? kKBLettersRow2EdgeSpacerMultiplier : 0.0;
2025-10-28 10:18:10 +08:00
[self buildRow:self.row2 withKeys:self.keysForRows[1] edgeSpacerMultiplier:row2Spacer];
2025-10-28 10:18:10 +08:00
[self buildRow:self.row3 withKeys:self.keysForRows[2]];
[self buildRow:self.row4 withKeys:self.keysForRows[3]];
}
#pragma mark - Key Model Construction
// KBKey
2025-10-28 10:18:10 +08:00
- (NSArray<NSArray<KBKey *> *> *)buildKeysForCurrentLayout {
if (self.layoutStyle == KBKeyboardLayoutStyleNumbers) {
return [self buildKeysForNumbersLayout];
} else {
return [self buildKeysForLettersLayout];
2025-10-28 10:18:10 +08:00
}
}
2025-10-28 10:18:10 +08:00
#pragma mark - Letters Layout
- (NSArray<NSArray<KBKey *> *> *)buildKeysForLettersLayout {
2025-10-28 10:18:10 +08:00
// 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]];
2025-10-28 19:24:35 +08:00
}
2025-10-28 10:18:10 +08:00
NSMutableArray *row2 = [NSMutableArray arrayWithCapacity:r2Letters.count];
for (NSString *s in r2Letters) {
[row2 addObject:[self kb_letterKeyWithChar:s]];
2025-10-28 19:24:35 +08:00
}
2025-10-28 10:18:10 +08:00
// Shift + Z...M + Backspace
2025-10-28 10:18:10 +08:00
NSMutableArray *row3 = [NSMutableArray array];
2025-11-18 20:53:47 +08:00
KBKey *shift = [KBKey keyWithIdentifier:@"shift"
title:@"⇧"
output:@""
type:KBKeyTypeShift];
2025-11-20 20:17:19 +08:00
// Shift
// - shift.caseVariant = Lower 使 KBSkinIconMap "shift"
// - shift.caseVariant = Upper 使 "shift_upper"
shift.caseVariant = self.shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower;
2025-11-18 20:53:47 +08:00
[row3 addObject:shift];
for (NSString *s in r3Letters) {
[row3 addObject:[self kb_letterKeyWithChar:s]];
2025-10-28 19:24:35 +08:00
}
2025-11-18 20:53:47 +08:00
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 {
2025-11-18 20:53:47 +08:00
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 ];
}
2025-10-28 10:18:10 +08:00
//
- (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 ];
2025-10-28 10:18:10 +08:00
}
#pragma mark - Row Building
2025-10-28 10:18:10 +08:00
- (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 {
2025-11-21 13:48:22 +08:00
// 4 使
// 123/ABCAISend Space
BOOL isBottomControlRow = [self kb_isBottomControlRowWithKeys:keys];
CGFloat spacing = 0; //
2025-10-28 10:18:10 +08:00
UIView *previous = nil;
UIView *leftSpacer = nil;
UIView *rightSpacer = nil;
2025-10-28 10:18:10 +08:00
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);
2025-10-28 10:18:10 +08:00
make.centerY.equalTo(row);
make.height.mas_equalTo(1);
}];
[rightSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(row.mas_right).offset(-kKBRowHorizontalInset);
2025-10-28 10:18:10 +08:00
make.centerY.equalTo(row);
make.height.mas_equalTo(1);
}];
}
2025-10-28 10:18:10 +08:00
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];
2025-10-28 10:18:10 +08:00
[btn addTarget:self action:@selector(onKeyTapped:) forControlEvents:UIControlEventTouchUpInside];
[row addSubview:btn];
// NSTimer使 UILongPressGestureRecognizer
2025-11-04 16:37:24 +08:00
if (key.type == KBKeyTypeBackspace) {
UILongPressGestureRecognizer *lp =
[[UILongPressGestureRecognizer alloc] initWithTarget:self
action:@selector(onBackspaceLongPress:)];
lp.minimumPressDuration = kKBBackspaceLongPressMinDuration;
2025-11-04 16:37:24 +08:00
lp.cancelsTouchesInView = YES; //
[btn addGestureRecognizer:lp];
}
2025-10-28 20:11:40 +08:00
// Shift
if (key.type == KBKeyTypeShift) {
btn.selected = self.shiftOn;
}
2025-10-28 10:18:10 +08:00
[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);
2025-10-28 10:18:10 +08:00
}
}
}];
//
2025-10-28 10:18:10 +08:00
if (key.type == KBKeyTypeCharacter) {
if (previous && [previous isKindOfClass:[KBKeyButton class]]) {
KBKeyButton *prevBtn = (KBKeyButton *)previous;
if (prevBtn.key.type == KBKeyTypeCharacter) {
2025-10-28 10:18:10 +08:00
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(previous);
}];
}
}
} else {
// special keys:
2025-10-28 10:18:10 +08:00
}
previous = btn;
}
2025-11-21 13:48:22 +08:00
2025-10-28 10:18:10 +08:00
// 使
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);
}
}];
}
2025-10-28 10:18:10 +08:00
2025-11-21 13:48:22 +08:00
// 123/ABCAISend
// Space
if (isBottomControlRow) {
[self kb_applyBottomControlRowWidthInRow:row];
return;
}
2025-10-28 10:18:10 +08:00
//
KBKeyButton *firstChar = nil;
2025-11-20 20:17:19 +08:00
BOOL hasCharacterInRow = NO;
for (UIView *v in row.subviews) {
if (![v isKindOfClass:[KBKeyButton class]]) continue;
KBKeyButton *b = (KBKeyButton *)v;
2025-11-20 20:17:19 +08:00
if (b.key.type == KBKeyTypeCharacter) {
firstChar = b;
hasCharacterInRow = YES;
break;
}
2025-10-28 10:18:10 +08:00
}
// 使
2025-10-29 12:59:22 +08:00
if (!firstChar) {
for (UIView *v in row.subviews) {
if ([v isKindOfClass:[KBKeyButton class]]) {
firstChar = (KBKeyButton *)v;
2025-11-20 20:17:19 +08:00
break;
}
}
2025-10-29 12:59:22 +08:00
}
2025-10-28 10:18:10 +08:00
if (firstChar) {
2025-11-20 20:17:19 +08:00
// 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);
2025-11-20 20:17:19 +08:00
}];
}
for (UIView *v in row.subviews) {
if (![v isKindOfClass:[KBKeyButton class]]) continue;
KBKeyButton *b = (KBKeyButton *)v;
// self == self * k
if (b == firstChar) continue;
2025-10-28 10:18:10 +08:00
if (b.key.type == KBKeyTypeCharacter) continue;
2025-11-20 19:57:11 +08:00
2025-11-20 20:17:19 +08:00
BOOL isBottomModeKey = (b.key.type == KBKeyTypeModeChange) ||
(b.key.type == KBKeyTypeSymbolsToggle) ||
(b.key.type == KBKeyTypeCustom);
// ~
2025-11-20 20:17:19 +08:00
if (b.key.type == KBKeyTypeShift ||
b.key.type == KBKeyTypeBackspace ||
isBottomModeKey) {
2025-11-20 19:57:11 +08:00
[b mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(b.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier);
2025-11-20 19:57:11 +08:00
}];
continue;
}
2025-10-28 10:18:10 +08:00
CGFloat multiplier = 1.5;
2025-11-20 19:57:11 +08:00
// Space
2025-11-20 19:57:11 +08:00
if (b.key.type == KBKeyTypeSpace) {
multiplier = kKBSpaceWidthMultiplier;
2025-11-20 19:57:11 +08:00
}
2025-11-20 20:17:19 +08:00
// Send 2.4
2025-11-20 19:57:11 +08:00
else if (b.key.type == KBKeyTypeReturn) {
multiplier = kKBReturnWidthMultiplier;
2025-11-20 19:57:11 +08:00
}
// Globe
else if (b.key.type == KBKeyTypeGlobe) {
2025-10-28 10:18:10 +08:00
multiplier = 1.5;
}
2025-11-20 19:57:11 +08:00
2025-10-28 10:18:10 +08:00
[b mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(firstChar).multipliedBy(multiplier);
}];
}
2025-10-28 10:18:10 +08:00
//
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);
}];
}
}
}
2025-11-21 13:48:22 +08:00
#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/ABCAI = * multiplier
// - Send = 2
2025-11-21 13:48:22 +08:00
// - 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);
2025-11-21 13:48:22 +08:00
}];
[aiBtn mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(row.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier);
2025-11-21 13:48:22 +08:00
}];
[retBtn mas_makeConstraints:^(MASConstraintMaker *make) {
// Send 2
make.width.equalTo(modeBtn.mas_width).multipliedBy(2.0);
2025-11-21 13:48:22 +08:00
}];
// Space
}
2025-10-28 10:18:10 +08:00
#pragma mark - Actions
- (void)onKeyTapped:(KBKeyButton *)sender {
KBKey *key = sender.key;
if (key.type == KBKeyTypeShift) {
self.shiftOn = !self.shiftOn;
[self reloadKeys];
return;
}
2025-10-28 20:03:43 +08:00
if (key.type == KBKeyTypeSymbolsToggle) {
// 123 <-> #+=
self.symbolsMoreOn = !self.symbolsMoreOn;
[self reloadKeys];
return;
}
2025-10-28 10:18:10 +08:00
if ([self.delegate respondsToSelector:@selector(keyboardView:didTapKey:)]) {
[self.delegate keyboardView:self didTapKey:key];
}
}
2025-11-20 21:11:27 +08:00
//
- (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;
2025-11-20 21:11:27 +08:00
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);
2025-11-20 21:11:27 +08:00
self.previewView.center = CGPointMake(centerX, centerY);
self.previewView.alpha = 0.0;
self.previewView.hidden = NO;
[UIView animateWithDuration:kKBPreviewShowDuration
2025-11-20 21:11:27 +08:00
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
2025-11-20 21:11:27 +08:00
delay:0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn
animations:^{
self.previewView.alpha = 0.0;
}
completion:^(BOOL finished) {
self.previewView.hidden = YES;
}];
}
2025-11-04 16:37:24 +08:00
// 退使 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(), ^{
2025-11-04 16:37:24 +08:00
__strong typeof(weakSelf) selfStrong = weakSelf;
[selfStrong kb_backspaceStep];
});
}
2025-10-28 10:18:10 +08:00
#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