331 lines
13 KiB
Mathematica
331 lines
13 KiB
Mathematica
|
|
//
|
|||
|
|
// 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
|