diff --git a/CustomKeyboard/Model/KBKeyboardLayoutConfig.h b/CustomKeyboard/Model/KBKeyboardLayoutConfig.h new file mode 100644 index 0000000..19e4096 --- /dev/null +++ b/CustomKeyboard/Model/KBKeyboardLayoutConfig.h @@ -0,0 +1,96 @@ +// +// KBKeyboardLayoutConfig.h +// CustomKeyboard +// +// 键盘布局配置模型(由 JSON 驱动) +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface KBKeyboardLayoutMetrics : NSObject +@property (nonatomic, strong, nullable) NSNumber *rowSpacing; +@property (nonatomic, strong, nullable) NSNumber *topInset; +@property (nonatomic, strong, nullable) NSNumber *bottomInset; +@property (nonatomic, strong, nullable) NSNumber *keyHeight; +@property (nonatomic, strong, nullable) NSNumber *edgeInset; +@property (nonatomic, strong, nullable) NSNumber *gap; +@property (nonatomic, strong, nullable) NSNumber *letterWidth; +@property (nonatomic, strong, nullable) NSNumber *controlWidth; +@property (nonatomic, strong, nullable) NSNumber *sendWidth; +@property (nonatomic, strong, nullable) NSNumber *symbolsWideWidth; +@property (nonatomic, strong, nullable) NSNumber *symbolsSideWidth; +@end + +@interface KBKeyboardLayoutFonts : NSObject +@property (nonatomic, strong, nullable) NSNumber *letter; +@property (nonatomic, strong, nullable) NSNumber *digit; +@property (nonatomic, strong, nullable) NSNumber *symbol; +@property (nonatomic, strong, nullable) NSNumber *mode; +@property (nonatomic, strong, nullable) NSNumber *space; +@property (nonatomic, strong, nullable) NSNumber *send; +@end + +@interface KBKeyboardKeyDef : NSObject +@property (nonatomic, copy, nullable) NSString *type; +@property (nonatomic, copy, nullable) NSString *title; +@property (nonatomic, copy, nullable) NSString *selectedTitle; +@property (nonatomic, copy, nullable) NSString *symbolName; +@property (nonatomic, copy, nullable) NSString *selectedSymbolName; +@property (nonatomic, copy, nullable) NSString *font; +@property (nonatomic, copy, nullable) NSString *width; +@property (nonatomic, strong, nullable) NSNumber *widthValue; +@property (nonatomic, copy, nullable) NSString *backgroundColor; +@end + +@interface KBKeyboardRowItem : NSObject +@property (nonatomic, copy, nullable) NSString *itemId; +@property (nonatomic, copy, nullable) NSString *width; +@property (nonatomic, strong, nullable) NSNumber *widthValue; ++ (NSArray *)itemsFromRawArray:(NSArray *)raw; +@end + +@interface KBKeyboardRowSegments : NSObject +@property (nonatomic, strong, nullable) NSArray *left; +@property (nonatomic, strong, nullable) NSArray *center; +@property (nonatomic, strong, nullable) NSArray *right; +- (NSArray *)leftItems; +- (NSArray *)centerItems; +- (NSArray *)rightItems; +@end + +@interface KBKeyboardRowConfig : NSObject +@property (nonatomic, strong, nullable) NSNumber *height; +@property (nonatomic, strong, nullable) NSNumber *insetLeft; +@property (nonatomic, strong, nullable) NSNumber *insetRight; +@property (nonatomic, strong, nullable) NSNumber *gap; +@property (nonatomic, copy, nullable) NSString *align; +@property (nonatomic, strong, nullable) NSArray *items; +@property (nonatomic, strong, nullable) KBKeyboardRowSegments *segments; +- (NSArray *)resolvedItems; +@end + +@interface KBKeyboardLayout : NSObject +@property (nonatomic, strong, nullable) NSArray *rows; +@end + +@interface KBKeyboardLayoutConfig : NSObject +@property (nonatomic, assign) CGFloat designWidth; +@property (nonatomic, strong, nullable) KBKeyboardLayoutMetrics *metrics; +@property (nonatomic, strong, nullable) KBKeyboardLayoutFonts *fonts; +@property (nonatomic, copy, nullable) NSString *defaultKeyBackground; +@property (nonatomic, strong, nullable) NSDictionary *keyDefs; +@property (nonatomic, strong, nullable) NSDictionary *layouts; + ++ (nullable instancetype)sharedConfig; ++ (nullable instancetype)configFromJSONData:(NSData *)data; +- (CGFloat)scaledValue:(CGFloat)designValue; +- (CGFloat)keyboardAreaDesignHeight; +- (CGFloat)keyboardAreaScaledHeight; +- (nullable KBKeyboardLayout *)layoutForName:(NSString *)name; +- (nullable KBKeyboardKeyDef *)keyDefForIdentifier:(NSString *)identifier; +@end + +NS_ASSUME_NONNULL_END diff --git a/CustomKeyboard/Model/KBKeyboardLayoutConfig.m b/CustomKeyboard/Model/KBKeyboardLayoutConfig.m new file mode 100644 index 0000000..64147eb --- /dev/null +++ b/CustomKeyboard/Model/KBKeyboardLayoutConfig.m @@ -0,0 +1,187 @@ +// +// KBKeyboardLayoutConfig.m +// CustomKeyboard +// + +#import "KBKeyboardLayoutConfig.h" +#import +#import "KBConfig.h" + +static NSString * const kKBKeyboardLayoutConfigFileName = @"kb_keyboard_layout_config"; + +@implementation KBKeyboardLayoutMetrics +@end + +@implementation KBKeyboardLayoutFonts +@end + +@implementation KBKeyboardKeyDef +@end + +@implementation KBKeyboardRowItem + ++ (NSDictionary *)mj_replacedKeyFromPropertyName { + return @{ @"itemId": @"id" }; +} + ++ (NSArray *)itemsFromRawArray:(NSArray *)raw { + if (![raw isKindOfClass:[NSArray class]] || raw.count == 0) { + return @[]; + } + NSMutableArray *items = [NSMutableArray arrayWithCapacity:raw.count]; + for (id obj in raw) { + if ([obj isKindOfClass:[NSString class]]) { + KBKeyboardRowItem *item = [KBKeyboardRowItem new]; + item.itemId = (NSString *)obj; + [items addObject:item]; + continue; + } + if ([obj isKindOfClass:[NSDictionary class]]) { + KBKeyboardRowItem *item = [KBKeyboardRowItem mj_objectWithKeyValues:obj]; + if (item.itemId.length == 0) { + NSString *fallback = ((NSDictionary *)obj)[@"id"]; + if ([fallback isKindOfClass:[NSString class]]) { + item.itemId = fallback; + } + } + if (item.itemId.length > 0) { + [items addObject:item]; + } + } + } + return items.copy; +} + +@end + +@implementation KBKeyboardRowSegments + +- (NSArray *)leftItems { + return [KBKeyboardRowItem itemsFromRawArray:self.left ?: @[]]; +} + +- (NSArray *)centerItems { + return [KBKeyboardRowItem itemsFromRawArray:self.center ?: @[]]; +} + +- (NSArray *)rightItems { + return [KBKeyboardRowItem itemsFromRawArray:self.right ?: @[]]; +} + +@end + +@implementation KBKeyboardRowConfig + +- (NSArray *)resolvedItems { + return [KBKeyboardRowItem itemsFromRawArray:self.items ?: @[]]; +} + +@end + +@implementation KBKeyboardLayout + ++ (NSDictionary *)mj_objectClassInArray { + return @{ @"rows": [KBKeyboardRowConfig class] }; +} + +@end + +@implementation KBKeyboardLayoutConfig + ++ (instancetype)sharedConfig { + static KBKeyboardLayoutConfig *config = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *path = [[NSBundle mainBundle] pathForResource:kKBKeyboardLayoutConfigFileName ofType:@"json"]; + NSData *data = path.length ? [NSData dataWithContentsOfFile:path] : nil; + config = data ? [KBKeyboardLayoutConfig configFromJSONData:data] : nil; + }); + return config; +} + ++ (instancetype)configFromJSONData:(NSData *)data { + if (data.length == 0) { return nil; } + NSError *error = nil; + id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + if (error || ![json isKindOfClass:[NSDictionary class]]) { + return nil; + } + NSDictionary *dict = (NSDictionary *)json; + KBKeyboardLayoutConfig *config = [KBKeyboardLayoutConfig mj_objectWithKeyValues:dict]; + + NSDictionary *keyDefsRaw = dict[@"keyDefs"]; + if ([keyDefsRaw isKindOfClass:[NSDictionary class]]) { + NSMutableDictionary *defs = [NSMutableDictionary dictionaryWithCapacity:keyDefsRaw.count]; + [keyDefsRaw enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + if (![key isKindOfClass:[NSString class]] || ![obj isKindOfClass:[NSDictionary class]]) { + return; + } + KBKeyboardKeyDef *def = [KBKeyboardKeyDef mj_objectWithKeyValues:obj]; + if (def) { + defs[key] = def; + } + }]; + config.keyDefs = defs.copy; + } + + NSDictionary *layoutsRaw = dict[@"layouts"]; + if ([layoutsRaw isKindOfClass:[NSDictionary class]]) { + NSMutableDictionary *layouts = [NSMutableDictionary dictionaryWithCapacity:layoutsRaw.count]; + [layoutsRaw enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + if (![key isKindOfClass:[NSString class]] || ![obj isKindOfClass:[NSDictionary class]]) { + return; + } + KBKeyboardLayout *layout = [KBKeyboardLayout mj_objectWithKeyValues:obj]; + if (layout) { + layouts[key] = layout; + } + }]; + config.layouts = layouts.copy; + } + + return config; +} + +- (CGFloat)scaledValue:(CGFloat)designValue { + CGFloat baseWidth = (self.designWidth > 0.0) ? self.designWidth : KB_DESIGN_WIDTH; + CGFloat scale = KBScreenWidth() / baseWidth; + return designValue * scale; +} + +- (CGFloat)keyboardAreaDesignHeight { + KBKeyboardLayout *layout = [self layoutForName:@"letters"] ?: self.layouts.allValues.firstObject; + NSUInteger rowCount = layout.rows.count; + if (rowCount == 0) { return 0.0; } + + CGFloat rowSpacing = self.metrics.rowSpacing.doubleValue; + CGFloat topInset = self.metrics.topInset.doubleValue; + CGFloat bottomInset = self.metrics.bottomInset.doubleValue; + + CGFloat total = topInset + bottomInset + rowSpacing * (rowCount - 1); + for (KBKeyboardRowConfig *row in layout.rows) { + CGFloat height = row.height.doubleValue; + if (height <= 0.0) { + height = self.metrics.keyHeight.doubleValue; + } + if (height <= 0.0) { height = 40.0; } + total += height; + } + return total; +} + +- (CGFloat)keyboardAreaScaledHeight { + CGFloat designHeight = [self keyboardAreaDesignHeight]; + return designHeight > 0.0 ? [self scaledValue:designHeight] : 0.0; +} + +- (KBKeyboardLayout *)layoutForName:(NSString *)name { + if (name.length == 0) { return nil; } + return self.layouts[name]; +} + +- (KBKeyboardKeyDef *)keyDefForIdentifier:(NSString *)identifier { + if (identifier.length == 0) { return nil; } + return self.keyDefs[identifier]; +} + +@end diff --git a/CustomKeyboard/Resource/kb_keyboard_layout_config.json b/CustomKeyboard/Resource/kb_keyboard_layout_config.json new file mode 100644 index 0000000..c779a2e --- /dev/null +++ b/CustomKeyboard/Resource/kb_keyboard_layout_config.json @@ -0,0 +1,196 @@ +{ + "designWidth": 375, + "defaultKeyBackground": "#FFFFFF", + "metrics": { + "rowSpacing": 8, + "topInset": 8, + "bottomInset": 6, + "keyHeight": 41, + "edgeInset": 4, + "gap": 5, + "letterWidth": 32, + "controlWidth": 41, + "sendWidth": 88, + "symbolsWideWidth": 47, + "symbolsSideWidth": 41 + }, + "fonts": { + "letter": 20, + "digit": 20, + "symbol": 18, + "mode": 14, + "space": 18, + "send": 18 + }, + "keyDefs": { + "shift": { "type": "shift", "title": "⇧", "symbolName": "shift", "selectedSymbolName": "shift.fill", "font": "symbol", "width": "controlWidth", "backgroundColor": "#B7BBC4" }, + "backspace": { "type": "backspace", "title": "⌫", "font": "symbol", "width": "controlWidth", "backgroundColor": "#B7BBC4" }, + "mode_123": { "type": "mode", "title": "123", "font": "mode", "width": "controlWidth", "backgroundColor": "#B7BBC4" }, + "mode_abc": { "type": "mode", "title": "ABC", "font": "mode", "width": "controlWidth", "backgroundColor": "#B7BBC4" }, + "symbols_toggle_more": { "type": "symbolsToggle", "title": "#+=", "font": "mode", "width": "symbolsSideWidth", "backgroundColor": "#B7BBC4" }, + "symbols_toggle_123": { "type": "symbolsToggle", "title": "123", "font": "mode", "width": "symbolsSideWidth", "backgroundColor": "#B7BBC4" }, + "emoji": { "type": "custom", "title": "😁", "font": "symbol", "width": "controlWidth", "backgroundColor": "#B7BBC4" }, + "space": { "type": "space", "title": "space", "font": "space", "width": "flex" }, + "send": { "type": "return", "title": "send", "font": "send", "width": "sendWidth", "backgroundColor": "#B7BBC4" } + }, + "layouts": { + "letters": { + "rows": [ + { + "align": "left", + "insetLeft": 4, + "insetRight": 4, + "gap": 5, + "items": [ + "letter:q", "letter:w", "letter:e", "letter:r", "letter:t", + "letter:y", "letter:u", "letter:i", "letter:o", "letter:p" + ] + }, + { + "align": "center", + "insetLeft": 0, + "insetRight": 0, + "gap": 5, + "items": [ + "letter:a", "letter:s", "letter:d", "letter:f", "letter:g", + "letter:h", "letter:j", "letter:k", "letter:l" + ] + }, + { + "align": "left", + "insetLeft": 4, + "insetRight": 4, + "gap": 5, + "segments": { + "left": [ + { "id": "shift", "width": "controlWidth" } + ], + "center": [ + "letter:z", "letter:x", "letter:c", "letter:v", "letter:b", "letter:n", "letter:m" + ], + "right": [ + { "id": "backspace", "width": "controlWidth" } + ] + } + }, + { + "align": "left", + "insetLeft": 4, + "insetRight": 4, + "gap": 5, + "items": [ + "mode_123", "emoji", "space", "send" + ] + } + ] + }, + "numbers": { + "rows": [ + { + "align": "left", + "insetLeft": 4, + "insetRight": 4, + "gap": 5, + "items": [ + "digit:1", "digit:2", "digit:3", "digit:4", "digit:5", + "digit:6", "digit:7", "digit:8", "digit:9", "digit:0" + ] + }, + { + "align": "left", + "insetLeft": 4, + "insetRight": 4, + "gap": 5, + "items": [ + "sym:-", "sym:/", "sym::", "sym:;", "sym:(", + "sym:)", "sym:¥", "sym:&", "sym:@", "sym:“" + ] + }, + { + "align": "center", + "insetLeft": 4, + "insetRight": 4, + "gap": 5, + "segments": { + "left": [ + { "id": "symbols_toggle_more", "width": "symbolsSideWidth" } + ], + "center": [ + { "id": "sym:.", "width": "symbolsWideWidth" }, + { "id": "sym:,", "width": "symbolsWideWidth" }, + { "id": "sym:?", "width": "symbolsWideWidth" }, + { "id": "sym:!", "width": "symbolsWideWidth" }, + { "id": "sym:‘", "width": "symbolsWideWidth" } + ], + "right": [ + { "id": "backspace", "width": "symbolsSideWidth" } + ] + } + }, + { + "align": "left", + "insetLeft": 4, + "insetRight": 4, + "gap": 5, + "items": [ + "mode_abc", "emoji", "space", "send" + ] + } + ] + }, + "symbolsMore": { + "rows": [ + { + "align": "left", + "insetLeft": 4, + "insetRight": 4, + "gap": 5, + "items": [ + "sym:[", "sym:]", "sym:{", "sym:}", "sym:#", + "sym:%", "sym:^", "sym:*", "sym:+", "sym:=" + ] + }, + { + "align": "left", + "insetLeft": 4, + "insetRight": 4, + "gap": 5, + "items": [ + "sym:_", "sym:\\", "sym:|", "sym:~", "sym:<", + "sym:>", "sym:€", "sym:¥", "sym:$", "sym:·" + ] + }, + { + "align": "center", + "insetLeft": 4, + "insetRight": 4, + "gap": 5, + "segments": { + "left": [ + { "id": "symbols_toggle_123", "width": "symbolsSideWidth" } + ], + "center": [ + { "id": "sym:.", "width": "symbolsWideWidth" }, + { "id": "sym:,", "width": "symbolsWideWidth" }, + { "id": "sym:?", "width": "symbolsWideWidth" }, + { "id": "sym:!", "width": "symbolsWideWidth" }, + { "id": "sym:‘", "width": "symbolsWideWidth" } + ], + "right": [ + { "id": "backspace", "width": "symbolsSideWidth" } + ] + } + }, + { + "align": "left", + "insetLeft": 4, + "insetRight": 4, + "gap": 5, + "items": [ + "mode_abc", "emoji", "space", "send" + ] + } + ] + } + } +} diff --git a/CustomKeyboard/Resource/normal_them.zip b/CustomKeyboard/Resource/normal_them.zip new file mode 100644 index 0000000..ac2e9c2 Binary files /dev/null and b/CustomKeyboard/Resource/normal_them.zip differ diff --git a/CustomKeyboard/View/KBKeyBoardMainView.m b/CustomKeyboard/View/KBKeyBoardMainView.m index 5a1528b..d742eab 100644 --- a/CustomKeyboard/View/KBKeyBoardMainView.m +++ b/CustomKeyboard/View/KBKeyBoardMainView.m @@ -15,6 +15,7 @@ #import "Masonry.h" #import "KBSkinManager.h" #import "KBBackspaceUndoManager.h" +#import "KBKeyboardLayoutConfig.h" @interface KBKeyBoardMainView () @property (nonatomic, strong) KBToolBar *topBar; @@ -45,6 +46,13 @@ // 键盘区域(高度按照设计值做等比缩放,避免不同机型上按键被压缩/拉伸) CGFloat keyboardAreaHeight = KBFit(200.0f); + KBKeyboardLayoutConfig *layoutConfig = [KBKeyboardLayoutConfig sharedConfig]; + if (layoutConfig) { + CGFloat configHeight = [layoutConfig keyboardAreaScaledHeight]; + if (configHeight > 0.0) { + keyboardAreaHeight = configHeight; + } + } CGFloat bottomInset = KBFit(4.0f); // CGFloat topBarHeight = KBFit(40.0f); CGFloat barSpacing = KBFit(6.0f); diff --git a/CustomKeyboard/View/KBKeyButton.h b/CustomKeyboard/View/KBKeyButton.h index 2d43d08..80a984b 100644 --- a/CustomKeyboard/View/KBKeyButton.h +++ b/CustomKeyboard/View/KBKeyButton.h @@ -11,6 +11,7 @@ @property (nonatomic, strong) KBKey *key; @property (nonatomic, strong) UIImageView *iconView; +@property (nonatomic, strong, nullable) UIColor *customBackgroundColor; /// 配置基础样式(背景、圆角等)。创建按钮时调用。 - (void)applyDefaultStyle; diff --git a/CustomKeyboard/View/KBKeyButton.m b/CustomKeyboard/View/KBKeyButton.m index 0994495..70fb2de 100644 --- a/CustomKeyboard/View/KBKeyButton.m +++ b/CustomKeyboard/View/KBKeyButton.m @@ -6,12 +6,14 @@ #import "KBKeyButton.h" #import "KBKey.h" #import "KBSkinManager.h" +#import @interface KBKeyButton () // 内部缓存:便于从按钮查找到所属的 KBKeyboardView @property (nonatomic, weak, readonly) UIView *kb_keyboardContainer; @property (nonatomic, strong) UIImageView *normalImageView; /// 没有皮肤的时候展示 @property (nonatomic, strong) UIColor *baseBackgroundColor; /// 无按下状态下,由皮肤/主题决定的底色(由 normalImageView 展示) +@property (nonatomic, strong) CAGradientLayer *bottomShadowLayer; @end @@ -24,8 +26,10 @@ [NSLayoutConstraint activateConstraints:@[ [self.normalImageView.topAnchor constraintEqualToAnchor:self.topAnchor], [self.normalImageView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor], - [self.normalImageView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:2], - [self.normalImageView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-2], +// [self.normalImageView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:2], +// [self.normalImageView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-2], + [self.normalImageView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:0], + [self.normalImageView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-0], ]]; [self applyDefaultStyle]; } @@ -48,6 +52,7 @@ // 初始状态下根据主题设置底色(给没有皮肤图的按键使用) [self refreshStateAppearance]; + [self kb_setupBottomShadowIfNeeded]; // 懒创建图标视图,用于后续皮肤按键小图标展示 if (!self.iconView) { @@ -72,6 +77,20 @@ } } +- (void)layoutSubviews { + [super layoutSubviews]; + if (!self.bottomShadowLayer) { return; } + CGRect bounds = self.normalImageView.bounds; + CGFloat shadowHeight = 2; + if (CGRectGetHeight(bounds) <= 0 || CGRectGetWidth(bounds) <= 0) { + return; + } + self.bottomShadowLayer.frame = CGRectMake(0, + CGRectGetHeight(bounds) - shadowHeight, + CGRectGetWidth(bounds), + shadowHeight); +} + - (void)setKey:(KBKey *)key { _key = key; } @@ -121,14 +140,25 @@ [self refreshStateAppearance]; } +- (void)setCustomBackgroundColor:(UIColor *)customBackgroundColor { + _customBackgroundColor = customBackgroundColor; + [self refreshStateAppearance]; +} + - (void)refreshStateAppearance { // 选中态用于 Shift/CapsLock 等特殊按键的高亮显示 KBSkinTheme *t = [KBSkinManager shared].current; UIColor *base = nil; if (self.isSelected) { base = t.keyHighlightBackground ?: t.keyBackground; + if (self.customBackgroundColor) { + base = t.keyHighlightBackground ?: self.customBackgroundColor; + } } else { - base = t.keyBackground; + base = self.customBackgroundColor ?: t.keyBackground; + } + if (self.customBackgroundColor && self.key.type == KBKeyTypeShift) { + base = self.customBackgroundColor; } if (!base) { base = [UIColor whiteColor]; @@ -138,6 +168,13 @@ // 按键背景统一由 normalImageView 控制,按钮本身透明 self.backgroundColor = [UIColor clearColor]; + if (self.key.type == KBKeyTypeShift) { + UIColor *textColor = self.isSelected ? [UIColor blackColor] : (t.keyTextColor ?: [UIColor blackColor]); + [self setTitleColor:textColor forState:UIControlStateNormal]; + [self setTitleColor:textColor forState:UIControlStateHighlighted]; + [self setTitleColor:textColor forState:UIControlStateSelected]; + } + // 有皮肤图时仅展示 icon,不再显示普通背景色 if (self.iconView.image != nil || self.normalImageView.hidden) { return; @@ -169,6 +206,7 @@ BOOL hasIcon = (iconImg != nil); self.normalImageView.hidden = hasIcon; + self.bottomShadowLayer.hidden = hasIcon; if (hasIcon) { // 有图标:仅显示图片,完全隐藏文字 [self setTitle:@"" forState:UIControlStateNormal]; @@ -184,6 +222,19 @@ } } +- (void)kb_setupBottomShadowIfNeeded { + if (self.bottomShadowLayer) { return; } + CAGradientLayer *layer = [CAGradientLayer layer]; + layer.startPoint = CGPointMake(0.5, 0.0); + layer.endPoint = CGPointMake(0.5, 1.0); + layer.colors = @[ + (id)[UIColor colorWithWhite:0 alpha:0.5].CGColor, + (id)[UIColor colorWithWhite:0 alpha:0.7].CGColor + ]; + [self.normalImageView.layer addSublayer:layer]; +// self.bottomShadowLayer = layer; +} + - (UIImageView *)normalImageView{ if (!_normalImageView) { _normalImageView = [[UIImageView alloc] init]; diff --git a/CustomKeyboard/View/KBKeyboardView.m b/CustomKeyboard/View/KBKeyboardView.m index 060841c..2b9849f 100644 --- a/CustomKeyboard/View/KBKeyboardView.m +++ b/CustomKeyboard/View/KBKeyboardView.m @@ -9,6 +9,7 @@ #import "KBSkinManager.h" #import "KBKeyPreviewView.h" #import "KBBackspaceLongPressHandler.h" +#import "KBKeyboardLayoutConfig.h" // UI 常量统一管理,方便后续调试样式(以 375 宽设计稿为基准,通过 KBFit 做等比缩放) #define kKBRowVerticalSpacing KBFit(8.0f) @@ -33,6 +34,7 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5; @property (nonatomic, strong) NSArray *> *keysForRows; @property (nonatomic, strong) KBBackspaceLongPressHandler *backspaceHandler; @property (nonatomic, strong) KBKeyPreviewView *previewView; +@property (nonatomic, strong) KBKeyboardLayoutConfig *layoutConfig; @end @implementation KBKeyboardView @@ -44,6 +46,7 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5; // 默认小写:与需求一致,初始不开启 Shift _shiftOn = NO; _symbolsMoreOn = NO; // 数字面板默认第一页(123) + self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig]; self.backspaceHandler = [[KBBackspaceLongPressHandler alloc] initWithContainerView:self]; [self buildBase]; [self reloadKeys]; @@ -67,26 +70,39 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5; [self addSubview:self.row3]; [self addSubview:self.row4]; + KBKeyboardLayoutConfig *config = [self kb_layoutConfig]; + KBKeyboardLayout *layout = [self kb_layoutForName:@"letters"]; + NSArray *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)]; + [self.row1 mas_makeConstraints:^(MASConstraintMaker *make) { - make.top.equalTo(self.mas_top).offset(kKBRowVerticalSpacing); + make.top.equalTo(self.mas_top).offset(topInset); make.left.right.equalTo(self); - make.height.mas_equalTo(kKBRowHeight); + make.height.mas_equalTo(row1Height); }]; [self.row2 mas_makeConstraints:^(MASConstraintMaker *make) { - make.top.equalTo(self.row1.mas_bottom).offset(kKBRowVerticalSpacing); + make.top.equalTo(self.row1.mas_bottom).offset(rowSpacing); make.left.right.equalTo(self); - make.height.equalTo(self.row1); + make.height.mas_equalTo(row2Height); }]; [self.row3 mas_makeConstraints:^(MASConstraintMaker *make) { - make.top.equalTo(self.row2.mas_bottom).offset(kKBRowVerticalSpacing); + make.top.equalTo(self.row2.mas_bottom).offset(rowSpacing); make.left.right.equalTo(self); - make.height.equalTo(self.row1); + make.height.mas_equalTo(row3Height); }]; [self.row4 mas_makeConstraints:^(MASConstraintMaker *make) { - make.top.equalTo(self.row3.mas_bottom).offset(kKBRowVerticalSpacing); + make.top.equalTo(self.row3.mas_bottom).offset(rowSpacing); make.left.right.equalTo(self); - make.height.equalTo(self.row1); - make.bottom.equalTo(self.mas_bottom).offset(-6); + make.height.mas_equalTo(row4Height); + make.bottom.equalTo(self.mas_bottom).offset(-bottomInset); }]; } @@ -99,18 +115,17 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5; [row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; } - self.keysForRows = [self buildKeysForCurrentLayout]; - if (self.keysForRows.count < 4) return; + KBKeyboardLayout *layout = [self kb_currentLayout]; + NSArray *rows = layout.rows ?: @[]; + if (rows.count < 4) { + [self kb_buildLegacyLayout]; + return; + } - [self buildRow:self.row1 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.row3 withKeys:self.keysForRows[2]]; - [self buildRow:self.row4 withKeys:self.keysForRows[3]]; + [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]]; } #pragma mark - Key Model Construction @@ -315,6 +330,152 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5; #pragma mark - Row Building +- (void)buildRow:(UIView *)row withRowConfig:(KBKeyboardRowConfig *)rowConfig { + if (!row || !rowConfig) { return; } + CGFloat gap = [self kb_gapForRow:rowConfig]; + CGFloat insetLeft = [self kb_insetLeftForRow:rowConfig]; + CGFloat insetRight = [self kb_insetRightForRow:rowConfig]; + + if (rowConfig.segments) { + KBKeyboardRowSegments *segments = rowConfig.segments; + NSArray *leftItems = [segments leftItems]; + NSArray *centerItems = [segments centerItems]; + NSArray *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]; + [self kb_buildButtonsInContainer:centerContainer + items:centerItems + gap:gap + insetLeft:0 + insetRight:0 + alignCenter:NO]; + [self kb_buildButtonsInContainer:rightContainer + items:rightItems + gap:gap + insetLeft:0 + insetRight:0 + alignCenter:NO]; + return; + } + + BOOL alignCenter = [rowConfig.align.lowercaseString isEqualToString:@"center"]; + [self kb_buildButtonsInContainer:row + items:[rowConfig resolvedItems] + gap:gap + insetLeft:insetLeft + insetRight:insetRight + alignCenter:alignCenter]; +} + +- (void)kb_buildButtonsInContainer:(UIView *)container + items:(NSArray *)items + gap:(CGFloat)gap + insetLeft:(CGFloat)insetLeft + insetRight:(CGFloat)insetRight + alignCenter:(BOOL)alignCenter { + 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); + }]; + } + + KBKeyButton *previous = nil; + for (KBKeyboardRowItem *item in items) { + KBKeyButton *btn = [self kb_buttonForItem:item]; + 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); + } + } + }]; + + 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); + } + }]; +} + - (void)buildRow:(UIView *)row withKeys:(NSArray *)keys { [self buildRow:row withKeys:keys edgeSpacerMultiplier:0.0]; } @@ -581,6 +742,382 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier { // Space 不设置宽度;通过此前已建立的左右约束自动占满剩余宽度。 } +#pragma mark - Config Helpers + +- (KBKeyboardLayoutConfig *)kb_layoutConfig { + if (!self.layoutConfig) { + self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig]; + } + return self.layoutConfig; +} + +- (KBKeyboardLayout *)kb_layoutForName:(NSString *)name { + return [[self kb_layoutConfig] layoutForName:name]; +} + +- (KBKeyboardLayout *)kb_currentLayout { + if (self.layoutStyle == KBKeyboardLayoutStyleNumbers) { + return [self kb_layoutForName:(self.symbolsMoreOn ? @"symbolsMore" : @"numbers")]; + } + return [self kb_layoutForName:@"letters"]; +} + +- (void)kb_buildLegacyLayout { + self.keysForRows = [self buildKeysForCurrentLayout]; + if (self.keysForRows.count < 4) { return; } + + [self buildRow:self.row1 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.row3 withKeys:self.keysForRows[2]]; + [self buildRow:self.row4 withKeys:self.keysForRows[3]]; +} + +- (CGFloat)kb_scaledValue:(CGFloat)designValue { + KBKeyboardLayoutConfig *config = [self kb_layoutConfig]; + if (config) { + return [config 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_rowHeightForRow:(KBKeyboardRowConfig *)row { + KBKeyboardLayoutConfig *config = [self kb_layoutConfig]; + NSNumber *height = row.height ?: config.metrics.keyHeight; + CGFloat value = [self kb_numberValue:height defaultValue:40.0]; + return [self kb_scaledValue:value]; +} + +- (CGFloat)kb_gapForRow:(KBKeyboardRowConfig *)row { + KBKeyboardLayoutConfig *config = [self kb_layoutConfig]; + return [self kb_metricValue:row.gap fallback:config.metrics.gap defaultValue:5.0]; +} + +- (CGFloat)kb_insetLeftForRow:(KBKeyboardRowConfig *)row { + KBKeyboardLayoutConfig *config = [self kb_layoutConfig]; + return [self kb_metricValue:row.insetLeft fallback:config.metrics.edgeInset defaultValue:0.0]; +} + +- (CGFloat)kb_insetRightForRow:(KBKeyboardRowConfig *)row { + KBKeyboardLayoutConfig *config = [self kb_layoutConfig]; + return [self kb_metricValue:row.insetRight fallback:config.metrics.edgeInset defaultValue:0.0]; +} + +- (KBKeyButton *)kb_buttonForItem:(KBKeyboardRowItem *)item { + if (item.itemId.length == 0) { return nil; } + KBKeyboardKeyDef *def = [[self kb_layoutConfig] keyDefForIdentifier:item.itemId]; + KBKey *key = [self kb_keyForItemId:item.itemId]; + 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 kb_fontSizeForItem:item key:key]; + if (fontSize > 0.0) { + btn.titleLabel.font = [UIFont systemFontOfSize:fontSize weight:UIFontWeightSemibold]; + } + + [btn applyThemeForCurrentKey]; + [self kb_applySymbolIfNeededForButton:btn keyDef:def fontSize:fontSize]; + [btn addTarget:self action:@selector(onKeyTapped:) forControlEvents:UIControlEventTouchUpInside]; + + if (key.type == KBKeyTypeBackspace) { + [self.backspaceHandler bindDeleteButton:btn showClearLabel:YES]; + } + if (key.type == KBKeyTypeShift) { + btn.selected = self.shiftOn; + } + return btn; +} + +- (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; +} + +- (UIColor *)kb_backgroundColorForItem:(KBKeyboardRowItem *)item keyDef:(KBKeyboardKeyDef *)def { + NSString *hex = def.backgroundColor; + if (hex.length == 0) { + hex = [self kb_layoutConfig].defaultKeyBackground; + } + if (hex.length == 0) { return nil; } + return [KBSkinManager colorFromHexString:hex defaultColor:nil]; +} + +- (CGFloat)kb_metricWidthForKey:(NSString *)key { + KBKeyboardLayoutMetrics *m = [self kb_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; +} + +- (CGFloat)kb_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 kb_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)kb_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 kb_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 kb_fontSizeForFontKey:fontKey]; +} + +- (CGFloat)kb_fontSizeForFontKey:(NSString *)fontKey { + KBKeyboardLayoutFonts *fonts = [self kb_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]; +} + +- (KBKey *)kb_keyForItemId:(NSString *)itemId { + if (itemId.length == 0) { return nil; } + KBKeyboardKeyDef *def = [[self kb_layoutConfig] keyDefForIdentifier:itemId]; + if (def) { + return [self kb_keyFromDef:def identifier:itemId]; + } + + NSRange range = [itemId rangeOfString:@":"]; + if (range.location != NSNotFound) { + NSString *prefix = [itemId substringToIndex:range.location]; + NSString *value = [itemId substringFromIndex:range.location + 1]; + if ([prefix isEqualToString:@"letter"]) { + if (value.length == 1) { + return [self kb_letterKeyWithChar:value]; + } + return nil; + } + if ([prefix isEqualToString:@"digit"]) { + NSString *identifier = [NSString stringWithFormat:@"digit_%@", value]; + KBKey *k = [KBKey keyWithIdentifier:identifier title:value output:value type:KBKeyTypeCharacter]; + k.caseVariant = KBKeyCaseVariantNone; + return k; + } + if ([prefix isEqualToString:@"sym"]) { + NSString *identifier = [self kb_identifierForSymbol:value]; + KBKey *k = [KBKey keyWithIdentifier:identifier title:value output:value type:KBKeyTypeCharacter]; + k.caseVariant = KBKeyCaseVariantNone; + return k; + } + } + return nil; +} + +- (KBKey *)kb_keyFromDef:(KBKeyboardKeyDef *)def identifier:(NSString *)identifier { + KBKeyType type = [self kb_keyTypeForDef:def]; + NSString *title = def.title ?: @""; + if (type == KBKeyTypeShift && self.shiftOn && def.selectedTitle.length > 0) { + title = def.selectedTitle; + } + NSString *output = @""; + switch (type) { + case KBKeyTypeSpace: + output = @" "; + break; + case KBKeyTypeReturn: + output = @"\n"; + break; + default: + output = @""; + break; + } + + NSString *finalId = identifier; + if ([identifier isEqualToString:@"emoji"]) { + finalId = KBKeyIdentifierEmojiPanel; + } else if ([identifier isEqualToString:@"send"]) { + finalId = @"return"; + } + + KBKey *k = [KBKey keyWithIdentifier:finalId title:title output:output type:type]; + k.caseVariant = KBKeyCaseVariantNone; + return k; +} + +- (KBKeyType)kb_keyTypeForDef:(KBKeyboardKeyDef *)def { + NSString *type = def.type.lowercaseString; + if ([type isEqualToString:@"shift"]) return KBKeyTypeShift; + if ([type isEqualToString:@"backspace"]) return KBKeyTypeBackspace; + if ([type isEqualToString:@"mode"]) return KBKeyTypeModeChange; + if ([type isEqualToString:@"symbolstoggle"]) return KBKeyTypeSymbolsToggle; + if ([type isEqualToString:@"space"]) return KBKeyTypeSpace; + if ([type isEqualToString:@"return"]) return KBKeyTypeReturn; + if ([type isEqualToString:@"globe"]) return KBKeyTypeGlobe; + return KBKeyTypeCustom; +} + +- (NSString *)kb_identifierForSymbol:(NSString *)symbol { + if (symbol.length == 0) { return nil; } + static NSDictionary *map = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + map = @{ + @"-": @"sym_minus", + @"/": @"sym_slash", + @":": @"sym_colon", + @";": @"sym_semicolon", + @"(": @"sym_paren_l", + @")": @"sym_paren_r", + @"¥": @"sym_money", + @"¥": @"sym_money", + @"&": @"sym_amp", + @"@": @"sym_at", + @"\"": @"sym_quote_double", + @"“": @"sym_quote_double", + @"”": @"sym_quote_double", + @".": @"sym_dot", + @",": @"sym_comma", + @"?": @"sym_question", + @"!": @"sym_exclam", + @"'": @"sym_quote_single", + @"‘": @"sym_quote_single", + @"’": @"sym_quote_single", + @"[": @"sym_bracket_l", + @"]": @"sym_bracket_r", + @"{": @"sym_brace_l", + @"}": @"sym_brace_r", + @"#": @"sym_hash", + @"%": @"sym_percent", + @"^": @"sym_caret", + @"*": @"sym_asterisk", + @"+": @"sym_plus", + @"=": @"sym_equal", + @"_": @"sym_underscore", + @"\\": @"sym_backslash", + @"|": @"sym_pipe", + @"~": @"sym_tilde", + @"<": @"sym_lt", + @">": @"sym_gt", + @"€": @"sym_euro", + @"$": @"sym_dollar", + @"·": @"sym_bullet" + }; + }); + return map[symbol]; +} + #pragma mark - Actions - (void)onKeyTapped:(KBKeyButton *)sender { diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index b37ba39..6373527 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -129,10 +129,6 @@ 0498BD8C2EE69E15006CC1D5 /* KBTagItemModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD8A2EE69E15006CC1D5 /* KBTagItemModel.m */; }; 0498BD8F2EE6A3BD006CC1D5 /* KBMyMainModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD8E2EE6A3BD006CC1D5 /* KBMyMainModel.m */; }; 0498BD902EE6A3BD006CC1D5 /* KBMyMainModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD8E2EE6A3BD006CC1D5 /* KBMyMainModel.m */; }; - A1F0C1D22FACAD0012345678 /* KBMaiPointReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1D12FACAD0012345678 /* KBMaiPointReporter.m */; }; - A1F0C1D32FACAD0012345678 /* KBMaiPointReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1D12FACAD0012345678 /* KBMaiPointReporter.m */; }; - A1F0C1C22FABCDEF12345678 /* KBInviteCodeModel.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1C12FABCDEF12345678 /* KBInviteCodeModel.m */; }; - A1F0C1C32FABCDEF12345678 /* KBInviteCodeModel.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1C12FABCDEF12345678 /* KBInviteCodeModel.m */; }; 0498BDDA2EE7ECEA006CC1D5 /* WJXEventSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BDD82EE7ECEA006CC1D5 /* WJXEventSource.m */; }; 0498BDDE2EE81508006CC1D5 /* KBShopVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BDDD2EE81508006CC1D5 /* KBShopVM.m */; }; 0498BDE12EEA87C9006CC1D5 /* KBShopStyleModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BDE02EEA87C8006CC1D5 /* KBShopStyleModel.m */; }; @@ -176,6 +172,7 @@ 04C6EADD2EAF8CEB0089C901 /* KBToolBar.m in Sources */ = {isa = PBXBuildFile; fileRef = 04C6EADC2EAF8CEB0089C901 /* KBToolBar.m */; }; 04D1F6B22EDFF10A00B12345 /* KBSkinInstallBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D1F6B12EDFF10A00B12345 /* KBSkinInstallBridge.m */; }; 04D1F6B32EDFF10A00B12345 /* KBSkinInstallBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D1F6B12EDFF10A00B12345 /* KBSkinInstallBridge.m */; }; + 04E161782F0FA7BD0022C23B /* normal_them.zip in Resources */ = {isa = PBXBuildFile; fileRef = 04E161772F0FA7BD0022C23B /* normal_them.zip */; }; 04FC95672EB0546C007BD342 /* KBKey.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95652EB0546C007BD342 /* KBKey.m */; }; 04FC956A2EB05497007BD342 /* KBKeyButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95692EB05497007BD342 /* KBKeyButton.m */; }; 04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC956C2EB054B7007BD342 /* KBKeyboardView.m */; }; @@ -205,6 +202,7 @@ 04FEDB032EFE000000123456 /* KBEmojiBottomBarView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FEDB022EFE000000123456 /* KBEmojiBottomBarView.m */; }; 04FEDC122F00010000999999 /* KBKeyboardSubscriptionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FEDC112F00010000999999 /* KBKeyboardSubscriptionView.m */; }; 04FEDC222F00020000999999 /* KBKeyboardSubscriptionProduct.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FEDC212F00020000999999 /* KBKeyboardSubscriptionProduct.m */; }; + 04FEDC252F10000100000001 /* KBKeyboardLayoutConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FEDC242F10000100000001 /* KBKeyboardLayoutConfig.m */; }; 04FEDC322F00030000999999 /* KBKeyboardSubscriptionFeatureItemView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FEDC312F00030000999999 /* KBKeyboardSubscriptionFeatureItemView.m */; }; 04FEDC422F00040000999999 /* KBKeyboardSubscriptionFeatureMarqueeView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FEDC412F00040000999999 /* KBKeyboardSubscriptionFeatureMarqueeView.m */; }; 05A1B2D12F5B1A2B3C4D5E60 /* KBSearchVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A1B2C52F5B1A2B3C4D5E60 /* KBSearchVM.m */; }; @@ -218,6 +216,7 @@ A1B2C3EA2F20000000000001 /* KBSuggestionEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3E72F20000000000001 /* KBSuggestionEngine.m */; }; A1B2C3EB2F20000000000001 /* KBSuggestionBarView.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3E92F20000000000001 /* KBSuggestionBarView.m */; }; A1B2C3ED2F20000000000001 /* kb_words.txt in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3EC2F20000000000001 /* kb_words.txt */; }; + A1B2C3F12F20000000000002 /* kb_keyboard_layout_config.json in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3F02F20000000000002 /* kb_keyboard_layout_config.json */; }; A1B2C3F42EB35A9900000001 /* KBFullAccessGuideView.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3F22EB35A9900000001 /* KBFullAccessGuideView.m */; }; A1B2C4002EB4A0A100000003 /* KBAuthManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C4002EB4A0A100000002 /* KBAuthManager.m */; }; A1B2C4002EB4A0A100000004 /* KBAuthManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C4002EB4A0A100000002 /* KBAuthManager.m */; }; @@ -232,6 +231,10 @@ A1F0C1B12F1234567890ABCD /* KBConsumptionRecord.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1A12F1234567890ABCD /* KBConsumptionRecord.m */; }; A1F0C1B22F1234567890ABCD /* KBConsumptionRecordCell.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1A32F1234567890ABCD /* KBConsumptionRecordCell.m */; }; A1F0C1B32F1234567890ABCD /* KBConsumptionRecordVC.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1A52F1234567890ABCD /* KBConsumptionRecordVC.m */; }; + A1F0C1C22FABCDEF12345678 /* KBInviteCodeModel.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1C12FABCDEF12345678 /* KBInviteCodeModel.m */; }; + A1F0C1C32FABCDEF12345678 /* KBInviteCodeModel.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1C12FABCDEF12345678 /* KBInviteCodeModel.m */; }; + A1F0C1D22FACAD0012345678 /* KBMaiPointReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1D12FACAD0012345678 /* KBMaiPointReporter.m */; }; + A1F0C1D32FACAD0012345678 /* KBMaiPointReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1D12FACAD0012345678 /* KBMaiPointReporter.m */; }; EB72B60040437E3C0A4890FC /* KBShopThemeDetailModel.m in Sources */ = {isa = PBXBuildFile; fileRef = B9F60894E529C3EDAF6BAC3D /* KBShopThemeDetailModel.m */; }; ECC9EE02174D86E8D792472F /* Pods_keyBoard.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 967065BB5230E43F293B3AF9 /* Pods_keyBoard.framework */; }; /* End PBXBuildFile section */ @@ -460,10 +463,6 @@ 0498BD8A2EE69E15006CC1D5 /* KBTagItemModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBTagItemModel.m; sourceTree = ""; }; 0498BD8D2EE6A3BD006CC1D5 /* KBMyMainModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMyMainModel.h; sourceTree = ""; }; 0498BD8E2EE6A3BD006CC1D5 /* KBMyMainModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMyMainModel.m; sourceTree = ""; }; - A1F0C1C02FABCDEF12345678 /* KBInviteCodeModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBInviteCodeModel.h; sourceTree = ""; }; - A1F0C1C12FABCDEF12345678 /* KBInviteCodeModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBInviteCodeModel.m; sourceTree = ""; }; - A1F0C1D02FACAD0012345678 /* KBMaiPointReporter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMaiPointReporter.h; sourceTree = ""; }; - A1F0C1D12FACAD0012345678 /* KBMaiPointReporter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMaiPointReporter.m; sourceTree = ""; }; 0498BDD72EE7ECEA006CC1D5 /* WJXEventSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WJXEventSource.h; sourceTree = ""; }; 0498BDD82EE7ECEA006CC1D5 /* WJXEventSource.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WJXEventSource.m; sourceTree = ""; }; 0498BDDC2EE81508006CC1D5 /* KBShopVM.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBShopVM.h; sourceTree = ""; }; @@ -542,6 +541,7 @@ 04C6EAE12EAF940F0089C901 /* KBPermissionViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBPermissionViewController.m; sourceTree = ""; }; 04D1F6B02EDFF10A00B12345 /* KBSkinInstallBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinInstallBridge.h; sourceTree = ""; }; 04D1F6B12EDFF10A00B12345 /* KBSkinInstallBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinInstallBridge.m; sourceTree = ""; }; + 04E161772F0FA7BD0022C23B /* normal_them.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = normal_them.zip; sourceTree = ""; }; 04FC953A2EAFAE56007BD342 /* KeyBoardPrefixHeader.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyBoardPrefixHeader.pch; sourceTree = ""; }; 04FC95642EB0546C007BD342 /* KBKey.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKey.h; sourceTree = ""; }; 04FC95652EB0546C007BD342 /* KBKey.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKey.m; sourceTree = ""; }; @@ -603,6 +603,8 @@ 04FEDC112F00010000999999 /* KBKeyboardSubscriptionView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardSubscriptionView.m; sourceTree = ""; }; 04FEDC202F00020000999999 /* KBKeyboardSubscriptionProduct.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardSubscriptionProduct.h; sourceTree = ""; }; 04FEDC212F00020000999999 /* KBKeyboardSubscriptionProduct.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardSubscriptionProduct.m; sourceTree = ""; }; + 04FEDC232F10000100000001 /* KBKeyboardLayoutConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardLayoutConfig.h; sourceTree = ""; }; + 04FEDC242F10000100000001 /* KBKeyboardLayoutConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardLayoutConfig.m; sourceTree = ""; }; 04FEDC302F00030000999999 /* KBKeyboardSubscriptionFeatureItemView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardSubscriptionFeatureItemView.h; sourceTree = ""; }; 04FEDC312F00030000999999 /* KBKeyboardSubscriptionFeatureItemView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardSubscriptionFeatureItemView.m; sourceTree = ""; }; 04FEDC402F00040000999999 /* KBKeyboardSubscriptionFeatureMarqueeView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardSubscriptionFeatureMarqueeView.h; sourceTree = ""; }; @@ -630,6 +632,7 @@ A1B2C3E82F20000000000001 /* KBSuggestionBarView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSuggestionBarView.h; sourceTree = ""; }; A1B2C3E92F20000000000001 /* KBSuggestionBarView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSuggestionBarView.m; sourceTree = ""; }; A1B2C3EC2F20000000000001 /* kb_words.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = kb_words.txt; sourceTree = ""; }; + A1B2C3F02F20000000000002 /* kb_keyboard_layout_config.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = kb_keyboard_layout_config.json; sourceTree = ""; }; A1B2C3F12EB35A9900000001 /* KBFullAccessGuideView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFullAccessGuideView.h; sourceTree = ""; }; A1B2C3F22EB35A9900000001 /* KBFullAccessGuideView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFullAccessGuideView.m; sourceTree = ""; }; A1B2C4002EB4A0A100000001 /* KBAuthManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAuthManager.h; sourceTree = ""; }; @@ -654,6 +657,10 @@ A1F0C1A32F1234567890ABCD /* KBConsumptionRecordCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBConsumptionRecordCell.m; sourceTree = ""; }; A1F0C1A42F1234567890ABCD /* KBConsumptionRecordVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBConsumptionRecordVC.h; sourceTree = ""; }; A1F0C1A52F1234567890ABCD /* KBConsumptionRecordVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBConsumptionRecordVC.m; sourceTree = ""; }; + A1F0C1C02FABCDEF12345678 /* KBInviteCodeModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBInviteCodeModel.h; sourceTree = ""; }; + A1F0C1C12FABCDEF12345678 /* KBInviteCodeModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBInviteCodeModel.m; sourceTree = ""; }; + A1F0C1D02FACAD0012345678 /* KBMaiPointReporter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMaiPointReporter.h; sourceTree = ""; }; + A1F0C1D12FACAD0012345678 /* KBMaiPointReporter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMaiPointReporter.m; sourceTree = ""; }; B12EC429812407B9F0E67565 /* Pods-CustomKeyboard.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CustomKeyboard.release.xcconfig"; path = "Target Support Files/Pods-CustomKeyboard/Pods-CustomKeyboard.release.xcconfig"; sourceTree = ""; }; B8CA018AB878499327504AAD /* Pods-CustomKeyboard.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CustomKeyboard.debug.xcconfig"; path = "Target Support Files/Pods-CustomKeyboard/Pods-CustomKeyboard.debug.xcconfig"; sourceTree = ""; }; B9F60894E529C3EDAF6BAC3D /* KBShopThemeDetailModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBShopThemeDetailModel.m; sourceTree = ""; }; @@ -685,7 +692,9 @@ 041007D02ECE010100D203BB /* Resource */ = { isa = PBXGroup; children = ( + 04E161772F0FA7BD0022C23B /* normal_them.zip */, A1B2C3EC2F20000000000001 /* kb_words.txt */, + A1B2C3F02F20000000000002 /* kb_keyboard_layout_config.json */, 0498BDF42EEC50EE006CC1D5 /* emoji_categories.json */, 041007D12ECE012000D203BB /* KBSkinIconMap.strings */, 041007D32ECE012500D203BB /* 002.zip */, @@ -1248,6 +1257,8 @@ 04FC95652EB0546C007BD342 /* KBKey.m */, 04FEDC202F00020000999999 /* KBKeyboardSubscriptionProduct.h */, 04FEDC212F00020000999999 /* KBKeyboardSubscriptionProduct.m */, + 04FEDC232F10000100000001 /* KBKeyboardLayoutConfig.h */, + 04FEDC242F10000100000001 /* KBKeyboardLayoutConfig.m */, ); path = Model; sourceTree = ""; @@ -1817,8 +1828,10 @@ files = ( 04A9FE202EB893F10020DB6D /* Localizable.strings in Resources */, 041007D42ECE012500D203BB /* 002.zip in Resources */, + 04E161782F0FA7BD0022C23B /* normal_them.zip in Resources */, 041007D22ECE012000D203BB /* KBSkinIconMap.strings in Resources */, A1B2C3ED2F20000000000001 /* kb_words.txt in Resources */, + A1B2C3F12F20000000000002 /* kb_keyboard_layout_config.json in Resources */, 04791FFB2ED5EAB8004E8522 /* fense.zip in Resources */, 0498BDF52EEC50EE006CC1D5 /* emoji_categories.json in Resources */, 04791FF72ED5B985004E8522 /* Christmas.zip in Resources */, @@ -1962,6 +1975,7 @@ A1F0C1C22FABCDEF12345678 /* KBInviteCodeModel.m in Sources */, A1F0C1D22FACAD0012345678 /* KBMaiPointReporter.m in Sources */, 04FEDC222F00020000999999 /* KBKeyboardSubscriptionProduct.m in Sources */, + 04FEDC252F10000100000001 /* KBKeyboardLayoutConfig.m in Sources */, 0450AA742EF013D000B6AF06 /* KBEmojiCollectionCell.m in Sources */, 550CB2630FA4A7B4B9782EFA /* KBMyTheme.m in Sources */, 0498BDDA2EE7ECEA006CC1D5 /* WJXEventSource.m in Sources */,