This commit is contained in:
2026-03-04 12:54:57 +08:00
parent 2a122d27a9
commit f30b1d7640
20 changed files with 2190 additions and 1546 deletions

View File

@@ -0,0 +1,330 @@
//
// KBKeyboardRowBuilder.m
// CustomKeyboard
//
#import "KBKeyboardRowBuilder.h"
#import "KBKeyboardLayoutConfig.h"
#import "KBKeyboardLayoutEngine.h"
#import "KBKeyboardKeyFactory.h"
#import "KBKeyButton.h"
#import "KBKey.h"
#import "KBBackspaceLongPressHandler.h"
#import "KBSkinManager.h"
#import <Masonry/Masonry.h>
@interface KBKeyboardRowBuilder ()
@property (nonatomic, strong) KBKeyboardLayoutConfig *layoutConfig;
@property (nonatomic, strong) KBKeyboardLayoutEngine *layoutEngine;
@property (nonatomic, strong) KBKeyboardKeyFactory *keyFactory;
@end
@implementation KBKeyboardRowBuilder
- (instancetype)initWithLayoutConfig:(KBKeyboardLayoutConfig *)layoutConfig
layoutEngine:(KBKeyboardLayoutEngine *)layoutEngine
keyFactory:(KBKeyboardKeyFactory *)keyFactory {
self = [super init];
if (self) {
_layoutConfig = layoutConfig;
_layoutEngine = layoutEngine;
_keyFactory = keyFactory;
}
return self;
}
- (void)buildRow:(UIView *)row
withRowConfig:(KBKeyboardRowConfig *)rowConfig
uniformCharWidth:(CGFloat)uniformCharWidth
shiftOn:(BOOL)shiftOn
backspaceHandler:(KBBackspaceLongPressHandler *)backspaceHandler
target:(id)target
action:(SEL)action {
if (!row || !rowConfig) { return; }
CGFloat gap = [self.layoutEngine gapForRow:rowConfig];
CGFloat insetLeft = [self.layoutEngine insetLeftForRow:rowConfig];
CGFloat insetRight = [self.layoutEngine 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
uniformCharWidth:uniformCharWidth
shiftOn:shiftOn
backspaceHandler:backspaceHandler
target:target
action:action];
[self kb_buildButtonsInContainer:centerContainer
items:centerItems
gap:gap
insetLeft:0
insetRight:0
alignCenter:NO
isTopLevelRow:NO
uniformCharWidth:uniformCharWidth
shiftOn:shiftOn
backspaceHandler:backspaceHandler
target:target
action:action];
[self kb_buildButtonsInContainer:rightContainer
items:rightItems
gap:gap
insetLeft:0
insetRight:0
alignCenter:NO
isTopLevelRow:NO
uniformCharWidth:uniformCharWidth
shiftOn:shiftOn
backspaceHandler:backspaceHandler
target:target
action:action];
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
uniformCharWidth:uniformCharWidth
shiftOn:shiftOn
backspaceHandler:backspaceHandler
target:target
action:action];
}
#pragma mark - Private
- (void)kb_buildButtonsInContainer:(UIView *)container
items:(NSArray<KBKeyboardRowItem *> *)items
gap:(CGFloat)gap
insetLeft:(CGFloat)insetLeft
insetRight:(CGFloat)insetRight
alignCenter:(BOOL)alignCenter
isTopLevelRow:(BOOL)isTopLevelRow
uniformCharWidth:(CGFloat)uniformCharWidth
shiftOn:(BOOL)shiftOn
backspaceHandler:(KBBackspaceLongPressHandler *)backspaceHandler
target:(id)target
action:(SEL)action {
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 = (uniformCharWidth > 0.0);
BOOL allCharacterKeys = YES; //
KBKeyButton *previous = nil;
KBKeyButton *firstCharBtn = nil; //
for (KBKeyboardRowItem *item in items) {
KBKeyButton *btn = [self kb_buttonForItem:item
shiftOn:shiftOn
backspaceHandler:backspaceHandler
target:target
action:action];
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 = uniformCharWidth;
[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.layoutEngine 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);
}
}];
}
}
- (KBKeyButton *)kb_buttonForItem:(KBKeyboardRowItem *)item
shiftOn:(BOOL)shiftOn
backspaceHandler:(KBBackspaceLongPressHandler *)backspaceHandler
target:(id)target
action:(SEL)action {
if (item.itemId.length == 0) { return nil; }
KBKeyboardKeyDef *def = [self.layoutConfig keyDefForIdentifier:item.itemId];
KBKey *key = [self.keyFactory keyForItemId:item.itemId shiftOn:shiftOn];
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.layoutEngine fontSizeForItem:item key:key];
if (fontSize > 0.0) {
btn.titleLabel.font = [UIFont systemFontOfSize:fontSize weight:UIFontWeightSemibold];
}
[btn applyThemeForCurrentKey];
if (target && action) {
[btn addTarget:target action:action forControlEvents:UIControlEventTouchDown];
}
if (key.type == KBKeyTypeBackspace) {
[backspaceHandler bindDeleteButton:btn showClearLabel:YES];
}
if (key.type == KBKeyTypeShift) {
btn.selected = shiftOn;
}
[self kb_applySymbolIfNeededForButton:btn keyDef:def fontSize:fontSize];
return btn;
}
- (UIColor *)kb_backgroundColorForItem:(KBKeyboardRowItem *)item keyDef:(KBKeyboardKeyDef *)def {
NSString *hex = def.backgroundColor;
if (hex.length == 0) {
hex = self.layoutConfig.defaultKeyBackground;
}
if (hex.length == 0) { return nil; }
return [KBSkinManager colorFromHexString:hex defaultColor:nil];
}
- (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;
}
@end