This commit is contained in:
2026-03-03 21:31:03 +08:00
parent 4c16ae1736
commit 7adccd60c5
4 changed files with 207 additions and 169 deletions

View File

@@ -8,7 +8,8 @@
"Bash(find:*)",
"Bash(ls:*)",
"Bash(wc:*)",
"Bash(chmod +x:*)"
"Bash(chmod +x:*)",
"Bash(python3:*)"
]
}
}

View File

@@ -536,7 +536,7 @@
},
{
"align": "left",
"insetLeft": 4,
"insetLeft": 15,
"insetRight": 4,
"gap": 5,
"items": [
@@ -546,7 +546,7 @@
},
{
"align": "left",
"insetLeft": 4,
"insetLeft": 27,
"insetRight": 4,
"gap": 5,
"items": [
@@ -561,7 +561,7 @@
"gap": 5,
"items": [
"letter:ㄈ", "letter:ㄌ", "letter:ㄏ", "letter:ㄒ", "letter:ㄖ",
"letter:ㄙ", "letter:ㄩ", "letter:ㄝ", "letter:ㄡ", "letter:ㄥ"
"letter:ㄙ", "letter:ㄩ", "letter:ㄝ", "letter:ㄡ", "letter:ㄥ", "backspace"
]
},
{
@@ -590,7 +590,7 @@
},
{
"align": "left",
"insetLeft": 4,
"insetLeft": 15,
"insetRight": 4,
"gap": 5,
"items": [
@@ -600,7 +600,7 @@
},
{
"align": "left",
"insetLeft": 4,
"insetLeft": 27,
"insetRight": 4,
"gap": 5,
"items": [
@@ -615,7 +615,7 @@
"gap": 5,
"items": [
"letter:ㄈ", "letter:ㄌ", "letter:ㄏ", "letter:ㄒ", "letter:ㄖ",
"letter:ㄙ", "letter:ㄩ", "letter:ㄝ", "letter:ㄡ", "letter:ㄥ"
"letter:ㄙ", "letter:ㄩ", "letter:ㄝ", "letter:ㄡ", "letter:ㄥ", "backspace"
]
},
{

View File

@@ -687,48 +687,6 @@
]
},
"letters_bopomofo_full": {
"__comment": "繁体注音全键盘布局",
"__comment_layout": "第一行:ㄅㄉˇˋㄓˊ˙ㄚㄞㄢㄦ | 第二行:ㄆㄊㄍㄐㄔㄗㄧㄛㄟㄣ | 第三行:ㄇㄋㄎㄑㄕㄘㄨㄜㄠㄤ | 第四行:ㄈㄌㄏㄒㄖㄙㄩㄝㄡㄥ",
"rows": [
{
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 5,
"items": ["letter:ㄅ", "letter:ㄉ", "letter:ˇ", "letter:ˋ", "letter:ㄓ", "letter:ˊ", "letter:˙", "letter:ㄚ", "letter:ㄞ", "letter:ㄢ", "letter:ㄦ"]
},
{
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 5,
"items": ["letter:ㄆ", "letter:ㄊ", "letter:ㄍ", "letter:ㄐ", "letter:ㄔ", "letter:ㄗ", "letter:ㄧ", "letter:ㄛ", "letter:ㄟ", "letter:ㄣ"]
},
{
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 5,
"items": ["letter:ㄇ", "letter:ㄋ", "letter:ㄎ", "letter:ㄑ", "letter:ㄕ", "letter:ㄘ", "letter:ㄨ", "letter:ㄜ", "letter:ㄠ", "letter:ㄤ"]
},
{
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 5,
"items": ["letter:ㄈ", "letter:ㄌ", "letter:ㄏ", "letter:ㄒ", "letter:ㄖ", "letter:ㄙ", "letter:ㄩ", "letter:ㄝ", "letter:ㄡ", "letter:ㄥ"]
},
{
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 5,
"items": ["mode_123", "emoji", "space", "send"]
}
]
},
"letters_bopomofo_full_numbers": {
"__comment": "繁体注音全键盘 - 数字面板123 页)",
"rows": [
@@ -817,47 +775,6 @@
]
},
"letters_bopomofo_standard": {
"__comment": "繁体注音标准布局(与全键盘相同)",
"rows": [
{
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 5,
"items": ["letter:ㄅ", "letter:ㄉ", "letter:ˇ", "letter:ˋ", "letter:ㄓ", "letter:ˊ", "letter:˙", "letter:ㄚ", "letter:ㄞ", "letter:ㄢ", "letter:ㄦ"]
},
{
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 5,
"items": ["letter:ㄆ", "letter:ㄊ", "letter:ㄍ", "letter:ㄐ", "letter:ㄔ", "letter:ㄗ", "letter:ㄧ", "letter:ㄛ", "letter:ㄟ", "letter:ㄣ"]
},
{
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 5,
"items": ["letter:ㄇ", "letter:ㄋ", "letter:ㄎ", "letter:ㄑ", "letter:ㄕ", "letter:ㄘ", "letter:ㄨ", "letter:ㄜ", "letter:ㄠ", "letter:ㄤ"]
},
{
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 5,
"items": ["letter:ㄈ", "letter:ㄌ", "letter:ㄏ", "letter:ㄒ", "letter:ㄖ", "letter:ㄙ", "letter:ㄩ", "letter:ㄝ", "letter:ㄡ", "letter:ㄥ"]
},
{
"align": "left",
"insetLeft": 4,
"insetRight": 4,
"gap": 5,
"items": ["mode_123", "emoji", "space", "send"]
}
]
},
"letters_bopomofo_standard_numbers": {
"__comment": "繁体注音标准 - 数字面板123 页)",
"rows": [

View File

@@ -28,14 +28,13 @@ 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) 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
@@ -76,55 +75,73 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5;
#pragma mark - Base Layout
- (void)buildBase {
[self addSubview:self.row1];
[self addSubview:self.row2];
[self addSubview:self.row3];
[self addSubview:self.row4];
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];
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)];
UIView *firstRow = nil;
UIView *previousRow = nil;
for (NSUInteger i = 0; i < rowCount; i++) {
UIView *rowView = [UIView new];
[self addSubview:rowView];
[self.rowViews addObject:rowView];
[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);
}];
[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];
//
for (UIView *row in @[self.row1, self.row2, self.row3, self.row4]) {
[row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
}
KBKeyboardLayout *layout = [self kb_currentLayout];
NSLog(@"[KBKeyboardView] reloadKeys: layoutName=%@ rows=%lu shiftRows=%lu shiftOn=%d",
@@ -138,24 +155,38 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5;
rows = layout.rows ?: @[];
}
NSLog(@"[KBKeyboardView] reloadKeys: usingRows=%lu", (unsigned long)rows.count);
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 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]];
//
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.row1, self.row2, self.row3, self.row4]) {
for (UIView *row in self.rowViews) {
[row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
}
[self kb_buildLegacyLayout];
@@ -180,7 +211,7 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5;
- (NSUInteger)kb_totalKeyButtonCount {
NSUInteger total = 0;
for (UIView *row in @[self.row1, self.row2, self.row3, self.row4]) {
for (UIView *row in self.rowViews) {
total += [self kb_collectKeyButtonsInView:row].count;
}
return total;
@@ -203,17 +234,16 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5;
- (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; }
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.row1, self.row2, self.row3, self.row4];
NSArray<UIView *> *rows = self.rowViews;
UIView *targetRow = nil;
for (UIView *row in rows) {
@@ -517,19 +547,22 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5;
gap:gap
insetLeft:0
insetRight:0
alignCenter:NO];
alignCenter:NO
isTopLevelRow:NO];
[self kb_buildButtonsInContainer:centerContainer
items:centerItems
gap:gap
insetLeft:0
insetRight:0
alignCenter:NO];
alignCenter:NO
isTopLevelRow:NO];
[self kb_buildButtonsInContainer:rightContainer
items:rightItems
gap:gap
insetLeft:0
insetRight:0
alignCenter:NO];
alignCenter:NO
isTopLevelRow:NO];
return;
}
@@ -539,7 +572,8 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5;
gap:gap
insetLeft:insetLeft
insetRight:insetRight
alignCenter:alignCenter];
alignCenter:alignCenter
isTopLevelRow:YES];
}
- (void)kb_buildButtonsInContainer:(UIView *)container
@@ -547,7 +581,8 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5;
gap:(CGFloat)gap
insetLeft:(CGFloat)insetLeft
insetRight:(CGFloat)insetRight
alignCenter:(BOOL)alignCenter {
alignCenter:(BOOL)alignCenter
isTopLevelRow:(BOOL)isTopLevelRow {
if (items.count == 0) { return; }
UIView *leftSpacer = nil;
@@ -570,7 +605,11 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5;
}];
}
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; }
@@ -589,24 +628,55 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5;
}
}];
CGFloat width = [self kb_widthForItem:item key:btn.key];
if (width > 0.0) {
// 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(width);
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; }
[previous mas_makeConstraints:^(MASConstraintMaker *make) {
if (rightSpacer) {
make.right.equalTo(rightSpacer.mas_left).offset(-gap);
} else {
make.right.equalTo(container.mas_right).offset(-insetRight);
}
}];
// 使
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 {
@@ -875,6 +945,55 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier {
// 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 {
@@ -931,15 +1050,16 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier {
- (void)kb_buildLegacyLayout {
self.keysForRows = [self buildKeysForCurrentLayout];
if (self.keysForRows.count < 4) { return; }
if (self.rowViews.count < 4) { return; }
[self buildRow:self.row1 withKeys:self.keysForRows[0]];
[self buildRow:self.rowViews[0] 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.rowViews[1] withKeys:self.keysForRows[1] edgeSpacerMultiplier:row2Spacer];
[self buildRow:self.row3 withKeys:self.keysForRows[2]];
[self buildRow:self.row4 withKeys:self.keysForRows[3]];
[self buildRow:self.rowViews[2] withKeys:self.keysForRows[2]];
[self buildRow:self.rowViews[3] withKeys:self.keysForRows[3]];
}
- (CGFloat)kb_scaledValue:(CGFloat)designValue {
@@ -1371,9 +1491,9 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier {
#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; }
- (NSMutableArray<UIView *> *)rowViews {
if (!_rowViews) _rowViews = [NSMutableArray array];
return _rowViews;
}
@end