Files
keyboard/CustomKeyboard/View/KBKeyboardView.m
2026-01-09 19:13:35 +08:00

1277 lines
51 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 "KBSkinManager.h"
#import "KBKeyPreviewView.h"
#import "KBBackspaceLongPressHandler.h"
#import "KBKeyboardLayoutConfig.h"
// 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;
@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;
@property (nonatomic, strong) KBBackspaceLongPressHandler *backspaceHandler;
@property (nonatomic, strong) KBKeyPreviewView *previewView;
@property (nonatomic, strong) KBKeyboardLayoutConfig *layoutConfig;
@end
@implementation KBKeyboardView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.backgroundColor = [UIColor clearColor];
_layoutStyle = KBKeyboardLayoutStyleLetters;
// 默认小写:与需求一致,初始不开启 Shift
_shiftOn = NO;
_symbolsMoreOn = NO; // 数字面板默认第一页123
self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig];
self.backspaceHandler = [[KBBackspaceLongPressHandler alloc] initWithContainerView:self];
[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];
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)];
[self.row1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.mas_top).offset(topInset);
make.left.right.equalTo(self);
make.height.mas_equalTo(row1Height);
}];
[self.row2 mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.row1.mas_bottom).offset(rowSpacing);
make.left.right.equalTo(self);
make.height.mas_equalTo(row2Height);
}];
[self.row3 mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.row2.mas_bottom).offset(rowSpacing);
make.left.right.equalTo(self);
make.height.mas_equalTo(row3Height);
}];
[self.row4 mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.row3.mas_bottom).offset(rowSpacing);
make.left.right.equalTo(self);
make.height.mas_equalTo(row4Height);
make.bottom.equalTo(self.mas_bottom).offset(-bottomInset);
}];
}
#pragma mark - Public
- (void)reloadKeys {
[self.backspaceHandler bindDeleteButton:nil showClearLabel:NO];
// 移除旧按钮
for (UIView *row in @[self.row1, self.row2, self.row3, self.row4]) {
[row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
}
KBKeyboardLayout *layout = [self kb_currentLayout];
NSArray<KBKeyboardRowConfig *> *rows = layout.rows ?: @[];
if (rows.count < 4) {
[self kb_buildLegacyLayout];
return;
}
[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]];
}
#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]) {
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;
}
- (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;
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];
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 列表
- (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_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 {
KBKey *mode123 = [KBKey keyWithIdentifier:@"mode_123"
title:@"123"
output:@""
type:KBKeyTypeModeChange];
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];
return @[ mode123, emoji, space, ret ];
}
// 底部控制行(数字布局)
- (NSArray<KBKey *> *)kb_bottomControlRowKeysForNumbersLayout {
KBKey *modeABC = [KBKey keyWithIdentifier:@"mode_abc"
title:@"abc"
output:@""
type:KBKeyTypeModeChange];
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];
return @[ modeABC, emoji, space, ret ];
}
#pragma mark - Row Building
- (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);
}
}];
}
- (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、Emoji、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];
if (key.type == KBKeyTypeBackspace) {
[self.backspaceHandler bindDeleteButton:btn showClearLabel:YES];
}
// 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、Emoji、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/#+= 等,
// 也将其约束为 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、Emoji正方形宽 = 行高 * multiplier
// - Send宽 = 模式键宽度的 2 倍
// - Space不加宽度约束依靠左右约束自动填充剩余空间。
- (void)kb_applyBottomControlRowWidthInRow:(UIView *)row {
KBKeyButton *modeBtn = nil;
NSMutableArray<KBKeyButton *> *customButtons = [NSMutableArray array];
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:
[customButtons addObject:b];
break;
case KBKeyTypeSpace:
spaceBtn = b;
break;
case KBKeyTypeReturn:
retBtn = b;
break;
default:
break;
}
}
if (!modeBtn || customButtons.count == 0 || !spaceBtn || !retBtn) {
return;
}
[modeBtn mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(row.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier);
}];
for (KBKeyButton *custom in customButtons) {
[custom mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(row.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier);
}];
}
[retBtn mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(modeBtn.mas_width).multipliedBy(2.0);
}];
// Space 不设置宽度;通过此前已建立的左右约束自动占满剩余宽度。
}
#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;
}
[self kb_applySymbolIfNeededForButton:btn keyDef:def fontSize:fontSize];
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];
if (type == KBKeyTypeShift) {
k.caseVariant = self.shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower;
} else {
k.caseVariant = KBKeyCaseVariantNone;
}
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];
}
#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;
}];
}
#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