Files
keyboard/CustomKeyboard/View/KBKeyboardView.m
2026-03-03 21:31:03 +08:00

1500 lines
59 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"
#import "KBKeyboardLayoutResolver.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) NSMutableArray<UIView *> *rowViews;
@property (nonatomic, strong) NSArray<NSArray<KBKey *> *> *keysForRows;
@property (nonatomic, strong) KBBackspaceLongPressHandler *backspaceHandler;
@property (nonatomic, strong) KBKeyPreviewView *previewView;
@property (nonatomic, strong) KBKeyboardLayoutConfig *layoutConfig;
/// 跨行统一字符键宽度按最多字符键的行计算0 表示不启用
@property (nonatomic, assign) CGFloat kb_uniformCharKeyWidth;
@end
@implementation KBKeyboardView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.backgroundColor = [UIColor clearColor];
_layoutStyle = KBKeyboardLayoutStyleLetters;
// 默认小写:与需求一致,初始不开启 Shift
_shiftOn = NO;
_symbolsMoreOn = NO; // 数字面板默认第一页123
// 从 App Group 读取当前 profileId 并设置布局
NSString *profileId = [[KBKeyboardLayoutResolver sharedResolver] currentProfileId];
if (profileId.length > 0) {
_currentLayoutJsonId = [[KBKeyboardLayoutResolver sharedResolver] layoutJsonIdForProfileId:profileId];
NSLog(@"[KBKeyboardView] Loaded profileId: %@, layoutJsonId: %@", profileId, _currentLayoutJsonId);
} else {
_currentLayoutJsonId = @"letters";
NSLog(@"[KBKeyboardView] No profileId found, using default 'letters'");
}
self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig];
self.backspaceHandler = [[KBBackspaceLongPressHandler alloc] initWithContainerView:self];
[self buildBase];
}
return self;
}
// 当切换大布局(字母/数字)时,重置数字二级页状态
- (void)setLayoutStyle:(KBKeyboardLayoutStyle)layoutStyle {
_layoutStyle = layoutStyle;
if (_layoutStyle != KBKeyboardLayoutStyleNumbers) {
_symbolsMoreOn = NO;
}
}
#pragma mark - Base Layout
- (void)buildBase {
KBKeyboardLayout *layout = [self kb_currentLayout];
NSArray<KBKeyboardRowConfig *> *rows = layout.rows ?: @[];
if (rows.count == 0) {
// Fallback: 至少创建 4 行容器
rows = @[[KBKeyboardRowConfig new], [KBKeyboardRowConfig new],
[KBKeyboardRowConfig new], [KBKeyboardRowConfig new]];
}
[self kb_rebuildRowContainersForRows:rows];
}
/// 根据行配置数组,动态创建/重建行容器(支持 4 行、5 行等任意行数)
- (void)kb_rebuildRowContainersForRows:(NSArray<KBKeyboardRowConfig *> *)rowConfigs {
// 移除旧的行容器
for (UIView *row in self.rowViews) {
[row removeFromSuperview];
}
[self.rowViews removeAllObjects];
NSUInteger rowCount = rowConfigs.count;
if (rowCount == 0) return;
KBKeyboardLayoutConfig *config = [self kb_layoutConfig];
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];
UIView *firstRow = nil;
UIView *previousRow = nil;
for (NSUInteger i = 0; i < rowCount; i++) {
UIView *rowView = [UIView new];
[self addSubview:rowView];
[self.rowViews addObject:rowView];
[rowView mas_makeConstraints:^(MASConstraintMaker *make) {
if (previousRow) {
make.top.equalTo(previousRow.mas_bottom).offset(rowSpacing);
} else {
make.top.equalTo(self.mas_top).offset(topInset);
}
make.left.right.equalTo(self);
// 所有行等高,自动根据可用空间分配行高
if (firstRow) {
make.height.equalTo(firstRow);
}
}];
// 最后一行锚定到底部
if (i == rowCount - 1) {
[rowView mas_makeConstraints:^(MASConstraintMaker *make) {
make.bottom.equalTo(self.mas_bottom).offset(-bottomInset);
}];
}
if (!firstRow) firstRow = rowView;
previousRow = rowView;
}
// 确保预览气泡视图在最上层
if (self.previewView && self.previewView.superview == self) {
[self bringSubviewToFront:self.previewView];
}
}
#pragma mark - Public
- (void)reloadKeys {
[self.backspaceHandler bindDeleteButton:nil showClearLabel:NO];
KBKeyboardLayout *layout = [self kb_currentLayout];
NSLog(@"[KBKeyboardView] reloadKeys: layoutName=%@ rows=%lu shiftRows=%lu shiftOn=%d",
self.currentLayoutJsonId, (unsigned long)layout.rows.count, (unsigned long)layout.shiftRows.count, self.shiftOn);
NSArray<KBKeyboardRowConfig *> *rows = nil;
if (self.shiftOn && layout.shiftRows.count > 0) {
rows = layout.shiftRows;
} else {
rows = layout.rows ?: @[];
}
NSLog(@"[KBKeyboardView] reloadKeys: usingRows=%lu currentContainers=%lu",
(unsigned long)rows.count, (unsigned long)self.rowViews.count);
// 行数变化时(如从 4 行布局切到 5 行注音布局),重建行容器
if (rows.count >= 4 && rows.count != self.rowViews.count) {
[self kb_rebuildRowContainersForRows:rows];
}
// 移除旧按钮
for (UIView *row in self.rowViews) {
[row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
}
if (rows.count < 4) {
NSLog(@"[KBKeyboardView] reloadKeys: rows.count < 4, fallback to legacy");
self.kb_uniformCharKeyWidth = 0.0;
[self kb_buildLegacyLayout];
return;
}
// 计算跨行统一字符键宽度(若各行字符键数量不同,则按最多键的行为基准)
self.kb_uniformCharKeyWidth = [self kb_calculateUniformCharKeyWidthForRows:rows];
for (NSUInteger i = 0; i < rows.count && i < self.rowViews.count; i++) {
[self buildRow:self.rowViews[i] withRowConfig:rows[i]];
}
NSUInteger totalButtons = [self kb_totalKeyButtonCount];
NSLog(@"[KBKeyboardView] reloadKeys: totalButtons=%lu", (unsigned long)totalButtons);
if (totalButtons == 0) {
NSLog(@"[KBKeyboardView] config layout produced no keys, fallback to legacy.");
for (UIView *row in self.rowViews) {
[row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
}
[self kb_buildLegacyLayout];
}
}
- (void)didMoveToWindow {
[super didMoveToWindow];
if (!self.window) { return; }
if ([self kb_totalKeyButtonCount] > 0) { return; }
// 兜底:系统编辑菜单切出切回等场景下,若按键丢失则自动重建。
[self reloadKeys];
// 自动重建后再触发一次上层主题应用,避免“按键恢复了但皮肤背景没恢复”。
UIView *container = self.superview;
if ([container respondsToSelector:@selector(kb_applyTheme)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[container performSelector:@selector(kb_applyTheme)];
#pragma clang diagnostic pop
}
}
- (NSUInteger)kb_totalKeyButtonCount {
NSUInteger total = 0;
for (UIView *row in self.rowViews) {
total += [self kb_collectKeyButtonsInView:row].count;
}
return total;
}
#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; }
for (UIView *row in self.rowViews) {
if ([hitView isDescendantOfView:row]) { return YES; }
}
return NO;
}
- (KBKeyButton *)kb_nearestKeyButtonForPoint:(CGPoint)point {
KBKeyButton *best = nil;
CGFloat bestDistance = CGFLOAT_MAX;
NSArray<UIView *> *rows = self.rowViews;
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 {
if (charString.length == 0) { return nil; }
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
isTopLevelRow:NO];
[self kb_buildButtonsInContainer:centerContainer
items:centerItems
gap:gap
insetLeft:0
insetRight:0
alignCenter:NO
isTopLevelRow:NO];
[self kb_buildButtonsInContainer:rightContainer
items:rightItems
gap:gap
insetLeft:0
insetRight:0
alignCenter:NO
isTopLevelRow:NO];
return;
}
BOOL alignCenter = [rowConfig.align.lowercaseString isEqualToString:@"center"];
[self kb_buildButtonsInContainer:row
items:[rowConfig resolvedItems]
gap:gap
insetLeft:insetLeft
insetRight:insetRight
alignCenter:alignCenter
isTopLevelRow:YES];
}
- (void)kb_buildButtonsInContainer:(UIView *)container
items:(NSArray<KBKeyboardRowItem *> *)items
gap:(CGFloat)gap
insetLeft:(CGFloat)insetLeft
insetRight:(CGFloat)insetRight
alignCenter:(BOOL)alignCenter
isTopLevelRow:(BOOL)isTopLevelRow {
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);
}];
}
BOOL usingUniformWidth = (self.kb_uniformCharKeyWidth > 0.0);
BOOL allCharacterKeys = YES; // 跟踪该容器内是否全部为字符键
KBKeyButton *previous = nil;
KBKeyButton *firstCharBtn = 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);
}
}
}];
// 字符键letter/digit/sym使用等宽约束自动分配可用空间
// 功能键shift/backspace/mode 等)使用固定宽度。
BOOL isCharacterKey = [item.itemId hasPrefix:@"letter:"] ||
[item.itemId hasPrefix:@"digit:"] ||
[item.itemId hasPrefix:@"sym:"];
if (!isCharacterKey) { allCharacterKeys = NO; }
if (isCharacterKey && usingUniformWidth) {
// 使用跨行统一的固定宽度
CGFloat w = self.kb_uniformCharKeyWidth;
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(w);
}];
} else if (isCharacterKey) {
// 行内等宽:所有字符键与第一个字符键等宽
if (firstCharBtn) {
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(firstCharBtn);
}];
} else {
firstCharBtn = btn;
}
} else {
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; }
// 当使用统一宽度且顶层行全部为字符键时,跳过右锚约束以实现左对齐(列对齐)
BOOL skipRightAnchor = isTopLevelRow && usingUniformWidth && allCharacterKeys;
if (!skipRightAnchor) {
[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:UIControlEventTouchDown];
[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 - Uniform Character Key Width
/// 计算跨行统一字符键宽度:遍历每行各自的 insets/gap/非字符键宽度,
/// 取各行可用字符键宽度的最小值,确保所有行都能容纳。
/// 当各行有效宽度相同时返回 0无需统一
- (CGFloat)kb_calculateUniformCharKeyWidthForRows:(NSArray<KBKeyboardRowConfig *> *)rows {
CGFloat minWidth = CGFLOAT_MAX;
CGFloat maxWidth = 0.0;
BOOL hasCharRow = NO;
CGFloat containerWidth = KBScreenWidth();
for (KBKeyboardRowConfig *row in rows) {
if (row.segments) { continue; } // 跳过分段行
NSArray<KBKeyboardRowItem *> *items = [row resolvedItems];
NSUInteger charCount = 0;
CGFloat nonCharWidth = 0.0;
for (KBKeyboardRowItem *item in items) {
BOOL isChar = [item.itemId hasPrefix:@"letter:"] ||
[item.itemId hasPrefix:@"digit:"] ||
[item.itemId hasPrefix:@"sym:"];
if (isChar) {
charCount++;
} else {
KBKey *key = [self kb_keyForItemId:item.itemId];
CGFloat w = [self kb_widthForItem:item key:key];
nonCharWidth += w;
}
}
if (charCount == 0) { continue; } // 跳过无字符键的行(如底部控制行)
hasCharRow = YES;
// 使用每行各自配置的 insets 和 gap
CGFloat gap = [self kb_gapForRow:row];
CGFloat insetLeft = [self kb_insetLeftForRow:row];
CGFloat insetRight = [self kb_insetRightForRow:row];
CGFloat totalGaps = (items.count > 1) ? (items.count - 1) * gap : 0.0;
CGFloat available = containerWidth - insetLeft - insetRight - totalGaps - nonCharWidth;
CGFloat width = available / charCount;
if (width < minWidth) { minWidth = width; }
if (width > maxWidth) { maxWidth = width; }
}
if (!hasCharRow || minWidth <= 0.0 || minWidth >= CGFLOAT_MAX) { return 0.0; }
// 各行有效宽度相同时无需统一
if (fabs(maxWidth - minWidth) < 0.5) { return 0.0; }
return minWidth;
}
#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 {
NSString *baseLayoutName = self.currentLayoutJsonId.length > 0 ? self.currentLayoutJsonId : @"letters";
if (self.layoutStyle == KBKeyboardLayoutStyleNumbers) {
// 优先查找当前语言的数字/符号布局,如 letters_es_numbers / letters_es_symbols
// 如果不存在,回退到通用布局 numbers / symbolsMore
NSString *numbersName = [NSString stringWithFormat:@"%@_numbers", baseLayoutName];
NSString *symbolsName = [NSString stringWithFormat:@"%@_symbols", baseLayoutName];
NSString *targetName = self.symbolsMoreOn ? symbolsName : numbersName;
KBKeyboardLayout *layout = [self kb_layoutForName:targetName];
if (layout && layout.rows.count >= 4) {
return layout;
}
// 回退到通用布局
return [self kb_layoutForName:(self.symbolsMoreOn ? @"symbolsMore" : @"numbers")];
}
return [self kb_layoutForName:baseLayoutName];
}
- (void)reloadLayoutWithProfileId:(NSString *)profileId {
if (profileId.length == 0) {
NSLog(@"[KBKeyboardView] reloadLayoutWithProfileId: empty profileId, ignoring");
return;
}
NSString *newLayoutJsonId = [[KBKeyboardLayoutResolver sharedResolver] layoutJsonIdForProfileId:profileId];
if ([newLayoutJsonId isEqualToString:self.currentLayoutJsonId]) {
NSLog(@"[KBKeyboardView] Layout already loaded: %@", newLayoutJsonId);
return;
}
NSLog(@"[KBKeyboardView] Switching layout from %@ to %@", self.currentLayoutJsonId, newLayoutJsonId);
self.currentLayoutJsonId = newLayoutJsonId;
// 重新加载键盘布局
[self reloadKeys];
}
- (void)kb_buildLegacyLayout {
self.keysForRows = [self buildKeysForCurrentLayout];
if (self.keysForRows.count < 4) { return; }
if (self.rowViews.count < 4) { return; }
[self buildRow:self.rowViews[0] withKeys:self.keysForRows[0]];
CGFloat row2Spacer = (self.layoutStyle == KBKeyboardLayoutStyleLetters)
? kKBLettersRow2EdgeSpacerMultiplier : 0.0;
[self buildRow:self.rowViews[1] withKeys:self.keysForRows[1] edgeSpacerMultiplier:row2Spacer];
[self buildRow:self.rowViews[2] withKeys:self.keysForRows[2]];
[self buildRow:self.rowViews[3] 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:UIControlEventTouchDown];
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_chinese_dot",
@",": @"sym_comma",
@"": @"sym_dun",
@"?": @"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_corner_l",
@"": @"sym_corner_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_pound",
@"·": @"sym_bullet",
@"^_^": @"sym_face",
@"": @"sym_emdash",
@"«": @"sym_guillemet_l",
@"»": @"sym_guillemet_r",
@"": @"sym_book_title_l",
@"": @"sym_book_title_r",
@"...": @"sym_ellipsis"
};
});
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
- (NSMutableArray<UIView *> *)rowViews {
if (!_rowViews) _rowViews = [NSMutableArray array];
return _rowViews;
}
@end