// // KBKeyboardLayoutEngine.m // CustomKeyboard // #import "KBKeyboardLayoutEngine.h" #import "KBKeyboardLayoutConfig.h" #import "KBKeyboardKeyFactory.h" #import "KBKey.h" #import "KBConfig.h" @interface KBKeyboardLayoutEngine () @property (nonatomic, strong) KBKeyboardLayoutConfig *layoutConfig; @end @implementation KBKeyboardLayoutEngine - (instancetype)initWithLayoutConfig:(KBKeyboardLayoutConfig *)layoutConfig { self = [super init]; if (self) { _layoutConfig = layoutConfig; } return self; } - (CGFloat)rowSpacingForLayout:(KBKeyboardLayout *)layout { KBKeyboardLayoutConfig *config = self.layoutConfig; NSNumber *layoutSpacing = layout.rowSpacing; return [self kb_metricValue:layoutSpacing fallback:config.metrics.rowSpacing defaultValue:8.0]; } - (CGFloat)topInsetForLayout:(KBKeyboardLayout *)layout { KBKeyboardLayoutConfig *config = self.layoutConfig; NSNumber *layoutInset = layout.topInset; return [self kb_metricValue:layoutInset fallback:config.metrics.topInset defaultValue:8.0]; } - (CGFloat)bottomInsetForLayout:(KBKeyboardLayout *)layout { KBKeyboardLayoutConfig *config = self.layoutConfig; NSNumber *layoutInset = layout.bottomInset; return [self kb_metricValue:layoutInset fallback:config.metrics.bottomInset defaultValue:6.0]; } - (CGFloat)rowHeightForRow:(KBKeyboardRowConfig *)row { KBKeyboardLayoutConfig *config = self.layoutConfig; NSNumber *height = row.height ?: config.metrics.keyHeight; CGFloat value = [self kb_numberValue:height defaultValue:40.0]; return [self kb_scaledValue:value]; } - (CGFloat)gapForRow:(KBKeyboardRowConfig *)row { KBKeyboardLayoutConfig *config = self.layoutConfig; return [self kb_metricValue:row.gap fallback:config.metrics.gap defaultValue:5.0]; } - (CGFloat)insetLeftForRow:(KBKeyboardRowConfig *)row { KBKeyboardLayoutConfig *config = self.layoutConfig; return [self kb_metricValue:row.insetLeft fallback:config.metrics.edgeInset defaultValue:0.0]; } - (CGFloat)insetRightForRow:(KBKeyboardRowConfig *)row { KBKeyboardLayoutConfig *config = self.layoutConfig; return [self kb_metricValue:row.insetRight fallback:config.metrics.edgeInset defaultValue:0.0]; } - (CGFloat)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.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)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.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 fontSizeForFontKey:fontKey]; } - (CGFloat)fontSizeForFontKey:(NSString *)fontKey { KBKeyboardLayoutFonts *fonts = self.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]; } /// 计算跨行统一字符键宽度:遍历每行各自的 insets/gap/非字符键宽度, /// 取各行可用字符键宽度的最小值,确保所有行都能容纳。 /// 当各行有效宽度相同时返回 0(无需统一)。 - (CGFloat)calculateUniformCharKeyWidthForRows:(NSArray *)rows keyFactory:(KBKeyboardKeyFactory *)keyFactory shiftOn:(BOOL)shiftOn { CGFloat minWidth = CGFLOAT_MAX; CGFloat maxWidth = 0.0; BOOL hasCharRow = NO; CGFloat containerWidth = KBScreenWidth(); for (KBKeyboardRowConfig *row in rows) { if (row.segments) { continue; } // 跳过分段行 NSArray *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 = [keyFactory keyForItemId:item.itemId shiftOn:shiftOn]; CGFloat w = [self widthForItem:item key:key]; nonCharWidth += w; } } if (charCount == 0) { continue; } // 跳过无字符键的行(如底部控制行) hasCharRow = YES; // 使用每行各自配置的 insets 和 gap CGFloat gap = [self gapForRow:row]; CGFloat insetLeft = [self insetLeftForRow:row]; CGFloat insetRight = [self 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 - Helpers - (CGFloat)kb_scaledValue:(CGFloat)designValue { if (self.layoutConfig) { return [self.layoutConfig 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_metricWidthForKey:(NSString *)key { KBKeyboardLayoutMetrics *m = self.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; } @end