Files
keyboard/CustomKeyboard/View/KBKeyboardView/KBKeyboardView.m

415 lines
16 KiB
Mathematica
Raw Normal View History

2026-03-04 12:54:57 +08:00
//
// 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