重构
This commit is contained in:
File diff suppressed because it is too large
Load Diff
27
CustomKeyboard/View/KBKeyboardView/KBKeyboardInputHandler.h
Normal file
27
CustomKeyboard/View/KBKeyboardView/KBKeyboardInputHandler.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// KBKeyboardInputHandler.h
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
// Key tap handling helper.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
@class KBKey;
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
typedef void (^KBKeyboardActionHandler)(void);
|
||||||
|
typedef void (^KBKeyboardKeyTapHandler)(KBKey *key);
|
||||||
|
|
||||||
|
@interface KBKeyboardInputHandler : NSObject
|
||||||
|
|
||||||
|
@property (nonatomic, copy, nullable) KBKeyboardActionHandler onToggleShift;
|
||||||
|
@property (nonatomic, copy, nullable) KBKeyboardActionHandler onToggleSymbols;
|
||||||
|
@property (nonatomic, copy, nullable) KBKeyboardKeyTapHandler onKeyTapped;
|
||||||
|
|
||||||
|
- (BOOL)handleKeyTap:(KBKey *)key;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
26
CustomKeyboard/View/KBKeyboardView/KBKeyboardInputHandler.m
Normal file
26
CustomKeyboard/View/KBKeyboardView/KBKeyboardInputHandler.m
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// KBKeyboardInputHandler.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBKeyboardInputHandler.h"
|
||||||
|
#import "KBKey.h"
|
||||||
|
|
||||||
|
@implementation KBKeyboardInputHandler
|
||||||
|
|
||||||
|
- (BOOL)handleKeyTap:(KBKey *)key {
|
||||||
|
if (!key) { return NO; }
|
||||||
|
switch (key.type) {
|
||||||
|
case KBKeyTypeShift:
|
||||||
|
if (self.onToggleShift) { self.onToggleShift(); }
|
||||||
|
return YES;
|
||||||
|
case KBKeyTypeSymbolsToggle:
|
||||||
|
if (self.onToggleSymbols) { self.onToggleSymbols(); }
|
||||||
|
return YES;
|
||||||
|
default:
|
||||||
|
if (self.onKeyTapped) { self.onKeyTapped(key); }
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
//
|
||||||
|
// KBKeyboardInteractionHandler.h
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
@class KBKeyButton;
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface KBKeyboardInteractionHandler : NSObject
|
||||||
|
|
||||||
|
- (UIView *)resolveHitView:(UIView *)hitView
|
||||||
|
point:(CGPoint)point
|
||||||
|
container:(UIView *)container
|
||||||
|
rowViews:(NSArray<UIView *> *)rowViews;
|
||||||
|
|
||||||
|
- (NSArray<KBKeyButton *> *)collectKeyButtonsInView:(UIView *)view;
|
||||||
|
|
||||||
|
- (void)showPreviewForButton:(KBKeyButton *)button inContainer:(UIView *)container;
|
||||||
|
- (void)hidePreview;
|
||||||
|
- (void)bringPreviewToFrontIfNeededInContainer:(UIView *)container;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
//
|
||||||
|
// KBKeyboardInteractionHandler.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBKeyboardInteractionHandler.h"
|
||||||
|
#import "KBKeyButton.h"
|
||||||
|
#import "KBKey.h"
|
||||||
|
#import "KBKeyPreviewView.h"
|
||||||
|
|
||||||
|
static const NSTimeInterval kKBPreviewShowDuration = 0.08;
|
||||||
|
static const NSTimeInterval kKBPreviewHideDuration = 0.06;
|
||||||
|
|
||||||
|
@interface KBKeyboardInteractionHandler ()
|
||||||
|
@property (nonatomic, strong) KBKeyPreviewView *previewView;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation KBKeyboardInteractionHandler
|
||||||
|
|
||||||
|
- (UIView *)resolveHitView:(UIView *)hitView
|
||||||
|
point:(CGPoint)point
|
||||||
|
container:(UIView *)container
|
||||||
|
rowViews:(NSArray<UIView *> *)rowViews {
|
||||||
|
if ([hitView isKindOfClass:[KBKeyButton class]]) {
|
||||||
|
return hitView;
|
||||||
|
}
|
||||||
|
if ([self isHitInsideKeyRows:hitView rowViews:rowViews]) {
|
||||||
|
KBKeyButton *btn = [self nearestKeyButtonForPoint:point
|
||||||
|
container:container
|
||||||
|
rowViews:rowViews];
|
||||||
|
if (btn) { return btn; }
|
||||||
|
}
|
||||||
|
return hitView;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSArray<KBKeyButton *> *)collectKeyButtonsInView:(UIView *)view {
|
||||||
|
if (!view) { return @[]; }
|
||||||
|
NSMutableArray<KBKeyButton *> *buttons = [NSMutableArray array];
|
||||||
|
[self collectKeyButtonsInView:view into:buttons];
|
||||||
|
return buttons.copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)showPreviewForButton:(KBKeyButton *)button inContainer:(UIView *)container {
|
||||||
|
if (!button || !container) { return; }
|
||||||
|
KBKey *key = button.key;
|
||||||
|
if (key.type != KBKeyTypeCharacter) return;
|
||||||
|
|
||||||
|
if (!self.previewView) {
|
||||||
|
self.previewView = [[KBKeyPreviewView alloc] initWithFrame:CGRectZero];
|
||||||
|
self.previewView.hidden = YES;
|
||||||
|
[container addSubview:self.previewView];
|
||||||
|
} else if (self.previewView.superview != container) {
|
||||||
|
[container addSubview:self.previewView];
|
||||||
|
}
|
||||||
|
|
||||||
|
[self.previewView configureWithKey:key icon:button.iconView.image];
|
||||||
|
|
||||||
|
// 计算预览视图位置:在按钮上方稍微偏上
|
||||||
|
CGRect btnFrameInSelf = [button convertRect:button.bounds toView:container];
|
||||||
|
CGFloat previewWidth = 42;
|
||||||
|
CGFloat previewHeight = CGRectGetHeight(btnFrameInSelf) * 1.2;
|
||||||
|
CGFloat centerX = CGRectGetMidX(btnFrameInSelf);
|
||||||
|
CGFloat centerY = CGRectGetMinY(btnFrameInSelf) - previewHeight * 0.6;
|
||||||
|
|
||||||
|
self.previewView.frame = CGRectMake(0, 0, previewWidth, previewHeight);
|
||||||
|
self.previewView.center = CGPointMake(centerX, centerY);
|
||||||
|
self.previewView.alpha = 0.0;
|
||||||
|
self.previewView.hidden = NO;
|
||||||
|
|
||||||
|
[UIView animateWithDuration:kKBPreviewShowDuration
|
||||||
|
delay:0
|
||||||
|
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseOut
|
||||||
|
animations:^{
|
||||||
|
self.previewView.alpha = 1.0;
|
||||||
|
}
|
||||||
|
completion:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)hidePreview {
|
||||||
|
if (!self.previewView || self.previewView.isHidden) return;
|
||||||
|
[UIView animateWithDuration:kKBPreviewHideDuration
|
||||||
|
delay:0
|
||||||
|
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn
|
||||||
|
animations:^{
|
||||||
|
self.previewView.alpha = 0.0;
|
||||||
|
}
|
||||||
|
completion:^(BOOL finished) {
|
||||||
|
self.previewView.hidden = YES;
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)bringPreviewToFrontIfNeededInContainer:(UIView *)container {
|
||||||
|
if (!container) { return; }
|
||||||
|
if (self.previewView && self.previewView.superview == container) {
|
||||||
|
[container bringSubviewToFront:self.previewView];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Private
|
||||||
|
|
||||||
|
- (BOOL)isHitInsideKeyRows:(UIView *)hitView rowViews:(NSArray<UIView *> *)rowViews {
|
||||||
|
if (!hitView) { return NO; }
|
||||||
|
if ([rowViews containsObject:hitView]) { return YES; }
|
||||||
|
for (UIView *row in rowViews) {
|
||||||
|
if ([hitView isDescendantOfView:row]) { return YES; }
|
||||||
|
}
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (KBKeyButton *)nearestKeyButtonForPoint:(CGPoint)point
|
||||||
|
container:(UIView *)container
|
||||||
|
rowViews:(NSArray<UIView *> *)rowViews {
|
||||||
|
if (!container) { return nil; }
|
||||||
|
KBKeyButton *best = nil;
|
||||||
|
CGFloat bestDistance = CGFLOAT_MAX;
|
||||||
|
|
||||||
|
UIView *targetRow = nil;
|
||||||
|
for (UIView *row in rowViews) {
|
||||||
|
CGRect rowFrame = [container convertRect:row.bounds fromView:row];
|
||||||
|
if (CGRectContainsPoint(rowFrame, point)) {
|
||||||
|
targetRow = row;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSArray<UIView *> *candidateRows = targetRow ? @[targetRow] : rowViews;
|
||||||
|
for (UIView *row in candidateRows) {
|
||||||
|
NSArray<KBKeyButton *> *buttons = [self collectKeyButtonsInView:row];
|
||||||
|
for (KBKeyButton *btn in buttons) {
|
||||||
|
CGRect frame = [container convertRect:btn.frame fromView:btn.superview];
|
||||||
|
CGFloat dx = point.x - CGRectGetMidX(frame);
|
||||||
|
CGFloat dy = point.y - CGRectGetMidY(frame);
|
||||||
|
CGFloat dist = (dx * dx) + (dy * dy);
|
||||||
|
if (dist < bestDistance) {
|
||||||
|
bestDistance = dist;
|
||||||
|
best = btn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)collectKeyButtonsInView:(UIView *)view
|
||||||
|
into:(NSMutableArray<KBKeyButton *> *)buttons {
|
||||||
|
for (UIView *sub in view.subviews) {
|
||||||
|
if ([sub isKindOfClass:[KBKeyButton class]]) {
|
||||||
|
[buttons addObject:(KBKeyButton *)sub];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (sub.subviews.count > 0) {
|
||||||
|
[self collectKeyButtonsInView:sub into:buttons];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
28
CustomKeyboard/View/KBKeyboardView/KBKeyboardKeyFactory.h
Normal file
28
CustomKeyboard/View/KBKeyboardView/KBKeyboardKeyFactory.h
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
//
|
||||||
|
// KBKeyboardKeyFactory.h
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
// Key creation helper for keyboard layouts.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
@class KBKeyboardLayoutConfig;
|
||||||
|
@class KBKeyboardKeyDef;
|
||||||
|
@class KBKey;
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface KBKeyboardKeyFactory : NSObject
|
||||||
|
|
||||||
|
- (instancetype)initWithLayoutConfig:(KBKeyboardLayoutConfig *)layoutConfig;
|
||||||
|
|
||||||
|
- (nullable KBKey *)keyForItemId:(NSString *)itemId shiftOn:(BOOL)shiftOn;
|
||||||
|
- (nullable KBKey *)keyFromDef:(KBKeyboardKeyDef *)def
|
||||||
|
identifier:(NSString *)identifier
|
||||||
|
shiftOn:(BOOL)shiftOn;
|
||||||
|
- (nullable KBKey *)letterKeyWithChar:(NSString *)charString shiftOn:(BOOL)shiftOn;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
186
CustomKeyboard/View/KBKeyboardView/KBKeyboardKeyFactory.m
Normal file
186
CustomKeyboard/View/KBKeyboardView/KBKeyboardKeyFactory.m
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
//
|
||||||
|
// KBKeyboardKeyFactory.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBKeyboardKeyFactory.h"
|
||||||
|
#import "KBKeyboardLayoutConfig.h"
|
||||||
|
#import "KBKey.h"
|
||||||
|
|
||||||
|
@interface KBKeyboardKeyFactory ()
|
||||||
|
@property (nonatomic, strong) KBKeyboardLayoutConfig *layoutConfig;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation KBKeyboardKeyFactory
|
||||||
|
|
||||||
|
- (instancetype)initWithLayoutConfig:(KBKeyboardLayoutConfig *)layoutConfig {
|
||||||
|
self = [super init];
|
||||||
|
if (self) {
|
||||||
|
_layoutConfig = layoutConfig;
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (KBKey *)keyForItemId:(NSString *)itemId shiftOn:(BOOL)shiftOn {
|
||||||
|
if (itemId.length == 0) { return nil; }
|
||||||
|
KBKeyboardKeyDef *def = [self.layoutConfig keyDefForIdentifier:itemId];
|
||||||
|
if (def) {
|
||||||
|
return [self keyFromDef:def identifier:itemId shiftOn:shiftOn];
|
||||||
|
}
|
||||||
|
|
||||||
|
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 letterKeyWithChar:value shiftOn:shiftOn];
|
||||||
|
}
|
||||||
|
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 *)keyFromDef:(KBKeyboardKeyDef *)def
|
||||||
|
identifier:(NSString *)identifier
|
||||||
|
shiftOn:(BOOL)shiftOn {
|
||||||
|
KBKeyType type = [self kb_keyTypeForDef:def];
|
||||||
|
NSString *title = def.title ?: @"";
|
||||||
|
if (type == KBKeyTypeShift && 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];
|
||||||
|
if (type == KBKeyTypeShift) {
|
||||||
|
k.caseVariant = shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower;
|
||||||
|
} else {
|
||||||
|
k.caseVariant = KBKeyCaseVariantNone;
|
||||||
|
}
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (KBKey *)letterKeyWithChar:(NSString *)charString shiftOn:(BOOL)shiftOn {
|
||||||
|
if (charString.length == 0) { return nil; }
|
||||||
|
NSString *lower = charString.lowercaseString;
|
||||||
|
NSString *upper = charString.uppercaseString;
|
||||||
|
|
||||||
|
NSString *shown = shiftOn ? upper : lower;
|
||||||
|
NSString *identifier = [NSString stringWithFormat:@"letter_%@", lower];
|
||||||
|
|
||||||
|
KBKey *k = [KBKey keyWithIdentifier:identifier
|
||||||
|
title:shown
|
||||||
|
output:shown
|
||||||
|
type:KBKeyTypeCharacter];
|
||||||
|
k.caseVariant = shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower;
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Private
|
||||||
|
|
||||||
|
- (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<NSString *, NSString *> *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_chinese_dot",
|
||||||
|
@",": @"sym_comma",
|
||||||
|
@"、": @"sym_dun",
|
||||||
|
@"?": @"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_corner_l",
|
||||||
|
@"」": @"sym_corner_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_pound",
|
||||||
|
@"·": @"sym_bullet",
|
||||||
|
@"^_^": @"sym_face",
|
||||||
|
@"—": @"sym_emdash",
|
||||||
|
@"«": @"sym_guillemet_l",
|
||||||
|
@"»": @"sym_guillemet_r",
|
||||||
|
@"《": @"sym_book_title_l",
|
||||||
|
@"》": @"sym_book_title_r",
|
||||||
|
@"...": @"sym_ellipsis"
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return map[symbol];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
42
CustomKeyboard/View/KBKeyboardView/KBKeyboardLayoutEngine.h
Normal file
42
CustomKeyboard/View/KBKeyboardView/KBKeyboardLayoutEngine.h
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
//
|
||||||
|
// KBKeyboardLayoutEngine.h
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
// Layout metrics calculation helper.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
@class KBKeyboardLayoutConfig;
|
||||||
|
@class KBKeyboardLayout;
|
||||||
|
@class KBKeyboardRowConfig;
|
||||||
|
@class KBKeyboardRowItem;
|
||||||
|
@class KBKeyboardKeyFactory;
|
||||||
|
@class KBKey;
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface KBKeyboardLayoutEngine : NSObject
|
||||||
|
|
||||||
|
- (instancetype)initWithLayoutConfig:(KBKeyboardLayoutConfig *)layoutConfig;
|
||||||
|
|
||||||
|
- (CGFloat)rowSpacingForLayout:(KBKeyboardLayout *)layout;
|
||||||
|
- (CGFloat)topInsetForLayout:(KBKeyboardLayout *)layout;
|
||||||
|
- (CGFloat)bottomInsetForLayout:(KBKeyboardLayout *)layout;
|
||||||
|
|
||||||
|
- (CGFloat)rowHeightForRow:(KBKeyboardRowConfig *)row;
|
||||||
|
- (CGFloat)gapForRow:(KBKeyboardRowConfig *)row;
|
||||||
|
- (CGFloat)insetLeftForRow:(KBKeyboardRowConfig *)row;
|
||||||
|
- (CGFloat)insetRightForRow:(KBKeyboardRowConfig *)row;
|
||||||
|
|
||||||
|
- (CGFloat)widthForItem:(KBKeyboardRowItem *)item key:(KBKey *)key;
|
||||||
|
- (CGFloat)fontSizeForItem:(KBKeyboardRowItem *)item key:(KBKey *)key;
|
||||||
|
- (CGFloat)fontSizeForFontKey:(NSString *)fontKey;
|
||||||
|
|
||||||
|
- (CGFloat)calculateUniformCharKeyWidthForRows:(NSArray<KBKeyboardRowConfig *> *)rows
|
||||||
|
keyFactory:(KBKeyboardKeyFactory *)keyFactory
|
||||||
|
shiftOn:(BOOL)shiftOn;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
245
CustomKeyboard/View/KBKeyboardView/KBKeyboardLayoutEngine.m
Normal file
245
CustomKeyboard/View/KBKeyboardView/KBKeyboardLayoutEngine.m
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
//
|
||||||
|
// 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<KBKeyboardRowConfig *> *)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<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 = [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
|
||||||
28
CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyBuilder.h
Normal file
28
CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyBuilder.h
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
//
|
||||||
|
// KBKeyboardLegacyBuilder.h
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
// Legacy layout builder (non-config layout).
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
@class KBKey;
|
||||||
|
@class KBBackspaceLongPressHandler;
|
||||||
|
@class UIView;
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface KBKeyboardLegacyBuilder : NSObject
|
||||||
|
|
||||||
|
- (void)buildRow:(UIView *)row
|
||||||
|
withKeys:(NSArray<KBKey *> *)keys
|
||||||
|
edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier
|
||||||
|
shiftOn:(BOOL)shiftOn
|
||||||
|
backspaceHandler:(KBBackspaceLongPressHandler *)backspaceHandler
|
||||||
|
target:(id)target
|
||||||
|
action:(SEL)action;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
291
CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyBuilder.m
Normal file
291
CustomKeyboard/View/KBKeyboardView/KBKeyboardLegacyBuilder.m
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
//
|
||||||
|
// KBKeyboardLegacyBuilder.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBKeyboardLegacyBuilder.h"
|
||||||
|
#import "KBKey.h"
|
||||||
|
#import "KBKeyButton.h"
|
||||||
|
#import "KBBackspaceLongPressHandler.h"
|
||||||
|
#import "KBConfig.h"
|
||||||
|
#import <Masonry/Masonry.h>
|
||||||
|
|
||||||
|
static const CGFloat kKBSpecialKeySquareMultiplier = 1.2;
|
||||||
|
static const CGFloat kKBReturnWidthMultiplier = 2.4;
|
||||||
|
static const CGFloat kKBSpaceWidthMultiplier = 3.0;
|
||||||
|
static inline CGFloat KBLegacyRowHorizontalInset(void) {
|
||||||
|
return KBFit(6.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@implementation KBKeyboardLegacyBuilder
|
||||||
|
|
||||||
|
- (void)buildRow:(UIView *)row
|
||||||
|
withKeys:(NSArray<KBKey *> *)keys
|
||||||
|
edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier
|
||||||
|
shiftOn:(BOOL)shiftOn
|
||||||
|
backspaceHandler:(KBBackspaceLongPressHandler *)backspaceHandler
|
||||||
|
target:(id)target
|
||||||
|
action:(SEL)action {
|
||||||
|
if (!row || keys.count == 0) { return; }
|
||||||
|
|
||||||
|
// 第 4 行(底部控制行)使用单独的布局规则:
|
||||||
|
// 123/ABC、Emoji、Send 给定尺寸,Space 自动吃掉剩余宽度。
|
||||||
|
BOOL isBottomControlRow = [self kb_isBottomControlRowWithKeys:keys];
|
||||||
|
|
||||||
|
CGFloat spacing = 0; // 键与键之间的间距
|
||||||
|
UIView *previous = nil;
|
||||||
|
UIView *leftSpacer = nil;
|
||||||
|
UIView *rightSpacer = nil;
|
||||||
|
|
||||||
|
if (edgeSpacerMultiplier > 0.0) {
|
||||||
|
leftSpacer = [UIView new];
|
||||||
|
rightSpacer = [UIView new];
|
||||||
|
leftSpacer.backgroundColor = [UIColor clearColor];
|
||||||
|
rightSpacer.backgroundColor = [UIColor clearColor];
|
||||||
|
[row addSubview:leftSpacer];
|
||||||
|
[row addSubview:rightSpacer];
|
||||||
|
[leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.left.equalTo(row.mas_left).offset(KBLegacyRowHorizontalInset());
|
||||||
|
make.centerY.equalTo(row);
|
||||||
|
make.height.mas_equalTo(1);
|
||||||
|
}];
|
||||||
|
[rightSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.right.equalTo(row.mas_right).offset(-KBLegacyRowHorizontalInset());
|
||||||
|
make.centerY.equalTo(row);
|
||||||
|
make.height.mas_equalTo(1);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (NSInteger i = 0; i < keys.count; i++) {
|
||||||
|
KBKey *key = keys[i];
|
||||||
|
KBKeyButton *btn = [[KBKeyButton alloc] init];
|
||||||
|
btn.key = key;
|
||||||
|
[btn setTitle:key.title forState:UIControlStateNormal];
|
||||||
|
// 在设置完标题后,按当前皮肤应用图标与文字显隐
|
||||||
|
[btn applyThemeForCurrentKey];
|
||||||
|
if (target && action) {
|
||||||
|
[btn addTarget:target action:action forControlEvents:UIControlEventTouchDown];
|
||||||
|
}
|
||||||
|
[row addSubview:btn];
|
||||||
|
|
||||||
|
if (key.type == KBKeyTypeBackspace) {
|
||||||
|
[backspaceHandler bindDeleteButton:btn showClearLabel:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift 按钮选中态随大小写状态变化
|
||||||
|
if (key.type == KBKeyTypeShift) {
|
||||||
|
btn.selected = shiftOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.top.bottom.equalTo(row);
|
||||||
|
if (previous) {
|
||||||
|
make.left.equalTo(previous.mas_right).offset(spacing);
|
||||||
|
} else {
|
||||||
|
if (leftSpacer) {
|
||||||
|
make.left.equalTo(leftSpacer.mas_right).offset(spacing);
|
||||||
|
} else {
|
||||||
|
make.left.equalTo(row.mas_left).offset(KBLegacyRowHorizontalInset());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 字符键:等宽
|
||||||
|
if (key.type == KBKeyTypeCharacter) {
|
||||||
|
if (previous && [previous isKindOfClass:[KBKeyButton class]]) {
|
||||||
|
KBKeyButton *prevBtn = (KBKeyButton *)previous;
|
||||||
|
if (prevBtn.key.type == KBKeyTypeCharacter) {
|
||||||
|
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.width.equalTo(previous);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// special keys: 宽度在第二遍统一设置
|
||||||
|
}
|
||||||
|
|
||||||
|
previous = btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧使用内边距或右占位
|
||||||
|
if (previous) {
|
||||||
|
[previous mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
if (rightSpacer) {
|
||||||
|
make.right.equalTo(rightSpacer.mas_left).offset(-spacing);
|
||||||
|
} else {
|
||||||
|
make.right.equalTo(row.mas_right).offset(-KBLegacyRowHorizontalInset());
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部控制行:在第一轮已完成左右约束的前提下,仅给 123/ABC、Emoji、Send 指定宽度,
|
||||||
|
// Space 不加宽度约束,让其自动填充剩余空间。
|
||||||
|
if (isBottomControlRow) {
|
||||||
|
[self kb_applyBottomControlRowWidthInRow:row];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第二遍:以首个字符键为基准,统一设置特殊键宽度倍数
|
||||||
|
KBKeyButton *firstChar = nil;
|
||||||
|
BOOL hasCharacterInRow = NO;
|
||||||
|
for (UIView *v in row.subviews) {
|
||||||
|
if (![v isKindOfClass:[KBKeyButton class]]) continue;
|
||||||
|
KBKeyButton *b = (KBKeyButton *)v;
|
||||||
|
if (b.key.type == KBKeyTypeCharacter) {
|
||||||
|
firstChar = b;
|
||||||
|
hasCharacterInRow = YES;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 若该行没有字符键(例如底部控制行之外的特殊行),则使用行内第一个按钮作为基准宽度
|
||||||
|
if (!firstChar) {
|
||||||
|
for (UIView *v in row.subviews) {
|
||||||
|
if ([v isKindOfClass:[KBKeyButton class]]) {
|
||||||
|
firstChar = (KBKeyButton *)v;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (firstChar) {
|
||||||
|
// 如果该行本身没有字符键(如底部控制行),且基准按钮是 123/ABC/#+= 等,
|
||||||
|
// 也将其约束为 1:1,避免 123/ABC 不是正方形。
|
||||||
|
if (!hasCharacterInRow &&
|
||||||
|
(firstChar.key.type == KBKeyTypeModeChange ||
|
||||||
|
firstChar.key.type == KBKeyTypeSymbolsToggle ||
|
||||||
|
firstChar.key.type == KBKeyTypeCustom)) {
|
||||||
|
[firstChar mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.width.equalTo(firstChar.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (UIView *v in row.subviews) {
|
||||||
|
if (![v isKindOfClass:[KBKeyButton class]]) continue;
|
||||||
|
KBKeyButton *b = (KBKeyButton *)v;
|
||||||
|
|
||||||
|
// 避免对基准按钮自身添加 self == self * k 的无效约束
|
||||||
|
if (b == firstChar) continue;
|
||||||
|
if (b.key.type == KBKeyTypeCharacter) continue;
|
||||||
|
|
||||||
|
BOOL isBottomModeKey = (b.key.type == KBKeyTypeModeChange) ||
|
||||||
|
(b.key.type == KBKeyTypeSymbolsToggle) ||
|
||||||
|
(b.key.type == KBKeyTypeCustom);
|
||||||
|
// 一类键强制近似正方形(宽 ~ 高)
|
||||||
|
if (b.key.type == KBKeyTypeShift ||
|
||||||
|
b.key.type == KBKeyTypeBackspace ||
|
||||||
|
isBottomModeKey) {
|
||||||
|
[b mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.width.equalTo(b.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier);
|
||||||
|
}];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CGFloat multiplier = 1.5;
|
||||||
|
|
||||||
|
// Space:宽度更大
|
||||||
|
if (b.key.type == KBKeyTypeSpace) {
|
||||||
|
multiplier = kKBSpaceWidthMultiplier;
|
||||||
|
}
|
||||||
|
// Send 按钮:宽度为基准键的 2.4 倍
|
||||||
|
else if (b.key.type == KBKeyTypeReturn) {
|
||||||
|
multiplier = kKBReturnWidthMultiplier;
|
||||||
|
}
|
||||||
|
// 其它特殊键(如 Globe)保持适度放大
|
||||||
|
else if (b.key.type == KBKeyTypeGlobe) {
|
||||||
|
multiplier = 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
[b mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.width.equalTo(firstChar).multipliedBy(multiplier);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有左右占位,则把占位宽度设置为字符键宽度的一定倍数,以实现整体居中;
|
||||||
|
// 同时强约束左右占位宽度相等,避免在某些系统上由于布局冲突导致只压缩一侧,
|
||||||
|
// 出现“左侧有空隙,右侧无空隙”的情况。
|
||||||
|
if (leftSpacer && rightSpacer) {
|
||||||
|
// 1) 左右占位宽度必须相等
|
||||||
|
[leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.width.equalTo(rightSpacer);
|
||||||
|
}];
|
||||||
|
// 2) 同时都接近字符键宽度的 edgeSpacerMultiplier 倍数
|
||||||
|
[rightSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.width.equalTo(firstChar).multipliedBy(edgeSpacerMultiplier);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Row Helpers (Bottom Control Row)
|
||||||
|
|
||||||
|
// 判断是否为底部控制行:包含 Space + Return,且有 ModeChange/SymbolsToggle,
|
||||||
|
// 并且不再含字符键。
|
||||||
|
- (BOOL)kb_isBottomControlRowWithKeys:(NSArray<KBKey *> *)keys {
|
||||||
|
BOOL hasSpace = NO;
|
||||||
|
BOOL hasReturn = NO;
|
||||||
|
BOOL hasModeOrSymbols = NO;
|
||||||
|
BOOL hasCharacters = NO;
|
||||||
|
|
||||||
|
for (KBKey *k in keys) {
|
||||||
|
if (k.type == KBKeyTypeSpace) {
|
||||||
|
hasSpace = YES;
|
||||||
|
} else if (k.type == KBKeyTypeReturn) {
|
||||||
|
hasReturn = YES;
|
||||||
|
} else if (k.type == KBKeyTypeModeChange || k.type == KBKeyTypeSymbolsToggle) {
|
||||||
|
hasModeOrSymbols = YES;
|
||||||
|
} else if (k.type == KBKeyTypeCharacter) {
|
||||||
|
hasCharacters = YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (hasSpace && hasReturn && hasModeOrSymbols && !hasCharacters);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_applyBottomControlRowWidthInRow:(UIView *)row {
|
||||||
|
if (!row) { return; }
|
||||||
|
|
||||||
|
KBKeyButton *modeBtn = nil;
|
||||||
|
KBKeyButton *spaceBtn = nil;
|
||||||
|
KBKeyButton *retBtn = nil;
|
||||||
|
NSMutableArray<KBKeyButton *> *customButtons = [NSMutableArray array];
|
||||||
|
|
||||||
|
for (UIView *v in row.subviews) {
|
||||||
|
if (![v isKindOfClass:[KBKeyButton class]]) continue;
|
||||||
|
KBKeyButton *b = (KBKeyButton *)v;
|
||||||
|
switch (b.key.type) {
|
||||||
|
case KBKeyTypeModeChange:
|
||||||
|
case KBKeyTypeSymbolsToggle:
|
||||||
|
modeBtn = b;
|
||||||
|
break;
|
||||||
|
case KBKeyTypeCustom:
|
||||||
|
[customButtons addObject:b];
|
||||||
|
break;
|
||||||
|
case KBKeyTypeSpace:
|
||||||
|
spaceBtn = b;
|
||||||
|
break;
|
||||||
|
case KBKeyTypeReturn:
|
||||||
|
retBtn = b;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!modeBtn || customButtons.count == 0 || !spaceBtn || !retBtn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[modeBtn mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.width.equalTo(row.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier);
|
||||||
|
}];
|
||||||
|
for (KBKeyButton *custom in customButtons) {
|
||||||
|
[custom mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.width.equalTo(row.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
[retBtn mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.width.equalTo(modeBtn.mas_width).multipliedBy(2.0);
|
||||||
|
}];
|
||||||
|
// Space 不设置宽度;通过此前已建立的左右约束自动占满剩余宽度。
|
||||||
|
(void)spaceBtn;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// KBKeyboardLegacyLayoutProvider.h
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
@class KBKey;
|
||||||
|
@class KBKeyboardKeyFactory;
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface KBKeyboardLegacyLayoutProvider : NSObject
|
||||||
|
|
||||||
|
- (NSArray<NSArray<KBKey *> *> *)keysForLayoutStyleIsNumbers:(BOOL)isNumbersLayout
|
||||||
|
shiftOn:(BOOL)shiftOn
|
||||||
|
symbolsMoreOn:(BOOL)symbolsMoreOn
|
||||||
|
keyFactory:(KBKeyboardKeyFactory *)keyFactory;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
//
|
||||||
|
// KBKeyboardLegacyLayoutProvider.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBKeyboardLegacyLayoutProvider.h"
|
||||||
|
#import "KBKeyboardKeyFactory.h"
|
||||||
|
#import "KBKey.h"
|
||||||
|
|
||||||
|
@implementation KBKeyboardLegacyLayoutProvider
|
||||||
|
|
||||||
|
- (NSArray<NSArray<KBKey *> *> *)keysForLayoutStyleIsNumbers:(BOOL)isNumbersLayout
|
||||||
|
shiftOn:(BOOL)shiftOn
|
||||||
|
symbolsMoreOn:(BOOL)symbolsMoreOn
|
||||||
|
keyFactory:(KBKeyboardKeyFactory *)keyFactory {
|
||||||
|
if (isNumbersLayout) {
|
||||||
|
return [self buildKeysForNumbersLayoutWithSymbolsMoreOn:symbolsMoreOn];
|
||||||
|
}
|
||||||
|
return [self buildKeysForLettersLayoutWithShiftOn:shiftOn keyFactory:keyFactory];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Letters Layout
|
||||||
|
|
||||||
|
- (NSArray<NSArray<KBKey *> *> *)buildKeysForLettersLayoutWithShiftOn:(BOOL)shiftOn
|
||||||
|
keyFactory:(KBKeyboardKeyFactory *)keyFactory {
|
||||||
|
// 字母布局(QWERTY)
|
||||||
|
NSArray *r1Letters = @[ @"q", @"w", @"e", @"r", @"t", @"y", @"u", @"i", @"o", @"p" ];
|
||||||
|
NSArray *r2Letters = @[ @"a", @"s", @"d", @"f", @"g", @"h", @"j", @"k", @"l" ];
|
||||||
|
NSArray *r3Letters = @[ @"z", @"x", @"c", @"v", @"b", @"n", @"m" ];
|
||||||
|
|
||||||
|
NSMutableArray *row1 = [NSMutableArray arrayWithCapacity:r1Letters.count];
|
||||||
|
for (NSString *s in r1Letters) {
|
||||||
|
KBKey *key = [keyFactory letterKeyWithChar:s shiftOn:shiftOn];
|
||||||
|
if (key) { [row1 addObject:key]; }
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableArray *row2 = [NSMutableArray arrayWithCapacity:r2Letters.count];
|
||||||
|
for (NSString *s in r2Letters) {
|
||||||
|
KBKey *key = [keyFactory letterKeyWithChar:s shiftOn:shiftOn];
|
||||||
|
if (key) { [row2 addObject:key]; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第三行:Shift + Z...M + Backspace
|
||||||
|
NSMutableArray *row3 = [NSMutableArray array];
|
||||||
|
KBKey *shift = [KBKey keyWithIdentifier:@"shift"
|
||||||
|
title:@"⇧"
|
||||||
|
output:@""
|
||||||
|
type:KBKeyTypeShift];
|
||||||
|
// Shift 键也支持大小写两套皮肤图
|
||||||
|
shift.caseVariant = shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower;
|
||||||
|
[row3 addObject:shift];
|
||||||
|
|
||||||
|
for (NSString *s in r3Letters) {
|
||||||
|
KBKey *key = [keyFactory letterKeyWithChar:s shiftOn:shiftOn];
|
||||||
|
if (key) { [row3 addObject:key]; }
|
||||||
|
}
|
||||||
|
|
||||||
|
KBKey *backspace = [KBKey keyWithIdentifier:@"backspace"
|
||||||
|
title:@"⌫"
|
||||||
|
output:@""
|
||||||
|
type:KBKeyTypeBackspace];
|
||||||
|
[row3 addObject:backspace];
|
||||||
|
|
||||||
|
NSArray *row4 = [self bottomControlRowKeysForLettersLayout];
|
||||||
|
|
||||||
|
return @[row1.copy, row2.copy, row3.copy, row4];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Numbers / Symbols Layout
|
||||||
|
|
||||||
|
- (NSArray<NSArray<KBKey *> *> *)buildKeysForNumbersLayoutWithSymbolsMoreOn:(BOOL)symbolsMoreOn {
|
||||||
|
// 数字/符号布局:3 行主键 + 底部控制行
|
||||||
|
NSArray *r1 = nil;
|
||||||
|
NSArray *r2 = nil;
|
||||||
|
NSArray *r3 = nil;
|
||||||
|
|
||||||
|
if (!symbolsMoreOn) {
|
||||||
|
// 数字第一页(123)
|
||||||
|
r1 = @[ [KBKey keyWithIdentifier:@"digit_1" title:@"1" output:@"1" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"digit_2" title:@"2" output:@"2" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"digit_3" title:@"3" output:@"3" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"digit_4" title:@"4" output:@"4" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"digit_5" title:@"5" output:@"5" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"digit_6" title:@"6" output:@"6" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"digit_7" title:@"7" output:@"7" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"digit_8" title:@"8" output:@"8" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"digit_9" title:@"9" output:@"9" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"digit_0" title:@"0" output:@"0" type:KBKeyTypeCharacter] ];
|
||||||
|
r2 = @[ [KBKey keyWithIdentifier:@"sym_minus" title:@"-" output:@"-" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_slash" title:@"/" output:@"/" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_colon" title:@":" output:@":" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_semicolon" title:@";" output:@";" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_paren_l" title:@"(" output:@"(" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_paren_r" title:@")" output:@")" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_money" title:@"¥" output:@"¥" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_amp" title:@"&" output:@"&" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_at" title:@"@" output:@"@" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_quote_double" title:@"\"" output:@"\"" type:KBKeyTypeCharacter] ];
|
||||||
|
|
||||||
|
r3 = [self symbolsCommonThirdRowWithToggleIsMore:NO];
|
||||||
|
} else {
|
||||||
|
// 数字第二页(#+=):前两行替换为更多符号,左下角按钮文案改为“123”
|
||||||
|
r1 = @[ [KBKey keyWithIdentifier:@"sym_bracket_l" title:@"[" output:@"[" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_bracket_r" title:@"]" output:@"]" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_brace_l" title:@"{" output:@"{" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_brace_r" title:@"}" output:@"}" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_hash" title:@"#" output:@"#" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_percent" title:@"%" output:@"%" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_caret" title:@"^" output:@"^" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_asterisk" title:@"*" output:@"*" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_plus" title:@"+" output:@"+" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_equal" title:@"=" output:@"=" type:KBKeyTypeCharacter] ];
|
||||||
|
r2 = @[ [KBKey keyWithIdentifier:@"sym_underscore" title:@"_" output:@"_" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_backslash" title:@"\\" output:@"\\" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_pipe" title:@"|" output:@"|" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_tilde" title:@"~" output:@"~" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_lt" title:@"<" output:@"<" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_gt" title:@">" output:@">" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_dollar" title:@"$" output:@"$" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_euro" title:@"€" output:@"€" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_pound" title:@"£" output:@"£" type:KBKeyTypeCharacter],
|
||||||
|
[KBKey keyWithIdentifier:@"sym_bullet" title:@"•" output:@"•" type:KBKeyTypeCharacter] ];
|
||||||
|
|
||||||
|
r3 = [self symbolsCommonThirdRowWithToggleIsMore:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSArray *r4 = [self bottomControlRowKeysForNumbersLayout];
|
||||||
|
return @[r1, r2, r3, r4];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Key Factories
|
||||||
|
|
||||||
|
- (NSArray<KBKey *> *)symbolsCommonThirdRowWithToggleIsMore:(BOOL)isMorePage {
|
||||||
|
NSString *identifier = isMorePage ? @"symbols_toggle_123" : @"symbols_toggle_more";
|
||||||
|
NSString *title = isMorePage ? @"123" : @"#+=";
|
||||||
|
|
||||||
|
KBKey *toggle = [KBKey keyWithIdentifier:identifier
|
||||||
|
title:title
|
||||||
|
output:@""
|
||||||
|
type:KBKeyTypeSymbolsToggle];
|
||||||
|
KBKey *comma = [KBKey keyWithIdentifier:@"sym_comma" title:@"," output:@"," type:KBKeyTypeCharacter];
|
||||||
|
KBKey *dot = [KBKey keyWithIdentifier:@"sym_dot" title:@"." output:@"." type:KBKeyTypeCharacter];
|
||||||
|
KBKey *q = [KBKey keyWithIdentifier:@"sym_question" title:@"?" output:@"?" type:KBKeyTypeCharacter];
|
||||||
|
KBKey *ex = [KBKey keyWithIdentifier:@"sym_exclam" title:@"!" output:@"!" type:KBKeyTypeCharacter];
|
||||||
|
KBKey *quote = [KBKey keyWithIdentifier:@"sym_quote_single" title:@"'" output:@"'" type:KBKeyTypeCharacter];
|
||||||
|
KBKey *back = [KBKey keyWithIdentifier:@"backspace"
|
||||||
|
title:@"⌫"
|
||||||
|
output:@""
|
||||||
|
type:KBKeyTypeBackspace];
|
||||||
|
return @[ toggle, comma, dot, q, ex, quote, back ];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSArray<KBKey *> *)bottomControlRowKeysForLettersLayout {
|
||||||
|
KBKey *mode123 = [KBKey keyWithIdentifier:@"mode_123"
|
||||||
|
title:@"123"
|
||||||
|
output:@""
|
||||||
|
type:KBKeyTypeModeChange];
|
||||||
|
KBKey *emoji = [KBKey keyWithIdentifier:KBKeyIdentifierEmojiPanel
|
||||||
|
title:@"😊"
|
||||||
|
output:@""
|
||||||
|
type:KBKeyTypeCustom];
|
||||||
|
KBKey *space = [KBKey keyWithIdentifier:@"space"
|
||||||
|
title:@"space"
|
||||||
|
output:@" "
|
||||||
|
type:KBKeyTypeSpace];
|
||||||
|
KBKey *ret = [KBKey keyWithIdentifier:@"return"
|
||||||
|
title:KBLocalized(@"Send")
|
||||||
|
output:@"\n"
|
||||||
|
type:KBKeyTypeReturn];
|
||||||
|
return @[ mode123, emoji, space, ret ];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSArray<KBKey *> *)bottomControlRowKeysForNumbersLayout {
|
||||||
|
KBKey *modeABC = [KBKey keyWithIdentifier:@"mode_abc"
|
||||||
|
title:@"abc"
|
||||||
|
output:@""
|
||||||
|
type:KBKeyTypeModeChange];
|
||||||
|
KBKey *emoji = [KBKey keyWithIdentifier:KBKeyIdentifierEmojiPanel
|
||||||
|
title:@"😊"
|
||||||
|
output:@""
|
||||||
|
type:KBKeyTypeCustom];
|
||||||
|
KBKey *space = [KBKey keyWithIdentifier:@"space"
|
||||||
|
title:@"space"
|
||||||
|
output:@" "
|
||||||
|
type:KBKeyTypeSpace];
|
||||||
|
KBKey *ret = [KBKey keyWithIdentifier:@"return"
|
||||||
|
title:KBLocalized(@"Send")
|
||||||
|
output:@"\n"
|
||||||
|
type:KBKeyTypeReturn];
|
||||||
|
return @[ modeABC, emoji, space, ret ];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
33
CustomKeyboard/View/KBKeyboardView/KBKeyboardRowBuilder.h
Normal file
33
CustomKeyboard/View/KBKeyboardView/KBKeyboardRowBuilder.h
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
//
|
||||||
|
// KBKeyboardRowBuilder.h
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
@class KBKeyboardLayoutConfig;
|
||||||
|
@class KBKeyboardLayoutEngine;
|
||||||
|
@class KBKeyboardKeyFactory;
|
||||||
|
@class KBKeyboardRowConfig;
|
||||||
|
@class KBBackspaceLongPressHandler;
|
||||||
|
@class UIView;
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface KBKeyboardRowBuilder : NSObject
|
||||||
|
|
||||||
|
- (instancetype)initWithLayoutConfig:(KBKeyboardLayoutConfig *)layoutConfig
|
||||||
|
layoutEngine:(KBKeyboardLayoutEngine *)layoutEngine
|
||||||
|
keyFactory:(KBKeyboardKeyFactory *)keyFactory;
|
||||||
|
|
||||||
|
- (void)buildRow:(UIView *)row
|
||||||
|
withRowConfig:(KBKeyboardRowConfig *)rowConfig
|
||||||
|
uniformCharWidth:(CGFloat)uniformCharWidth
|
||||||
|
shiftOn:(BOOL)shiftOn
|
||||||
|
backspaceHandler:(KBBackspaceLongPressHandler *)backspaceHandler
|
||||||
|
target:(id)target
|
||||||
|
action:(SEL)action;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
330
CustomKeyboard/View/KBKeyboardView/KBKeyboardRowBuilder.m
Normal file
330
CustomKeyboard/View/KBKeyboardView/KBKeyboardRowBuilder.m
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// KBKeyboardRowContainerBuilder.h
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
@class KBKeyboardRowConfig;
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface KBKeyboardRowContainerBuilder : NSObject
|
||||||
|
|
||||||
|
- (void)rebuildRowContainersForRows:(NSArray<KBKeyboardRowConfig *> *)rowConfigs
|
||||||
|
inContainer:(UIView *)container
|
||||||
|
rowViews:(NSMutableArray<UIView *> *)rowViews
|
||||||
|
rowSpacing:(CGFloat)rowSpacing
|
||||||
|
topInset:(CGFloat)topInset
|
||||||
|
bottomInset:(CGFloat)bottomInset;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
//
|
||||||
|
// KBKeyboardRowContainerBuilder.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBKeyboardRowContainerBuilder.h"
|
||||||
|
#import <Masonry/Masonry.h>
|
||||||
|
|
||||||
|
@implementation KBKeyboardRowContainerBuilder
|
||||||
|
|
||||||
|
- (void)rebuildRowContainersForRows:(NSArray<KBKeyboardRowConfig *> *)rowConfigs
|
||||||
|
inContainer:(UIView *)container
|
||||||
|
rowViews:(NSMutableArray<UIView *> *)rowViews
|
||||||
|
rowSpacing:(CGFloat)rowSpacing
|
||||||
|
topInset:(CGFloat)topInset
|
||||||
|
bottomInset:(CGFloat)bottomInset {
|
||||||
|
if (!container || !rowViews) { return; }
|
||||||
|
|
||||||
|
for (UIView *row in rowViews) {
|
||||||
|
[row removeFromSuperview];
|
||||||
|
}
|
||||||
|
[rowViews removeAllObjects];
|
||||||
|
|
||||||
|
NSUInteger rowCount = rowConfigs.count;
|
||||||
|
if (rowCount == 0) { return; }
|
||||||
|
|
||||||
|
UIView *firstRow = nil;
|
||||||
|
UIView *previousRow = nil;
|
||||||
|
for (NSUInteger i = 0; i < rowCount; i++) {
|
||||||
|
UIView *rowView = [UIView new];
|
||||||
|
[container addSubview:rowView];
|
||||||
|
[rowViews addObject:rowView];
|
||||||
|
|
||||||
|
[rowView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
if (previousRow) {
|
||||||
|
make.top.equalTo(previousRow.mas_bottom).offset(rowSpacing);
|
||||||
|
} else {
|
||||||
|
make.top.equalTo(container.mas_top).offset(topInset);
|
||||||
|
}
|
||||||
|
make.left.right.equalTo(container);
|
||||||
|
// 所有行等高,自动根据可用空间分配行高
|
||||||
|
if (firstRow) {
|
||||||
|
make.height.equalTo(firstRow);
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 最后一行锚定到底部
|
||||||
|
if (i == rowCount - 1) {
|
||||||
|
[rowView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.bottom.equalTo(container.mas_bottom).offset(-bottomInset);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firstRow) { firstRow = rowView; }
|
||||||
|
previousRow = rowView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
414
CustomKeyboard/View/KBKeyboardView/KBKeyboardView.m
Normal file
414
CustomKeyboard/View/KBKeyboardView/KBKeyboardView.m
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
//
|
||||||
|
// KBKeyboardView.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBKeyboardView.h"
|
||||||
|
#import "KBKeyButton.h"
|
||||||
|
#import "KBKey.h"
|
||||||
|
#import "KBBackspaceLongPressHandler.h"
|
||||||
|
#import "KBKeyboardLayoutConfig.h"
|
||||||
|
#import "KBKeyboardLayoutResolver.h"
|
||||||
|
#import "KBKeyboardKeyFactory.h"
|
||||||
|
#import "KBKeyboardLayoutEngine.h"
|
||||||
|
#import "KBKeyboardRowBuilder.h"
|
||||||
|
#import "KBKeyboardInputHandler.h"
|
||||||
|
#import "KBKeyboardLegacyBuilder.h"
|
||||||
|
#import "KBKeyboardLegacyLayoutProvider.h"
|
||||||
|
#import "KBKeyboardInteractionHandler.h"
|
||||||
|
#import "KBKeyboardRowContainerBuilder.h"
|
||||||
|
|
||||||
|
// 第二行字母行的左右占位比例(用于居中)
|
||||||
|
static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5;
|
||||||
|
|
||||||
|
@interface KBKeyboardView ()
|
||||||
|
@property (nonatomic, strong) NSMutableArray<UIView *> *rowViews;
|
||||||
|
@property (nonatomic, strong) NSArray<NSArray<KBKey *> *> *keysForRows;
|
||||||
|
@property (nonatomic, strong) KBBackspaceLongPressHandler *backspaceHandler;
|
||||||
|
@property (nonatomic, strong) KBKeyboardLayoutConfig *layoutConfig;
|
||||||
|
@property (nonatomic, strong) KBKeyboardKeyFactory *keyFactory;
|
||||||
|
@property (nonatomic, strong) KBKeyboardLayoutEngine *layoutEngine;
|
||||||
|
@property (nonatomic, strong) KBKeyboardRowBuilder *rowBuilder;
|
||||||
|
@property (nonatomic, strong) KBKeyboardInputHandler *inputHandler;
|
||||||
|
@property (nonatomic, strong) KBKeyboardLegacyBuilder *legacyBuilder;
|
||||||
|
@property (nonatomic, strong) KBKeyboardLegacyLayoutProvider *legacyLayoutProvider;
|
||||||
|
@property (nonatomic, strong) KBKeyboardInteractionHandler *interactionHandler;
|
||||||
|
@property (nonatomic, strong) KBKeyboardRowContainerBuilder *rowContainerBuilder;
|
||||||
|
/// 跨行统一字符键宽度(按最多字符键的行计算),0 表示不启用
|
||||||
|
@property (nonatomic, assign) CGFloat kb_uniformCharKeyWidth;
|
||||||
|
/// 记录当前行间距,便于切换布局时判断是否需要重建容器
|
||||||
|
@property (nonatomic, assign) CGFloat kb_currentRowSpacing;
|
||||||
|
/// 记录当前顶/底间距,便于切换布局时判断是否需要重建容器
|
||||||
|
@property (nonatomic, assign) CGFloat kb_currentTopInset;
|
||||||
|
@property (nonatomic, assign) CGFloat kb_currentBottomInset;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation KBKeyboardView
|
||||||
|
|
||||||
|
- (instancetype)initWithFrame:(CGRect)frame {
|
||||||
|
if (self = [super initWithFrame:frame]) {
|
||||||
|
self.backgroundColor = [UIColor clearColor];
|
||||||
|
_layoutStyle = KBKeyboardLayoutStyleLetters;
|
||||||
|
// 默认小写:与需求一致,初始不开启 Shift
|
||||||
|
_shiftOn = NO;
|
||||||
|
_symbolsMoreOn = NO; // 数字面板默认第一页(123)
|
||||||
|
|
||||||
|
// 从 App Group 读取当前 profileId 并设置布局
|
||||||
|
NSString *profileId = [[KBKeyboardLayoutResolver sharedResolver] currentProfileId];
|
||||||
|
if (profileId.length > 0) {
|
||||||
|
_currentLayoutJsonId = [[KBKeyboardLayoutResolver sharedResolver] layoutJsonIdForProfileId:profileId];
|
||||||
|
NSLog(@"[KBKeyboardView] Loaded profileId: %@, layoutJsonId: %@", profileId, _currentLayoutJsonId);
|
||||||
|
} else {
|
||||||
|
_currentLayoutJsonId = @"letters";
|
||||||
|
NSLog(@"[KBKeyboardView] No profileId found, using default 'letters'");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig];
|
||||||
|
self.backspaceHandler = [[KBBackspaceLongPressHandler alloc] initWithContainerView:self];
|
||||||
|
[self buildBase];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当切换大布局(字母/数字)时,重置数字二级页状态
|
||||||
|
- (void)setLayoutStyle:(KBKeyboardLayoutStyle)layoutStyle {
|
||||||
|
_layoutStyle = layoutStyle;
|
||||||
|
if (_layoutStyle != KBKeyboardLayoutStyleNumbers) {
|
||||||
|
_symbolsMoreOn = NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Base Layout
|
||||||
|
|
||||||
|
- (void)buildBase {
|
||||||
|
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]];
|
||||||
|
}
|
||||||
|
CGFloat rowSpacing = [self.layoutEngine rowSpacingForLayout:layout];
|
||||||
|
CGFloat topInset = [self.layoutEngine topInsetForLayout:layout];
|
||||||
|
CGFloat bottomInset = [self.layoutEngine bottomInsetForLayout:layout];
|
||||||
|
self.kb_currentRowSpacing = rowSpacing;
|
||||||
|
self.kb_currentTopInset = topInset;
|
||||||
|
self.kb_currentBottomInset = bottomInset;
|
||||||
|
[self.rowContainerBuilder rebuildRowContainersForRows:rows
|
||||||
|
inContainer:self
|
||||||
|
rowViews:self.rowViews
|
||||||
|
rowSpacing:rowSpacing
|
||||||
|
topInset:topInset
|
||||||
|
bottomInset:bottomInset];
|
||||||
|
[self.interactionHandler bringPreviewToFrontIfNeededInContainer:self];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Public
|
||||||
|
|
||||||
|
- (void)reloadKeys {
|
||||||
|
[self.backspaceHandler bindDeleteButton:nil showClearLabel:NO];
|
||||||
|
|
||||||
|
KBKeyboardLayout *layout = [self kb_currentLayout];
|
||||||
|
CGFloat rowSpacing = [self.layoutEngine rowSpacingForLayout:layout];
|
||||||
|
CGFloat topInset = [self.layoutEngine topInsetForLayout:layout];
|
||||||
|
CGFloat bottomInset = [self.layoutEngine bottomInsetForLayout:layout];
|
||||||
|
NSLog(@"[KBKeyboardView] reloadKeys: layoutName=%@ rows=%lu shiftRows=%lu shiftOn=%d",
|
||||||
|
self.currentLayoutJsonId, (unsigned long)layout.rows.count, (unsigned long)layout.shiftRows.count, self.shiftOn);
|
||||||
|
|
||||||
|
NSArray<KBKeyboardRowConfig *> *rows = nil;
|
||||||
|
|
||||||
|
if (self.shiftOn && layout.shiftRows.count > 0) {
|
||||||
|
rows = layout.shiftRows;
|
||||||
|
} else {
|
||||||
|
rows = layout.rows ?: @[];
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ||
|
||||||
|
fabs(self.kb_currentRowSpacing - rowSpacing) > 0.1 ||
|
||||||
|
fabs(self.kb_currentTopInset - topInset) > 0.1 ||
|
||||||
|
fabs(self.kb_currentBottomInset - bottomInset) > 0.1)) {
|
||||||
|
self.kb_currentRowSpacing = rowSpacing;
|
||||||
|
self.kb_currentTopInset = topInset;
|
||||||
|
self.kb_currentBottomInset = bottomInset;
|
||||||
|
[self.rowContainerBuilder rebuildRowContainersForRows:rows
|
||||||
|
inContainer:self
|
||||||
|
rowViews:self.rowViews
|
||||||
|
rowSpacing:rowSpacing
|
||||||
|
topInset:topInset
|
||||||
|
bottomInset:bottomInset];
|
||||||
|
[self.interactionHandler bringPreviewToFrontIfNeededInContainer:self];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除旧按钮
|
||||||
|
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.kb_uniformCharKeyWidth = [self.layoutEngine calculateUniformCharKeyWidthForRows:rows
|
||||||
|
keyFactory:self.keyFactory
|
||||||
|
shiftOn:self.shiftOn];
|
||||||
|
|
||||||
|
for (NSUInteger i = 0; i < rows.count && i < self.rowViews.count; i++) {
|
||||||
|
[self.rowBuilder buildRow:self.rowViews[i]
|
||||||
|
withRowConfig:rows[i]
|
||||||
|
uniformCharWidth:self.kb_uniformCharKeyWidth
|
||||||
|
shiftOn:self.shiftOn
|
||||||
|
backspaceHandler:self.backspaceHandler
|
||||||
|
target:self
|
||||||
|
action:@selector(onKeyTapped:)];
|
||||||
|
}
|
||||||
|
|
||||||
|
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.rowViews) {
|
||||||
|
[row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
|
||||||
|
}
|
||||||
|
[self kb_buildLegacyLayout];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)didMoveToWindow {
|
||||||
|
[super didMoveToWindow];
|
||||||
|
if (!self.window) { return; }
|
||||||
|
if ([self kb_totalKeyButtonCount] > 0) { return; }
|
||||||
|
// 兜底:系统编辑菜单切出切回等场景下,若按键丢失则自动重建。
|
||||||
|
[self reloadKeys];
|
||||||
|
// 自动重建后再触发一次上层主题应用,避免“按键恢复了但皮肤背景没恢复”。
|
||||||
|
UIView *container = self.superview;
|
||||||
|
if ([container respondsToSelector:@selector(kb_applyTheme)]) {
|
||||||
|
#pragma clang diagnostic push
|
||||||
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||||
|
[container performSelector:@selector(kb_applyTheme)];
|
||||||
|
#pragma clang diagnostic pop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSUInteger)kb_totalKeyButtonCount {
|
||||||
|
NSUInteger total = 0;
|
||||||
|
for (UIView *row in self.rowViews) {
|
||||||
|
total += [self.interactionHandler collectKeyButtonsInView:row].count;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Hit Test
|
||||||
|
|
||||||
|
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
|
||||||
|
UIView *hit = [super hitTest:point withEvent:event];
|
||||||
|
return [self.interactionHandler resolveHitView:hit
|
||||||
|
point:point
|
||||||
|
container:self
|
||||||
|
rowViews:self.rowViews];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Config Helpers
|
||||||
|
|
||||||
|
- (KBKeyboardLayoutConfig *)kb_layoutConfig {
|
||||||
|
if (!self.layoutConfig) {
|
||||||
|
self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig];
|
||||||
|
}
|
||||||
|
return self.layoutConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (KBKeyboardKeyFactory *)keyFactory {
|
||||||
|
if (!_keyFactory) {
|
||||||
|
_keyFactory = [[KBKeyboardKeyFactory alloc] initWithLayoutConfig:[self kb_layoutConfig]];
|
||||||
|
}
|
||||||
|
return _keyFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (KBKeyboardLayoutEngine *)layoutEngine {
|
||||||
|
if (!_layoutEngine) {
|
||||||
|
_layoutEngine = [[KBKeyboardLayoutEngine alloc] initWithLayoutConfig:[self kb_layoutConfig]];
|
||||||
|
}
|
||||||
|
return _layoutEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (KBKeyboardRowBuilder *)rowBuilder {
|
||||||
|
if (!_rowBuilder) {
|
||||||
|
_rowBuilder = [[KBKeyboardRowBuilder alloc] initWithLayoutConfig:[self kb_layoutConfig]
|
||||||
|
layoutEngine:self.layoutEngine
|
||||||
|
keyFactory:self.keyFactory];
|
||||||
|
}
|
||||||
|
return _rowBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (KBKeyboardInputHandler *)inputHandler {
|
||||||
|
if (!_inputHandler) {
|
||||||
|
_inputHandler = [[KBKeyboardInputHandler alloc] init];
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
_inputHandler.onToggleShift = ^{
|
||||||
|
__strong typeof(weakSelf) self = weakSelf;
|
||||||
|
if (!self) { return; }
|
||||||
|
self.shiftOn = !self.shiftOn;
|
||||||
|
[self reloadKeys];
|
||||||
|
};
|
||||||
|
_inputHandler.onToggleSymbols = ^{
|
||||||
|
__strong typeof(weakSelf) self = weakSelf;
|
||||||
|
if (!self) { return; }
|
||||||
|
self.symbolsMoreOn = !self.symbolsMoreOn;
|
||||||
|
[self reloadKeys];
|
||||||
|
};
|
||||||
|
_inputHandler.onKeyTapped = ^(KBKey *key) {
|
||||||
|
__strong typeof(weakSelf) self = weakSelf;
|
||||||
|
if (!self) { return; }
|
||||||
|
if ([self.delegate respondsToSelector:@selector(keyboardView:didTapKey:)]) {
|
||||||
|
[self.delegate keyboardView:self didTapKey:key];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return _inputHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (KBKeyboardLegacyBuilder *)legacyBuilder {
|
||||||
|
if (!_legacyBuilder) {
|
||||||
|
_legacyBuilder = [[KBKeyboardLegacyBuilder alloc] init];
|
||||||
|
}
|
||||||
|
return _legacyBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (KBKeyboardLegacyLayoutProvider *)legacyLayoutProvider {
|
||||||
|
if (!_legacyLayoutProvider) {
|
||||||
|
_legacyLayoutProvider = [[KBKeyboardLegacyLayoutProvider alloc] init];
|
||||||
|
}
|
||||||
|
return _legacyLayoutProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (KBKeyboardInteractionHandler *)interactionHandler {
|
||||||
|
if (!_interactionHandler) {
|
||||||
|
_interactionHandler = [[KBKeyboardInteractionHandler alloc] init];
|
||||||
|
}
|
||||||
|
return _interactionHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (KBKeyboardRowContainerBuilder *)rowContainerBuilder {
|
||||||
|
if (!_rowContainerBuilder) {
|
||||||
|
_rowContainerBuilder = [[KBKeyboardRowContainerBuilder alloc] init];
|
||||||
|
}
|
||||||
|
return _rowContainerBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (KBKeyboardLayout *)kb_layoutForName:(NSString *)name {
|
||||||
|
return [[self kb_layoutConfig] layoutForName:name];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (KBKeyboardLayout *)kb_currentLayout {
|
||||||
|
NSString *baseLayoutName = self.currentLayoutJsonId.length > 0 ? self.currentLayoutJsonId : @"letters";
|
||||||
|
|
||||||
|
if (self.layoutStyle == KBKeyboardLayoutStyleNumbers) {
|
||||||
|
// 优先查找当前语言的数字/符号布局,如 letters_es_numbers / letters_es_symbols
|
||||||
|
// 如果不存在,回退到通用布局 numbers / symbolsMore
|
||||||
|
NSString *numbersName = [NSString stringWithFormat:@"%@_numbers", baseLayoutName];
|
||||||
|
NSString *symbolsName = [NSString stringWithFormat:@"%@_symbols", baseLayoutName];
|
||||||
|
NSString *targetName = self.symbolsMoreOn ? symbolsName : numbersName;
|
||||||
|
|
||||||
|
KBKeyboardLayout *layout = [self kb_layoutForName:targetName];
|
||||||
|
if (layout && layout.rows.count >= 4) {
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
// 回退到通用布局
|
||||||
|
return [self kb_layoutForName:(self.symbolsMoreOn ? @"symbolsMore" : @"numbers")];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [self kb_layoutForName:baseLayoutName];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)reloadLayoutWithProfileId:(NSString *)profileId {
|
||||||
|
if (profileId.length == 0) {
|
||||||
|
NSLog(@"[KBKeyboardView] reloadLayoutWithProfileId: empty profileId, ignoring");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *newLayoutJsonId = [[KBKeyboardLayoutResolver sharedResolver] layoutJsonIdForProfileId:profileId];
|
||||||
|
if ([newLayoutJsonId isEqualToString:self.currentLayoutJsonId]) {
|
||||||
|
NSLog(@"[KBKeyboardView] Layout already loaded: %@", newLayoutJsonId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog(@"[KBKeyboardView] Switching layout from %@ to %@", self.currentLayoutJsonId, newLayoutJsonId);
|
||||||
|
self.currentLayoutJsonId = newLayoutJsonId;
|
||||||
|
|
||||||
|
// 重新加载键盘布局
|
||||||
|
[self reloadKeys];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_buildLegacyLayout {
|
||||||
|
self.keysForRows = [self.legacyLayoutProvider keysForLayoutStyleIsNumbers:(self.layoutStyle == KBKeyboardLayoutStyleNumbers)
|
||||||
|
shiftOn:self.shiftOn
|
||||||
|
symbolsMoreOn:self.symbolsMoreOn
|
||||||
|
keyFactory:self.keyFactory];
|
||||||
|
if (self.keysForRows.count < 4) { return; }
|
||||||
|
if (self.rowViews.count < 4) { return; }
|
||||||
|
|
||||||
|
[self.legacyBuilder buildRow:self.rowViews[0]
|
||||||
|
withKeys:self.keysForRows[0]
|
||||||
|
edgeSpacerMultiplier:0.0
|
||||||
|
shiftOn:self.shiftOn
|
||||||
|
backspaceHandler:self.backspaceHandler
|
||||||
|
target:self
|
||||||
|
action:@selector(onKeyTapped:)];
|
||||||
|
|
||||||
|
CGFloat row2Spacer = (self.layoutStyle == KBKeyboardLayoutStyleLetters)
|
||||||
|
? kKBLettersRow2EdgeSpacerMultiplier : 0.0;
|
||||||
|
[self.legacyBuilder buildRow:self.rowViews[1]
|
||||||
|
withKeys:self.keysForRows[1]
|
||||||
|
edgeSpacerMultiplier:row2Spacer
|
||||||
|
shiftOn:self.shiftOn
|
||||||
|
backspaceHandler:self.backspaceHandler
|
||||||
|
target:self
|
||||||
|
action:@selector(onKeyTapped:)];
|
||||||
|
|
||||||
|
[self.legacyBuilder buildRow:self.rowViews[2]
|
||||||
|
withKeys:self.keysForRows[2]
|
||||||
|
edgeSpacerMultiplier:0.0
|
||||||
|
shiftOn:self.shiftOn
|
||||||
|
backspaceHandler:self.backspaceHandler
|
||||||
|
target:self
|
||||||
|
action:@selector(onKeyTapped:)];
|
||||||
|
[self.legacyBuilder buildRow:self.rowViews[3]
|
||||||
|
withKeys:self.keysForRows[3]
|
||||||
|
edgeSpacerMultiplier:0.0
|
||||||
|
shiftOn:self.shiftOn
|
||||||
|
backspaceHandler:self.backspaceHandler
|
||||||
|
target:self
|
||||||
|
action:@selector(onKeyTapped:)];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Actions
|
||||||
|
|
||||||
|
- (void)onKeyTapped:(KBKeyButton *)sender {
|
||||||
|
[self.inputHandler handleKeyTap:sender.key];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在字符键按下时,显示一个上方气泡预览(类似系统键盘)。
|
||||||
|
- (void)showPreviewForButton:(KBKeyButton *)button {
|
||||||
|
[self.interactionHandler showPreviewForButton:button inContainer:self];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)hidePreview {
|
||||||
|
[self.interactionHandler hidePreview];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Lazy
|
||||||
|
|
||||||
|
- (NSMutableArray<UIView *> *)rowViews {
|
||||||
|
if (!_rowViews) _rowViews = [NSMutableArray array];
|
||||||
|
return _rowViews;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -325,6 +325,14 @@
|
|||||||
A1F0C1C32FABCDEF12345678 /* 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 */; };
|
A1F0C1D22FACAD0012345678 /* KBMaiPointReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1D12FACAD0012345678 /* KBMaiPointReporter.m */; };
|
||||||
A1F0C1D32FACAD0012345678 /* KBMaiPointReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1D12FACAD0012345678 /* KBMaiPointReporter.m */; };
|
A1F0C1D32FACAD0012345678 /* KBMaiPointReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = A1F0C1D12FACAD0012345678 /* KBMaiPointReporter.m */; };
|
||||||
|
B7F1A1D02F90000100000001 /* KBKeyboardKeyFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = B7F1A1C52F90000100000001 /* KBKeyboardKeyFactory.m */; };
|
||||||
|
B7F1A1D12F90000100000001 /* KBKeyboardLayoutEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = B7F1A1C72F90000100000001 /* KBKeyboardLayoutEngine.m */; };
|
||||||
|
B7F1A1D22F90000100000001 /* KBKeyboardRowBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = B7F1A1C92F90000100000001 /* KBKeyboardRowBuilder.m */; };
|
||||||
|
B7F1A1D32F90000100000001 /* KBKeyboardInputHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = B7F1A1CB2F90000100000001 /* KBKeyboardInputHandler.m */; };
|
||||||
|
B7F1A1D42F90000100000001 /* KBKeyboardLegacyBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = B7F1A1CD2F90000100000001 /* KBKeyboardLegacyBuilder.m */; };
|
||||||
|
B7F1A1D52F90000100000001 /* KBKeyboardLegacyLayoutProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = B7F1A1CF2F90000100000001 /* KBKeyboardLegacyLayoutProvider.m */; };
|
||||||
|
B7F1A1D62F90000100000001 /* KBKeyboardInteractionHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = B7F1A1D82F90000100000001 /* KBKeyboardInteractionHandler.m */; };
|
||||||
|
B7F1A1D92F90000100000001 /* KBKeyboardRowContainerBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = B7F1A1DB2F90000100000001 /* KBKeyboardRowContainerBuilder.m */; };
|
||||||
EB72B60040437E3C0A4890FC /* KBShopThemeDetailModel.m in Sources */ = {isa = PBXBuildFile; fileRef = B9F60894E529C3EDAF6BAC3D /* KBShopThemeDetailModel.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 */; };
|
ECC9EE02174D86E8D792472F /* Pods_keyBoard.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 967065BB5230E43F293B3AF9 /* Pods_keyBoard.framework */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
@@ -898,6 +906,22 @@
|
|||||||
A1F0C1D02FACAD0012345678 /* KBMaiPointReporter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMaiPointReporter.h; sourceTree = "<group>"; };
|
A1F0C1D02FACAD0012345678 /* KBMaiPointReporter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMaiPointReporter.h; sourceTree = "<group>"; };
|
||||||
A1F0C1D12FACAD0012345678 /* KBMaiPointReporter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMaiPointReporter.m; sourceTree = "<group>"; };
|
A1F0C1D12FACAD0012345678 /* KBMaiPointReporter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMaiPointReporter.m; sourceTree = "<group>"; };
|
||||||
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 = "<group>"; };
|
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 = "<group>"; };
|
||||||
|
B7F1A1C42F90000100000001 /* KBKeyboardKeyFactory.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardKeyFactory.h; sourceTree = "<group>"; };
|
||||||
|
B7F1A1C52F90000100000001 /* KBKeyboardKeyFactory.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardKeyFactory.m; sourceTree = "<group>"; };
|
||||||
|
B7F1A1C62F90000100000001 /* KBKeyboardLayoutEngine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardLayoutEngine.h; sourceTree = "<group>"; };
|
||||||
|
B7F1A1C72F90000100000001 /* KBKeyboardLayoutEngine.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardLayoutEngine.m; sourceTree = "<group>"; };
|
||||||
|
B7F1A1C82F90000100000001 /* KBKeyboardRowBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardRowBuilder.h; sourceTree = "<group>"; };
|
||||||
|
B7F1A1C92F90000100000001 /* KBKeyboardRowBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardRowBuilder.m; sourceTree = "<group>"; };
|
||||||
|
B7F1A1CA2F90000100000001 /* KBKeyboardInputHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardInputHandler.h; sourceTree = "<group>"; };
|
||||||
|
B7F1A1CB2F90000100000001 /* KBKeyboardInputHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardInputHandler.m; sourceTree = "<group>"; };
|
||||||
|
B7F1A1CC2F90000100000001 /* KBKeyboardLegacyBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardLegacyBuilder.h; sourceTree = "<group>"; };
|
||||||
|
B7F1A1CD2F90000100000001 /* KBKeyboardLegacyBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardLegacyBuilder.m; sourceTree = "<group>"; };
|
||||||
|
B7F1A1CE2F90000100000001 /* KBKeyboardLegacyLayoutProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardLegacyLayoutProvider.h; sourceTree = "<group>"; };
|
||||||
|
B7F1A1CF2F90000100000001 /* KBKeyboardLegacyLayoutProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardLegacyLayoutProvider.m; sourceTree = "<group>"; };
|
||||||
|
B7F1A1D72F90000100000001 /* KBKeyboardInteractionHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardInteractionHandler.h; sourceTree = "<group>"; };
|
||||||
|
B7F1A1D82F90000100000001 /* KBKeyboardInteractionHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardInteractionHandler.m; sourceTree = "<group>"; };
|
||||||
|
B7F1A1DA2F90000100000001 /* KBKeyboardRowContainerBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardRowContainerBuilder.h; sourceTree = "<group>"; };
|
||||||
|
B7F1A1DB2F90000100000001 /* KBKeyboardRowContainerBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardRowContainerBuilder.m; sourceTree = "<group>"; };
|
||||||
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 = "<group>"; };
|
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 = "<group>"; };
|
||||||
B9F60894E529C3EDAF6BAC3D /* KBShopThemeDetailModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBShopThemeDetailModel.m; sourceTree = "<group>"; };
|
B9F60894E529C3EDAF6BAC3D /* KBShopThemeDetailModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBShopThemeDetailModel.m; sourceTree = "<group>"; };
|
||||||
E2A844CD2D8584596DBE6316 /* KBShopThemeTagModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBShopThemeTagModel.m; sourceTree = "<group>"; };
|
E2A844CD2D8584596DBE6316 /* KBShopThemeTagModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBShopThemeTagModel.m; sourceTree = "<group>"; };
|
||||||
@@ -1084,6 +1108,31 @@
|
|||||||
path = Resource;
|
path = Resource;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
043213C72F57DD600065C888 /* KBKeyboardView */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
04FC956B2EB054B7007BD342 /* KBKeyboardView.h */,
|
||||||
|
04FC956C2EB054B7007BD342 /* KBKeyboardView.m */,
|
||||||
|
B7F1A1C42F90000100000001 /* KBKeyboardKeyFactory.h */,
|
||||||
|
B7F1A1C52F90000100000001 /* KBKeyboardKeyFactory.m */,
|
||||||
|
B7F1A1C62F90000100000001 /* KBKeyboardLayoutEngine.h */,
|
||||||
|
B7F1A1C72F90000100000001 /* KBKeyboardLayoutEngine.m */,
|
||||||
|
B7F1A1C82F90000100000001 /* KBKeyboardRowBuilder.h */,
|
||||||
|
B7F1A1C92F90000100000001 /* KBKeyboardRowBuilder.m */,
|
||||||
|
B7F1A1CA2F90000100000001 /* KBKeyboardInputHandler.h */,
|
||||||
|
B7F1A1CB2F90000100000001 /* KBKeyboardInputHandler.m */,
|
||||||
|
B7F1A1CC2F90000100000001 /* KBKeyboardLegacyBuilder.h */,
|
||||||
|
B7F1A1CD2F90000100000001 /* KBKeyboardLegacyBuilder.m */,
|
||||||
|
B7F1A1CE2F90000100000001 /* KBKeyboardLegacyLayoutProvider.h */,
|
||||||
|
B7F1A1CF2F90000100000001 /* KBKeyboardLegacyLayoutProvider.m */,
|
||||||
|
B7F1A1D72F90000100000001 /* KBKeyboardInteractionHandler.h */,
|
||||||
|
B7F1A1D82F90000100000001 /* KBKeyboardInteractionHandler.m */,
|
||||||
|
B7F1A1DA2F90000100000001 /* KBKeyboardRowContainerBuilder.h */,
|
||||||
|
B7F1A1DB2F90000100000001 /* KBKeyboardRowContainerBuilder.m */,
|
||||||
|
);
|
||||||
|
path = KBKeyboardView;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
0450ABFB2EF11E4400B6AF06 /* Converts */ = {
|
0450ABFB2EF11E4400B6AF06 /* Converts */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -1672,8 +1721,7 @@
|
|||||||
04A9A67D2EB9E1690023B8F4 /* KBResponderUtils.h */,
|
04A9A67D2EB9E1690023B8F4 /* KBResponderUtils.h */,
|
||||||
04FC95682EB05497007BD342 /* KBKeyButton.h */,
|
04FC95682EB05497007BD342 /* KBKeyButton.h */,
|
||||||
04FC95692EB05497007BD342 /* KBKeyButton.m */,
|
04FC95692EB05497007BD342 /* KBKeyButton.m */,
|
||||||
04FC956B2EB054B7007BD342 /* KBKeyboardView.h */,
|
043213C72F57DD600065C888 /* KBKeyboardView */,
|
||||||
04FC956C2EB054B7007BD342 /* KBKeyboardView.m */,
|
|
||||||
046131122ECF454500A6FADF /* KBKeyPreviewView.h */,
|
046131122ECF454500A6FADF /* KBKeyPreviewView.h */,
|
||||||
046131132ECF454500A6FADF /* KBKeyPreviewView.m */,
|
046131132ECF454500A6FADF /* KBKeyPreviewView.m */,
|
||||||
04FC95772EB09BC8007BD342 /* KBKeyBoardMainView.h */,
|
04FC95772EB09BC8007BD342 /* KBKeyBoardMainView.h */,
|
||||||
@@ -2490,6 +2538,14 @@
|
|||||||
049FB23F2EC4B6EF00FAB05D /* KBULBridgeNotification.m in Sources */,
|
049FB23F2EC4B6EF00FAB05D /* KBULBridgeNotification.m in Sources */,
|
||||||
04791F992ED49CE7004E8522 /* KBFont.m in Sources */,
|
04791F992ED49CE7004E8522 /* KBFont.m in Sources */,
|
||||||
04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */,
|
04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */,
|
||||||
|
B7F1A1D02F90000100000001 /* KBKeyboardKeyFactory.m in Sources */,
|
||||||
|
B7F1A1D12F90000100000001 /* KBKeyboardLayoutEngine.m in Sources */,
|
||||||
|
B7F1A1D22F90000100000001 /* KBKeyboardRowBuilder.m in Sources */,
|
||||||
|
B7F1A1D32F90000100000001 /* KBKeyboardInputHandler.m in Sources */,
|
||||||
|
B7F1A1D42F90000100000001 /* KBKeyboardLegacyBuilder.m in Sources */,
|
||||||
|
B7F1A1D52F90000100000001 /* KBKeyboardLegacyLayoutProvider.m in Sources */,
|
||||||
|
B7F1A1D62F90000100000001 /* KBKeyboardInteractionHandler.m in Sources */,
|
||||||
|
B7F1A1D92F90000100000001 /* KBKeyboardRowContainerBuilder.m in Sources */,
|
||||||
04FC95672EB0546C007BD342 /* KBKey.m in Sources */,
|
04FC95672EB0546C007BD342 /* KBKey.m in Sources */,
|
||||||
A1B2C3F42EB35A9900000001 /* KBFullAccessGuideView.m in Sources */,
|
A1B2C3F42EB35A9900000001 /* KBFullAccessGuideView.m in Sources */,
|
||||||
0498BD8F2EE6A3BD006CC1D5 /* KBMyMainModel.m in Sources */,
|
0498BD8F2EE6A3BD006CC1D5 /* KBMyMainModel.m in Sources */,
|
||||||
|
|||||||
Reference in New Issue
Block a user