Files
keyboard/CustomKeyboard/View/KBKeyboardView/KBKeyboardRowBuilder.m
2026-03-04 12:54:57 +08:00

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