Files
keyboard/CustomKeyboard/View/KBKeyboardView.m

1277 lines
51 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 21:01:46 +08:00
#import "KBSkinManager.h"
2025-11-20 21:11:27 +08:00
#import "KBKeyPreviewView.h"
2025-12-19 18:45:14 +08:00
#import "KBBackspaceLongPressHandler.h"
2026-01-08 16:54:38 +08:00
#import "KBKeyboardLayoutConfig.h"
2025-10-28 10:18:10 +08:00
2025-11-21 21:50:40 +08:00
// UI 便 375 稿 KBFit
#define kKBRowVerticalSpacing KBFit(8.0f)
#define kKBRowHorizontalInset KBFit(6.0f)
#define kKBRowHeight KBFit(40.0f)
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-12-19 18:45:14 +08:00
@property (nonatomic, strong) KBBackspaceLongPressHandler *backspaceHandler;
2025-11-20 21:11:27 +08:00
@property (nonatomic, strong) KBKeyPreviewView *previewView;
2026-01-08 16:54:38 +08:00
@property (nonatomic, strong) KBKeyboardLayoutConfig *layoutConfig;
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
2026-01-08 16:54:38 +08:00
self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig];
2025-12-19 18:45:14 +08:00
self.backspaceHandler = [[KBBackspaceLongPressHandler alloc] initWithContainerView:self];
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];
2026-01-08 16:54:38 +08:00
KBKeyboardLayoutConfig *config = [self kb_layoutConfig];
KBKeyboardLayout *layout = [self kb_layoutForName:@"letters"];
NSArray<KBKeyboardRowConfig *> *rows = layout.rows ?: @[];
CGFloat rowSpacing = [self kb_metricValue:config.metrics.rowSpacing fallback:nil defaultValue:8.0];
CGFloat topInset = [self kb_metricValue:config.metrics.topInset fallback:nil defaultValue:8.0];
CGFloat bottomInset = [self kb_metricValue:config.metrics.bottomInset fallback:nil defaultValue:6.0];
CGFloat row1Height = [self kb_rowHeightForRow:(rows.count > 0 ? rows[0] : nil)];
CGFloat row2Height = [self kb_rowHeightForRow:(rows.count > 1 ? rows[1] : nil)];
CGFloat row3Height = [self kb_rowHeightForRow:(rows.count > 2 ? rows[2] : nil)];
CGFloat row4Height = [self kb_rowHeightForRow:(rows.count > 3 ? rows[3] : nil)];
2025-10-28 10:18:10 +08:00
[self.row1 mas_makeConstraints:^(MASConstraintMaker *make) {
2026-01-08 16:54:38 +08:00
make.top.equalTo(self.mas_top).offset(topInset);
2025-10-28 10:18:10 +08:00
make.left.right.equalTo(self);
2026-01-08 16:54:38 +08:00
make.height.mas_equalTo(row1Height);
2025-10-28 10:18:10 +08:00
}];
[self.row2 mas_makeConstraints:^(MASConstraintMaker *make) {
2026-01-08 16:54:38 +08:00
make.top.equalTo(self.row1.mas_bottom).offset(rowSpacing);
2025-10-28 10:18:10 +08:00
make.left.right.equalTo(self);
2026-01-08 16:54:38 +08:00
make.height.mas_equalTo(row2Height);
2025-10-28 10:18:10 +08:00
}];
[self.row3 mas_makeConstraints:^(MASConstraintMaker *make) {
2026-01-08 16:54:38 +08:00
make.top.equalTo(self.row2.mas_bottom).offset(rowSpacing);
2025-10-28 10:18:10 +08:00
make.left.right.equalTo(self);
2026-01-08 16:54:38 +08:00
make.height.mas_equalTo(row3Height);
2025-10-28 10:18:10 +08:00
}];
[self.row4 mas_makeConstraints:^(MASConstraintMaker *make) {
2026-01-08 16:54:38 +08:00
make.top.equalTo(self.row3.mas_bottom).offset(rowSpacing);
2025-10-28 10:18:10 +08:00
make.left.right.equalTo(self);
2026-01-08 16:54:38 +08:00
make.height.mas_equalTo(row4Height);
make.bottom.equalTo(self.mas_bottom).offset(-bottomInset);
2025-10-28 10:18:10 +08:00
}];
}
#pragma mark - Public
2025-10-28 10:18:10 +08:00
- (void)reloadKeys {
[self.backspaceHandler bindDeleteButton:nil showClearLabel:NO];
2025-10-28 10:18:10 +08:00
//
for (UIView *row in @[self.row1, self.row2, self.row3, self.row4]) {
[row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
}
2026-01-08 16:54:38 +08:00
KBKeyboardLayout *layout = [self kb_currentLayout];
NSArray<KBKeyboardRowConfig *> *rows = layout.rows ?: @[];
if (rows.count < 4) {
[self kb_buildLegacyLayout];
return;
}
2026-01-08 16:54:38 +08:00
[self buildRow:self.row1 withRowConfig:rows[0]];
[self buildRow:self.row2 withRowConfig:rows[1]];
[self buildRow:self.row3 withRowConfig:rows[2]];
[self buildRow:self.row4 withRowConfig:rows[3]];
2025-10-28 10:18:10 +08:00
}
2026-01-08 20:04:23 +08:00
#pragma mark - Hit Test
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hit = [super hitTest:point withEvent:event];
if ([hit isKindOfClass:[KBKeyButton class]]) {
return hit;
}
if ([self kb_isHitInsideKeyRows:hit]) {
2026-01-08 20:04:23 +08:00
KBKeyButton *btn = [self kb_nearestKeyButtonForPoint:point];
if (btn) { return btn; }
}
return hit;
}
- (BOOL)kb_isHitInsideKeyRows:(UIView *)hitView {
if (!hitView) { return NO; }
if (hitView == self) { return YES; }
if ([hitView isDescendantOfView:self.row1]) { return YES; }
if ([hitView isDescendantOfView:self.row2]) { return YES; }
if ([hitView isDescendantOfView:self.row3]) { return YES; }
if ([hitView isDescendantOfView:self.row4]) { return YES; }
return NO;
}
2026-01-08 20:04:23 +08:00
- (KBKeyButton *)kb_nearestKeyButtonForPoint:(CGPoint)point {
KBKeyButton *best = nil;
CGFloat bestDistance = CGFLOAT_MAX;
NSArray<UIView *> *rows = @[self.row1, self.row2, self.row3, self.row4];
UIView *targetRow = nil;
2026-01-08 20:04:23 +08:00
for (UIView *row in rows) {
CGRect rowFrame = [self convertRect:row.bounds fromView:row];
if (CGRectContainsPoint(rowFrame, point)) {
targetRow = row;
break;
}
}
NSArray<UIView *> *candidateRows = targetRow ? @[targetRow] : rows;
for (UIView *row in candidateRows) {
NSArray<KBKeyButton *> *buttons = [self kb_collectKeyButtonsInView:row];
for (KBKeyButton *btn in buttons) {
CGRect frame = [self convertRect:btn.frame fromView:btn.superview];
2026-01-08 20:04:23 +08:00
CGFloat dx = point.x - CGRectGetMidX(frame);
CGFloat dy = point.y - CGRectGetMidY(frame);
CGFloat dist = (dx * dx) + (dy * dy);
if (dist < bestDistance) {
bestDistance = dist;
best = btn;
}
}
}
return best;
}
- (NSArray<KBKeyButton *> *)kb_collectKeyButtonsInView:(UIView *)view {
if (!view) { return @[]; }
NSMutableArray<KBKeyButton *> *buttons = [NSMutableArray array];
[self kb_collectKeyButtonsInView:view into:buttons];
return buttons.copy;
}
- (void)kb_collectKeyButtonsInView:(UIView *)view
into:(NSMutableArray<KBKeyButton *> *)buttons {
for (UIView *sub in view.subviews) {
if ([sub isKindOfClass:[KBKeyButton class]]) {
[buttons addObject:(KBKeyButton *)sub];
continue;
}
if (sub.subviews.count > 0) {
[self kb_collectKeyButtonsInView:sub into:buttons];
}
}
}
#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_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<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];
2025-12-15 13:24:43 +08:00
KBKey *emoji = [KBKey keyWithIdentifier:KBKeyIdentifierEmojiPanel
title:@"😊"
output:@""
type:KBKeyTypeCustom];
2025-11-18 20:53:47 +08:00
KBKey *space = [KBKey keyWithIdentifier:@"space"
title:@"space"
output:@" "
type:KBKeyTypeSpace];
KBKey *ret = [KBKey keyWithIdentifier:@"return"
title:KBLocalized(@"Send")
output:@"\n"
type:KBKeyTypeReturn];
2025-12-19 20:08:13 +08:00
return @[ mode123, emoji, 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];
2025-12-15 13:24:43 +08:00
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];
2025-12-19 20:08:13 +08:00
return @[ modeABC, emoji, space, ret ];
2025-10-28 10:18:10 +08:00
}
#pragma mark - Row Building
2026-01-08 16:54:38 +08:00
- (void)buildRow:(UIView *)row withRowConfig:(KBKeyboardRowConfig *)rowConfig {
if (!row || !rowConfig) { return; }
CGFloat gap = [self kb_gapForRow:rowConfig];
CGFloat insetLeft = [self kb_insetLeftForRow:rowConfig];
CGFloat insetRight = [self kb_insetRightForRow:rowConfig];
if (rowConfig.segments) {
KBKeyboardRowSegments *segments = rowConfig.segments;
NSArray<KBKeyboardRowItem *> *leftItems = [segments leftItems];
NSArray<KBKeyboardRowItem *> *centerItems = [segments centerItems];
NSArray<KBKeyboardRowItem *> *rightItems = [segments rightItems];
UIView *leftContainer = [UIView new];
UIView *centerContainer = [UIView new];
UIView *rightContainer = [UIView new];
[row addSubview:leftContainer];
[row addSubview:centerContainer];
[row addSubview:rightContainer];
[leftContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(row.mas_left).offset(insetLeft);
make.top.bottom.equalTo(row);
}];
[rightContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(row.mas_right).offset(-insetRight);
make.top.bottom.equalTo(row);
}];
[centerContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(row);
make.top.bottom.equalTo(row);
make.left.greaterThanOrEqualTo(leftContainer.mas_right).offset(gap);
make.right.lessThanOrEqualTo(rightContainer.mas_left).offset(-gap);
}];
if (leftItems.count == 0) {
[leftContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(0);
}];
}
if (centerItems.count == 0) {
[centerContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(0);
}];
}
if (rightItems.count == 0) {
[rightContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(0);
}];
}
[self kb_buildButtonsInContainer:leftContainer
items:leftItems
gap:gap
insetLeft:0
insetRight:0
alignCenter:NO];
[self kb_buildButtonsInContainer:centerContainer
items:centerItems
gap:gap
insetLeft:0
insetRight:0
alignCenter:NO];
[self kb_buildButtonsInContainer:rightContainer
items:rightItems
gap:gap
insetLeft:0
insetRight:0
alignCenter:NO];
return;
}
BOOL alignCenter = [rowConfig.align.lowercaseString isEqualToString:@"center"];
[self kb_buildButtonsInContainer:row
items:[rowConfig resolvedItems]
gap:gap
insetLeft:insetLeft
insetRight:insetRight
alignCenter:alignCenter];
}
- (void)kb_buildButtonsInContainer:(UIView *)container
items:(NSArray<KBKeyboardRowItem *> *)items
gap:(CGFloat)gap
insetLeft:(CGFloat)insetLeft
insetRight:(CGFloat)insetRight
alignCenter:(BOOL)alignCenter {
if (items.count == 0) { return; }
UIView *leftSpacer = nil;
UIView *rightSpacer = nil;
if (alignCenter) {
leftSpacer = [UIView new];
rightSpacer = [UIView new];
[container addSubview:leftSpacer];
[container addSubview:rightSpacer];
[leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(container.mas_left).offset(insetLeft);
make.top.bottom.equalTo(container);
}];
[rightSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(container.mas_right).offset(-insetRight);
make.top.bottom.equalTo(container);
}];
[leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(rightSpacer);
}];
}
KBKeyButton *previous = nil;
for (KBKeyboardRowItem *item in items) {
KBKeyButton *btn = [self kb_buttonForItem:item];
if (!btn) { continue; }
[container addSubview:btn];
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.bottom.equalTo(container);
if (previous) {
make.left.equalTo(previous.mas_right).offset(gap);
} else {
if (leftSpacer) {
make.left.equalTo(leftSpacer.mas_right).offset(gap);
} else {
make.left.equalTo(container.mas_left).offset(insetLeft);
}
}
}];
CGFloat width = [self kb_widthForItem:item key:btn.key];
if (width > 0.0) {
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(width);
}];
}
previous = btn;
}
if (!previous) { return; }
[previous mas_makeConstraints:^(MASConstraintMaker *make) {
if (rightSpacer) {
make.right.equalTo(rightSpacer.mas_left).offset(-gap);
} else {
make.right.equalTo(container.mas_right).offset(-insetRight);
}
}];
}
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 使
2025-12-19 20:08:13 +08:00
// 123/ABCEmojiSend Space
2025-11-21 13:48:22 +08:00
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];
2025-11-04 16:37:24 +08:00
if (key.type == KBKeyTypeBackspace) {
[self.backspaceHandler bindDeleteButton:btn showClearLabel:YES];
2025-11-04 16:37:24 +08:00
}
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-12-19 20:08:13 +08:00
// 123/ABCEmojiSend
2025-11-21 13:48:22 +08:00
// 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-12-19 20:08:13 +08:00
// 123/ABC/#+=
2025-11-20 20:17:19 +08:00
// 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-11-21 20:59:39 +08:00
//
//
//
2025-10-28 10:18:10 +08:00
if (leftSpacer && rightSpacer) {
2025-11-21 20:59:39 +08:00
// 1)
2025-10-28 10:18:10 +08:00
[leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
2025-11-21 20:59:39 +08:00
make.width.equalTo(rightSpacer);
2025-10-28 10:18:10 +08:00
}];
2025-11-21 20:59:39 +08:00
// 2) edgeSpacerMultiplier
2025-10-28 10:18:10 +08:00
[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;
}
//
2025-12-19 20:08:13 +08:00
// - 123/ABCEmoji = * multiplier
// - Send = 2
2025-11-21 13:48:22 +08:00
// - Space
- (void)kb_applyBottomControlRowWidthInRow:(UIView *)row {
KBKeyButton *modeBtn = nil;
2025-12-15 13:32:47 +08:00
NSMutableArray<KBKeyButton *> *customButtons = [NSMutableArray array];
2025-11-21 13:48:22 +08:00
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:
2025-12-15 13:32:47 +08:00
[customButtons addObject:b];
2025-11-21 13:48:22 +08:00
break;
case KBKeyTypeSpace:
spaceBtn = b;
break;
case KBKeyTypeReturn:
retBtn = b;
break;
default:
break;
}
}
2025-12-15 13:32:47 +08:00
if (!modeBtn || customButtons.count == 0 || !spaceBtn || !retBtn) {
2025-11-21 13:48:22 +08:00
return;
}
[modeBtn mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(row.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier);
2025-11-21 13:48:22 +08:00
}];
2025-12-15 13:32:47 +08:00
for (KBKeyButton *custom in customButtons) {
[custom 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) {
make.width.equalTo(modeBtn.mas_width).multipliedBy(2.0);
2025-11-21 13:48:22 +08:00
}];
// Space
}
2026-01-08 16:54:38 +08:00
#pragma mark - Config Helpers
- (KBKeyboardLayoutConfig *)kb_layoutConfig {
if (!self.layoutConfig) {
self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig];
}
return self.layoutConfig;
}
- (KBKeyboardLayout *)kb_layoutForName:(NSString *)name {
return [[self kb_layoutConfig] layoutForName:name];
}
- (KBKeyboardLayout *)kb_currentLayout {
if (self.layoutStyle == KBKeyboardLayoutStyleNumbers) {
return [self kb_layoutForName:(self.symbolsMoreOn ? @"symbolsMore" : @"numbers")];
}
return [self kb_layoutForName:@"letters"];
}
- (void)kb_buildLegacyLayout {
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]];
}
- (CGFloat)kb_scaledValue:(CGFloat)designValue {
KBKeyboardLayoutConfig *config = [self kb_layoutConfig];
if (config) {
return [config scaledValue:designValue];
}
return KBFit(designValue);
}
- (CGFloat)kb_numberValue:(NSNumber *)value defaultValue:(CGFloat)defaultValue {
if ([value isKindOfClass:[NSNumber class]]) {
return value.doubleValue;
}
return defaultValue;
}
- (CGFloat)kb_metricValue:(NSNumber *)value fallback:(NSNumber *)fallback defaultValue:(CGFloat)defaultValue {
CGFloat v = [self kb_numberValue:value defaultValue:-1.0];
if (v < 0.0) {
v = [self kb_numberValue:fallback defaultValue:defaultValue];
}
if (v < 0.0) {
v = defaultValue;
}
return [self kb_scaledValue:v];
}
- (CGFloat)kb_rowHeightForRow:(KBKeyboardRowConfig *)row {
KBKeyboardLayoutConfig *config = [self kb_layoutConfig];
NSNumber *height = row.height ?: config.metrics.keyHeight;
CGFloat value = [self kb_numberValue:height defaultValue:40.0];
return [self kb_scaledValue:value];
}
- (CGFloat)kb_gapForRow:(KBKeyboardRowConfig *)row {
KBKeyboardLayoutConfig *config = [self kb_layoutConfig];
return [self kb_metricValue:row.gap fallback:config.metrics.gap defaultValue:5.0];
}
- (CGFloat)kb_insetLeftForRow:(KBKeyboardRowConfig *)row {
KBKeyboardLayoutConfig *config = [self kb_layoutConfig];
return [self kb_metricValue:row.insetLeft fallback:config.metrics.edgeInset defaultValue:0.0];
}
- (CGFloat)kb_insetRightForRow:(KBKeyboardRowConfig *)row {
KBKeyboardLayoutConfig *config = [self kb_layoutConfig];
return [self kb_metricValue:row.insetRight fallback:config.metrics.edgeInset defaultValue:0.0];
}
- (KBKeyButton *)kb_buttonForItem:(KBKeyboardRowItem *)item {
if (item.itemId.length == 0) { return nil; }
KBKeyboardKeyDef *def = [[self kb_layoutConfig] keyDefForIdentifier:item.itemId];
KBKey *key = [self kb_keyForItemId:item.itemId];
if (!key) { return nil; }
KBKeyButton *btn = [[KBKeyButton alloc] init];
btn.key = key;
[btn setTitle:key.title forState:UIControlStateNormal];
UIColor *bgColor = [self kb_backgroundColorForItem:item keyDef:def];
if (bgColor) {
btn.customBackgroundColor = bgColor;
}
CGFloat fontSize = [self kb_fontSizeForItem:item key:key];
if (fontSize > 0.0) {
btn.titleLabel.font = [UIFont systemFontOfSize:fontSize weight:UIFontWeightSemibold];
}
[btn applyThemeForCurrentKey];
[btn addTarget:self action:@selector(onKeyTapped:) forControlEvents:UIControlEventTouchUpInside];
if (key.type == KBKeyTypeBackspace) {
[self.backspaceHandler bindDeleteButton:btn showClearLabel:YES];
}
if (key.type == KBKeyTypeShift) {
btn.selected = self.shiftOn;
}
2026-01-08 18:57:17 +08:00
[self kb_applySymbolIfNeededForButton:btn keyDef:def fontSize:fontSize];
2026-01-08 16:54:38 +08:00
return btn;
}
- (void)kb_applySymbolIfNeededForButton:(KBKeyButton *)button
keyDef:(KBKeyboardKeyDef *)def
fontSize:(CGFloat)fontSize {
if (!button || !def) { return; }
if (button.iconView.image != nil) { return; }
NSString *symbolName = button.isSelected ? def.selectedSymbolName : def.symbolName;
if (symbolName.length == 0) { return; }
UIImage *image = [UIImage systemImageNamed:symbolName];
if (!image) { return; }
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:fontSize weight:UIFontWeightSemibold];
image = [image imageWithConfiguration:config];
button.iconView.image = image;
button.iconView.hidden = NO;
button.iconView.contentMode = UIViewContentModeCenter;
button.titleLabel.hidden = YES;
UIColor *textColor = [KBSkinManager shared].current.keyTextColor ?: [UIColor blackColor];
button.iconView.tintColor = button.isSelected ? [UIColor blackColor] : textColor;
}
- (UIColor *)kb_backgroundColorForItem:(KBKeyboardRowItem *)item keyDef:(KBKeyboardKeyDef *)def {
NSString *hex = def.backgroundColor;
if (hex.length == 0) {
hex = [self kb_layoutConfig].defaultKeyBackground;
}
if (hex.length == 0) { return nil; }
return [KBSkinManager colorFromHexString:hex defaultColor:nil];
}
- (CGFloat)kb_metricWidthForKey:(NSString *)key {
KBKeyboardLayoutMetrics *m = [self kb_layoutConfig].metrics;
if ([key isEqualToString:@"letterWidth"]) { return m.letterWidth.doubleValue; }
if ([key isEqualToString:@"controlWidth"]) { return m.controlWidth.doubleValue; }
if ([key isEqualToString:@"sendWidth"]) { return m.sendWidth.doubleValue; }
if ([key isEqualToString:@"symbolsWideWidth"]) { return m.symbolsWideWidth.doubleValue; }
if ([key isEqualToString:@"symbolsSideWidth"]) { return m.symbolsSideWidth.doubleValue; }
return 0.0;
}
- (CGFloat)kb_widthForItem:(KBKeyboardRowItem *)item key:(KBKey *)key {
CGFloat width = 0.0;
if (item.widthValue.doubleValue > 0.0) {
width = item.widthValue.doubleValue;
} else if (item.width.length > 0) {
if ([item.width.lowercaseString isEqualToString:@"flex"]) {
return 0.0;
}
width = [self kb_metricWidthForKey:item.width];
if (width <= 0.0) {
width = item.width.doubleValue;
}
}
if (width <= 0.0) {
KBKeyboardLayoutMetrics *m = [self kb_layoutConfig].metrics;
if ([item.itemId hasPrefix:@"letter:"] ||
[item.itemId hasPrefix:@"digit:"] ||
[item.itemId hasPrefix:@"sym:"]) {
width = m.letterWidth.doubleValue;
} else if (key.type == KBKeyTypeReturn) {
width = m.sendWidth.doubleValue;
} else if (key.type == KBKeyTypeSpace) {
return 0.0;
} else {
width = m.controlWidth.doubleValue;
}
}
if (width <= 0.0) {
if ([item.itemId hasPrefix:@"letter:"] ||
[item.itemId hasPrefix:@"digit:"] ||
[item.itemId hasPrefix:@"sym:"]) {
width = 32.0;
} else if (key.type == KBKeyTypeReturn) {
width = 88.0;
} else if (key.type == KBKeyTypeSpace) {
return 0.0;
} else {
width = 41.0;
}
}
return width > 0.0 ? [self kb_scaledValue:width] : 0.0;
}
- (CGFloat)kb_fontSizeForItem:(KBKeyboardRowItem *)item key:(KBKey *)key {
NSString *fontKey = nil;
if ([item.itemId hasPrefix:@"letter:"]) {
fontKey = @"letter";
} else if ([item.itemId hasPrefix:@"digit:"]) {
fontKey = @"digit";
} else if ([item.itemId hasPrefix:@"sym:"]) {
fontKey = @"symbol";
} else {
KBKeyboardKeyDef *def = [[self kb_layoutConfig] keyDefForIdentifier:item.itemId];
fontKey = def.font;
}
if (fontKey.length == 0) {
switch (key.type) {
case KBKeyTypeModeChange:
case KBKeyTypeSymbolsToggle:
fontKey = @"mode";
break;
case KBKeyTypeSpace:
fontKey = @"space";
break;
case KBKeyTypeReturn:
fontKey = @"send";
break;
default:
fontKey = @"symbol";
break;
}
}
return [self kb_fontSizeForFontKey:fontKey];
}
- (CGFloat)kb_fontSizeForFontKey:(NSString *)fontKey {
KBKeyboardLayoutFonts *fonts = [self kb_layoutConfig].fonts;
CGFloat size = 0.0;
if ([fontKey isEqualToString:@"letter"]) { size = fonts.letter.doubleValue; }
else if ([fontKey isEqualToString:@"digit"]) { size = fonts.digit.doubleValue; }
else if ([fontKey isEqualToString:@"symbol"]) { size = fonts.symbol.doubleValue; }
else if ([fontKey isEqualToString:@"mode"]) { size = fonts.mode.doubleValue; }
else if ([fontKey isEqualToString:@"space"]) { size = fonts.space.doubleValue; }
else if ([fontKey isEqualToString:@"send"]) { size = fonts.send.doubleValue; }
if (size <= 0.0) { size = 18.0; }
return [self kb_scaledValue:size];
}
- (KBKey *)kb_keyForItemId:(NSString *)itemId {
if (itemId.length == 0) { return nil; }
KBKeyboardKeyDef *def = [[self kb_layoutConfig] keyDefForIdentifier:itemId];
if (def) {
return [self kb_keyFromDef:def identifier:itemId];
}
NSRange range = [itemId rangeOfString:@":"];
if (range.location != NSNotFound) {
NSString *prefix = [itemId substringToIndex:range.location];
NSString *value = [itemId substringFromIndex:range.location + 1];
if ([prefix isEqualToString:@"letter"]) {
if (value.length == 1) {
return [self kb_letterKeyWithChar:value];
}
return nil;
}
if ([prefix isEqualToString:@"digit"]) {
NSString *identifier = [NSString stringWithFormat:@"digit_%@", value];
KBKey *k = [KBKey keyWithIdentifier:identifier title:value output:value type:KBKeyTypeCharacter];
k.caseVariant = KBKeyCaseVariantNone;
return k;
}
if ([prefix isEqualToString:@"sym"]) {
NSString *identifier = [self kb_identifierForSymbol:value];
KBKey *k = [KBKey keyWithIdentifier:identifier title:value output:value type:KBKeyTypeCharacter];
k.caseVariant = KBKeyCaseVariantNone;
return k;
}
}
return nil;
}
- (KBKey *)kb_keyFromDef:(KBKeyboardKeyDef *)def identifier:(NSString *)identifier {
KBKeyType type = [self kb_keyTypeForDef:def];
NSString *title = def.title ?: @"";
if (type == KBKeyTypeShift && self.shiftOn && def.selectedTitle.length > 0) {
title = def.selectedTitle;
}
NSString *output = @"";
switch (type) {
case KBKeyTypeSpace:
output = @" ";
break;
case KBKeyTypeReturn:
output = @"\n";
break;
default:
output = @"";
break;
}
NSString *finalId = identifier;
if ([identifier isEqualToString:@"emoji"]) {
finalId = KBKeyIdentifierEmojiPanel;
} else if ([identifier isEqualToString:@"send"]) {
finalId = @"return";
}
KBKey *k = [KBKey keyWithIdentifier:finalId title:title output:output type:type];
2026-01-08 18:57:17 +08:00
if (type == KBKeyTypeShift) {
k.caseVariant = self.shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower;
} else {
k.caseVariant = KBKeyCaseVariantNone;
}
2026-01-08 16:54:38 +08:00
return k;
}
- (KBKeyType)kb_keyTypeForDef:(KBKeyboardKeyDef *)def {
NSString *type = def.type.lowercaseString;
if ([type isEqualToString:@"shift"]) return KBKeyTypeShift;
if ([type isEqualToString:@"backspace"]) return KBKeyTypeBackspace;
if ([type isEqualToString:@"mode"]) return KBKeyTypeModeChange;
if ([type isEqualToString:@"symbolstoggle"]) return KBKeyTypeSymbolsToggle;
if ([type isEqualToString:@"space"]) return KBKeyTypeSpace;
if ([type isEqualToString:@"return"]) return KBKeyTypeReturn;
if ([type isEqualToString:@"globe"]) return KBKeyTypeGlobe;
return KBKeyTypeCustom;
}
- (NSString *)kb_identifierForSymbol:(NSString *)symbol {
if (symbol.length == 0) { return nil; }
static NSDictionary<NSString *, NSString *> *map = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
map = @{
@"-": @"sym_minus",
@"/": @"sym_slash",
@":": @"sym_colon",
@";": @"sym_semicolon",
@"(": @"sym_paren_l",
@")": @"sym_paren_r",
@"¥": @"sym_money",
@"¥": @"sym_money",
@"&": @"sym_amp",
@"@": @"sym_at",
@"\"": @"sym_quote_double",
@"“": @"sym_quote_double",
@"”": @"sym_quote_double",
@".": @"sym_dot",
@",": @"sym_comma",
@"?": @"sym_question",
@"!": @"sym_exclam",
@"'": @"sym_quote_single",
@"": @"sym_quote_single",
@"": @"sym_quote_single",
@"[": @"sym_bracket_l",
@"]": @"sym_bracket_r",
@"{": @"sym_brace_l",
@"}": @"sym_brace_r",
@"#": @"sym_hash",
@"%": @"sym_percent",
@"^": @"sym_caret",
@"*": @"sym_asterisk",
@"+": @"sym_plus",
@"=": @"sym_equal",
@"_": @"sym_underscore",
@"\\": @"sym_backslash",
@"|": @"sym_pipe",
@"~": @"sym_tilde",
@"<": @"sym_lt",
@">": @"sym_gt",
@"€": @"sym_euro",
@"$": @"sym_dollar",
@"·": @"sym_bullet"
};
});
return map[symbol];
}
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-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