添加联想
This commit is contained in:
@@ -20,14 +20,15 @@
|
||||
#import "KBKeyboardSubscriptionView.h"
|
||||
#import "KBKeyboardSubscriptionProduct.h"
|
||||
#import "KBBackspaceUndoManager.h"
|
||||
#import "KBSuggestionEngine.h"
|
||||
|
||||
// 提前声明一个类别,使编译器在 static 回调中识别 kb_consumePendingShopSkin 方法。
|
||||
@interface KeyboardViewController (KBSkinShopBridge)
|
||||
- (void)kb_consumePendingShopSkin;
|
||||
@end
|
||||
|
||||
// 以 375 宽设计稿为基准的键盘总高度(包括顶部工具栏)
|
||||
static const CGFloat kKBKeyboardDesignHeight = 250.0f;
|
||||
// 以 375 宽设计稿为基准的键盘总高度
|
||||
static const CGFloat kKBKeyboardBaseHeight = 250.0f;
|
||||
|
||||
static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
||||
void *observer,
|
||||
@@ -50,6 +51,9 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
||||
@property (nonatomic, strong) KBSettingView *settingView; // 设置页
|
||||
@property (nonatomic, strong) UIImageView *bgImageView; // 背景图(在底层)
|
||||
@property (nonatomic, strong) KBKeyboardSubscriptionView *subscriptionView;
|
||||
@property (nonatomic, strong) KBSuggestionEngine *suggestionEngine;
|
||||
@property (nonatomic, copy) NSString *currentWord;
|
||||
@property (nonatomic, assign) BOOL suppressSuggestions;
|
||||
@end
|
||||
|
||||
@implementation KeyboardViewController
|
||||
@@ -61,6 +65,8 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
[self setupUI];
|
||||
self.suggestionEngine = [KBSuggestionEngine shared];
|
||||
self.currentWord = @"";
|
||||
// 指定 HUD 的承载视图(扩展里无法取到 App 的 KeyWindow)
|
||||
[KBHUD setContainerView:self.view];
|
||||
// 绑定完全访问管理器,便于统一感知和联动网络开关
|
||||
@@ -94,7 +100,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
||||
self.view.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
// 按屏幕宽度对设计值做等比缩放,避免在不同机型上键盘整体高度失真导致皮肤被压缩/拉伸
|
||||
CGFloat keyboardHeight = KBFit(kKBKeyboardDesignHeight);
|
||||
CGFloat keyboardHeight = KBFit(kKBKeyboardBaseHeight);
|
||||
CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;
|
||||
CGFloat outerVerticalInset = KBFit(4.0f);
|
||||
|
||||
@@ -137,6 +143,113 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
// MARK: - Suggestions
|
||||
|
||||
- (void)kb_updateCurrentWordWithInsertedText:(NSString *)text {
|
||||
if (text.length == 0) { return; }
|
||||
if ([self kb_isAlphabeticString:text]) {
|
||||
NSString *current = self.currentWord ?: @"";
|
||||
self.currentWord = [current stringByAppendingString:text];
|
||||
self.suppressSuggestions = NO;
|
||||
[self kb_updateSuggestionsForCurrentWord];
|
||||
} else {
|
||||
[self kb_clearCurrentWord];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)kb_clearCurrentWord {
|
||||
self.currentWord = @"";
|
||||
[self.keyBoardMainView kb_setSuggestions:@[]];
|
||||
self.suppressSuggestions = NO;
|
||||
}
|
||||
|
||||
- (void)kb_scheduleContextRefreshResetSuppression:(BOOL)resetSuppression {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self kb_refreshCurrentWordFromDocumentContextResetSuppression:resetSuppression];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)kb_refreshCurrentWordFromDocumentContextResetSuppression:(BOOL)resetSuppression {
|
||||
NSString *context = self.textDocumentProxy.documentContextBeforeInput ?: @"";
|
||||
NSString *word = [self kb_extractTrailingWordFromContext:context];
|
||||
self.currentWord = word ?: @"";
|
||||
if (resetSuppression) {
|
||||
self.suppressSuggestions = NO;
|
||||
}
|
||||
[self kb_updateSuggestionsForCurrentWord];
|
||||
}
|
||||
|
||||
- (NSString *)kb_extractTrailingWordFromContext:(NSString *)context {
|
||||
if (context.length == 0) { return @""; }
|
||||
static NSCharacterSet *letters = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
letters = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"];
|
||||
});
|
||||
|
||||
NSInteger idx = (NSInteger)context.length - 1;
|
||||
while (idx >= 0) {
|
||||
unichar ch = [context characterAtIndex:(NSUInteger)idx];
|
||||
if (![letters characterIsMember:ch]) {
|
||||
break;
|
||||
}
|
||||
idx -= 1;
|
||||
}
|
||||
NSUInteger start = (NSUInteger)(idx + 1);
|
||||
if (start >= context.length) { return @""; }
|
||||
return [context substringFromIndex:start];
|
||||
}
|
||||
|
||||
- (BOOL)kb_isAlphabeticString:(NSString *)text {
|
||||
if (text.length == 0) { return NO; }
|
||||
static NSCharacterSet *letters = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
letters = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"];
|
||||
});
|
||||
for (NSUInteger i = 0; i < text.length; i++) {
|
||||
if (![letters characterIsMember:[text characterAtIndex:i]]) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)kb_updateSuggestionsForCurrentWord {
|
||||
NSString *prefix = self.currentWord ?: @"";
|
||||
if (prefix.length == 0) {
|
||||
[self.keyBoardMainView kb_setSuggestions:@[]];
|
||||
return;
|
||||
}
|
||||
if (self.suppressSuggestions) {
|
||||
[self.keyBoardMainView kb_setSuggestions:@[]];
|
||||
return;
|
||||
}
|
||||
NSArray<NSString *> *items = [self.suggestionEngine suggestionsForPrefix:prefix limit:5];
|
||||
NSArray<NSString *> *cased = [self kb_applyCaseToSuggestions:items prefix:prefix];
|
||||
[self.keyBoardMainView kb_setSuggestions:cased];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)kb_applyCaseToSuggestions:(NSArray<NSString *> *)items prefix:(NSString *)prefix {
|
||||
if (items.count == 0 || prefix.length == 0) { return items; }
|
||||
BOOL allUpper = [prefix isEqualToString:prefix.uppercaseString];
|
||||
BOOL firstUpper = [[prefix substringToIndex:1] isEqualToString:[[prefix substringToIndex:1] uppercaseString]];
|
||||
|
||||
if (!allUpper && !firstUpper) { return items; }
|
||||
|
||||
NSMutableArray<NSString *> *result = [NSMutableArray arrayWithCapacity:items.count];
|
||||
for (NSString *word in items) {
|
||||
if (allUpper) {
|
||||
[result addObject:word.uppercaseString];
|
||||
} else {
|
||||
NSString *first = [[word substringToIndex:1] uppercaseString];
|
||||
NSString *rest = (word.length > 1) ? [word substringFromIndex:1] : @"";
|
||||
[result addObject:[first stringByAppendingString:rest]];
|
||||
}
|
||||
}
|
||||
return result.copy;
|
||||
}
|
||||
|
||||
/// 切换显示功能面板/键盘主视图
|
||||
- (void)showFunctionPanel:(BOOL)show {
|
||||
// 简单显隐切换,复用相同的布局区域
|
||||
@@ -254,19 +367,29 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||
}
|
||||
switch (key.type) {
|
||||
case KBKeyTypeCharacter:
|
||||
[self.textDocumentProxy insertText:key.output ?: key.title ?: @""]; break;
|
||||
case KBKeyTypeCharacter: {
|
||||
NSString *text = key.output ?: key.title ?: @"";
|
||||
[self.textDocumentProxy insertText:text];
|
||||
[self kb_updateCurrentWordWithInsertedText:text];
|
||||
} break;
|
||||
case KBKeyTypeBackspace:
|
||||
[self.textDocumentProxy deleteBackward]; break;
|
||||
[self.textDocumentProxy deleteBackward];
|
||||
[self kb_scheduleContextRefreshResetSuppression:NO];
|
||||
break;
|
||||
case KBKeyTypeSpace:
|
||||
[self.textDocumentProxy insertText:@" "]; break;
|
||||
[self.textDocumentProxy insertText:@" "];
|
||||
[self kb_clearCurrentWord];
|
||||
break;
|
||||
case KBKeyTypeReturn:
|
||||
[self.textDocumentProxy insertText:@"\n"]; break;
|
||||
[self.textDocumentProxy insertText:@"\n"];
|
||||
[self kb_clearCurrentWord];
|
||||
break;
|
||||
case KBKeyTypeGlobe:
|
||||
[self advanceToNextInputMode]; break;
|
||||
case KBKeyTypeCustom:
|
||||
// 点击自定义键切换到功能面板
|
||||
[self showFunctionPanel:YES];
|
||||
[self kb_clearCurrentWord];
|
||||
break;
|
||||
case KBKeyTypeModeChange:
|
||||
case KBKeyTypeShift:
|
||||
@@ -278,6 +401,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
||||
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapToolActionAtIndex:(NSInteger)index {
|
||||
if (index == 0) {
|
||||
[self showFunctionPanel:YES];
|
||||
[self kb_clearCurrentWord];
|
||||
return;
|
||||
}
|
||||
[self showFunctionPanel:NO];
|
||||
@@ -291,16 +415,33 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
||||
if (emoji.length == 0) { return; }
|
||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||
[self.textDocumentProxy insertText:emoji];
|
||||
[self kb_clearCurrentWord];
|
||||
}
|
||||
|
||||
- (void)keyBoardMainViewDidTapUndo:(KBKeyBoardMainView *)keyBoardMainView {
|
||||
[[KBBackspaceUndoManager shared] performUndoFromResponder:self.view];
|
||||
[self kb_scheduleContextRefreshResetSuppression:YES];
|
||||
}
|
||||
|
||||
- (void)keyBoardMainViewDidTapEmojiSearch:(KBKeyBoardMainView *)keyBoardMainView {
|
||||
[KBHUD showInfo:KBLocalized(@"Search coming soon")];
|
||||
}
|
||||
|
||||
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectSuggestion:(NSString *)suggestion {
|
||||
if (suggestion.length == 0) { return; }
|
||||
NSString *current = self.currentWord ?: @"";
|
||||
if (current.length > 0) {
|
||||
for (NSUInteger i = 0; i < current.length; i++) {
|
||||
[self.textDocumentProxy deleteBackward];
|
||||
}
|
||||
}
|
||||
[self.textDocumentProxy insertText:suggestion];
|
||||
self.currentWord = suggestion;
|
||||
[self.suggestionEngine recordSelection:suggestion];
|
||||
self.suppressSuggestions = YES;
|
||||
[self.keyBoardMainView kb_setSuggestions:@[]];
|
||||
}
|
||||
|
||||
// MARK: - KBFunctionViewDelegate
|
||||
- (void)functionView:(KBFunctionView *)functionView didTapToolActionAtIndex:(NSInteger)index {
|
||||
// 需求:当 index == 0 时,切回键盘主视图
|
||||
|
||||
23
CustomKeyboard/Manager/KBSuggestionEngine.h
Normal file
23
CustomKeyboard/Manager/KBSuggestionEngine.h
Normal file
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// KBSuggestionEngine.h
|
||||
// CustomKeyboard
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// Simple local suggestion engine (prefix match + lightweight ranking).
|
||||
@interface KBSuggestionEngine : NSObject
|
||||
|
||||
+ (instancetype)shared;
|
||||
|
||||
/// Returns suggestions for prefix (lowercase expected), limited by count.
|
||||
- (NSArray<NSString *> *)suggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit;
|
||||
|
||||
/// Record a selection to slightly boost ranking next time.
|
||||
- (void)recordSelection:(NSString *)word;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
167
CustomKeyboard/Manager/KBSuggestionEngine.m
Normal file
167
CustomKeyboard/Manager/KBSuggestionEngine.m
Normal file
@@ -0,0 +1,167 @@
|
||||
//
|
||||
// KBSuggestionEngine.m
|
||||
// CustomKeyboard
|
||||
//
|
||||
|
||||
#import "KBSuggestionEngine.h"
|
||||
#import "KBConfig.h"
|
||||
|
||||
@interface KBSuggestionEngine ()
|
||||
@property (nonatomic, copy) NSArray<NSString *> *words;
|
||||
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *selectionCounts;
|
||||
@property (nonatomic, strong) NSSet<NSString *> *priorityWords;
|
||||
@end
|
||||
|
||||
@implementation KBSuggestionEngine
|
||||
|
||||
+ (instancetype)shared {
|
||||
static KBSuggestionEngine *engine;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
engine = [[KBSuggestionEngine alloc] init];
|
||||
});
|
||||
return engine;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
_selectionCounts = [NSMutableDictionary dictionary];
|
||||
NSArray<NSString *> *defaults = [self.class kb_defaultWords];
|
||||
_priorityWords = [NSSet setWithArray:defaults];
|
||||
_words = [self kb_loadWords];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)suggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit {
|
||||
if (prefix.length == 0 || limit == 0) { return @[]; }
|
||||
NSString *lower = prefix.lowercaseString;
|
||||
NSMutableArray<NSString *> *matches = [NSMutableArray array];
|
||||
|
||||
for (NSString *word in self.words) {
|
||||
if ([word hasPrefix:lower]) {
|
||||
[matches addObject:word];
|
||||
if (matches.count >= limit * 3) {
|
||||
// Avoid scanning too many matches for long lists.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matches.count == 0) { return @[]; }
|
||||
|
||||
[matches sortUsingComparator:^NSComparisonResult(NSString *a, NSString *b) {
|
||||
NSInteger ca = self.selectionCounts[a].integerValue;
|
||||
NSInteger cb = self.selectionCounts[b].integerValue;
|
||||
if (ca != cb) {
|
||||
return (cb > ca) ? NSOrderedAscending : NSOrderedDescending;
|
||||
}
|
||||
BOOL pa = [self.priorityWords containsObject:a];
|
||||
BOOL pb = [self.priorityWords containsObject:b];
|
||||
if (pa != pb) {
|
||||
return pa ? NSOrderedAscending : NSOrderedDescending;
|
||||
}
|
||||
return [a compare:b];
|
||||
}];
|
||||
|
||||
if (matches.count > limit) {
|
||||
return [matches subarrayWithRange:NSMakeRange(0, limit)];
|
||||
}
|
||||
return matches.copy;
|
||||
}
|
||||
|
||||
- (void)recordSelection:(NSString *)word {
|
||||
if (word.length == 0) { return; }
|
||||
NSString *key = word.lowercaseString;
|
||||
NSInteger count = self.selectionCounts[key].integerValue + 1;
|
||||
self.selectionCounts[key] = @(count);
|
||||
}
|
||||
|
||||
#pragma mark - Defaults
|
||||
|
||||
- (NSArray<NSString *> *)kb_loadWords {
|
||||
NSMutableOrderedSet<NSString *> *set = [[NSMutableOrderedSet alloc] init];
|
||||
[set addObjectsFromArray:[self.class kb_defaultWords]];
|
||||
|
||||
NSArray<NSString *> *paths = [self kb_wordListPaths];
|
||||
for (NSString *path in paths) {
|
||||
if (path.length == 0) { continue; }
|
||||
NSString *content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
|
||||
if (content.length == 0) { continue; }
|
||||
NSArray<NSString *> *lines = [content componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
|
||||
for (NSString *line in lines) {
|
||||
NSString *word = [self kb_sanitizedWordFromLine:line];
|
||||
if (word.length == 0) { continue; }
|
||||
[set addObject:word];
|
||||
}
|
||||
}
|
||||
|
||||
NSArray<NSString *> *result = set.array ?: @[];
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)kb_wordListPaths {
|
||||
NSMutableArray<NSString *> *paths = [NSMutableArray array];
|
||||
// 1) App Group override (allows server-downloaded large list).
|
||||
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup];
|
||||
if (containerURL.path.length > 0) {
|
||||
NSString *groupPath = [[containerURL path] stringByAppendingPathComponent:@"kb_words.txt"];
|
||||
[paths addObject:groupPath];
|
||||
}
|
||||
// 2) Bundle fallback.
|
||||
NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"kb_words" ofType:@"txt"];
|
||||
if (bundlePath.length > 0) {
|
||||
[paths addObject:bundlePath];
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
- (NSString *)kb_sanitizedWordFromLine:(NSString *)line {
|
||||
NSString *trimmed = [[line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] lowercaseString];
|
||||
if (trimmed.length == 0) { return @""; }
|
||||
static NSCharacterSet *letters = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
letters = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyz"];
|
||||
});
|
||||
for (NSUInteger i = 0; i < trimmed.length; i++) {
|
||||
if (![letters characterIsMember:[trimmed characterAtIndex:i]]) {
|
||||
return @"";
|
||||
}
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
+ (NSArray<NSString *> *)kb_defaultWords {
|
||||
return @[
|
||||
@"a", @"an", @"and", @"are", @"as", @"at",
|
||||
@"app", @"ap", @"apple", @"apply", @"april", @"application",
|
||||
@"about", @"above", @"after", @"again", @"against", @"all",
|
||||
@"am", @"among", @"amount", @"any", @"around",
|
||||
@"be", @"because", @"been", @"before", @"being", @"below",
|
||||
@"best", @"between", @"both", @"but", @"by",
|
||||
@"can", @"could", @"come", @"common", @"case",
|
||||
@"do", @"does", @"down", @"day",
|
||||
@"each", @"early", @"end", @"even", @"every",
|
||||
@"for", @"from", @"first", @"found", @"free",
|
||||
@"get", @"good", @"great", @"go",
|
||||
@"have", @"has", @"had", @"help", @"how",
|
||||
@"in", @"is", @"it", @"if", @"into",
|
||||
@"just", @"keep", @"kind", @"know",
|
||||
@"like", @"look", @"long", @"last",
|
||||
@"make", @"more", @"most", @"my",
|
||||
@"new", @"no", @"not", @"now",
|
||||
@"of", @"on", @"one", @"or", @"other", @"our", @"out",
|
||||
@"people", @"place", @"please",
|
||||
@"quick", @"quite",
|
||||
@"right", @"read", @"real",
|
||||
@"see", @"say", @"some", @"such", @"so",
|
||||
@"the", @"to", @"this", @"that", @"them", @"then", @"there", @"they", @"these", @"time",
|
||||
@"use", @"up", @"under",
|
||||
@"very",
|
||||
@"we", @"with", @"what", @"when", @"where", @"who", @"why", @"will", @"would",
|
||||
@"you", @"your"
|
||||
];
|
||||
}
|
||||
|
||||
@end
|
||||
234454
CustomKeyboard/Resource/kb_words.txt
Normal file
234454
CustomKeyboard/Resource/kb_words.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// emoji 面板点击搜索
|
||||
- (void)keyBoardMainViewDidTapEmojiSearch:(KBKeyBoardMainView *)keyBoardMainView;
|
||||
|
||||
/// 选择了联想词
|
||||
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectSuggestion:(NSString *)suggestion;
|
||||
@end
|
||||
|
||||
@interface KBKeyBoardMainView : UIView
|
||||
@@ -39,6 +42,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
/// 应用当前皮肤(会触发键区重载以应用按键颜色)
|
||||
- (void)kb_applyTheme;
|
||||
|
||||
/// 更新联想候选
|
||||
- (void)kb_setSuggestions:(NSArray<NSString *> *)suggestions;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -11,14 +11,17 @@
|
||||
#import "KBFunctionView.h"
|
||||
#import "KBKey.h"
|
||||
#import "KBEmojiPanelView.h"
|
||||
#import "KBSuggestionBarView.h"
|
||||
#import "Masonry.h"
|
||||
#import "KBSkinManager.h"
|
||||
|
||||
@interface KBKeyBoardMainView ()<KBToolBarDelegate, KBKeyboardViewDelegate, KBEmojiPanelViewDelegate>
|
||||
@interface KBKeyBoardMainView ()<KBToolBarDelegate, KBKeyboardViewDelegate, KBEmojiPanelViewDelegate, KBSuggestionBarViewDelegate>
|
||||
@property (nonatomic, strong) KBToolBar *topBar;
|
||||
@property (nonatomic, strong) KBSuggestionBarView *suggestionBar;
|
||||
@property (nonatomic, strong) KBKeyboardView *keyboardView;
|
||||
@property (nonatomic, strong) KBEmojiPanelView *emojiView;
|
||||
@property (nonatomic, assign) BOOL emojiPanelVisible;
|
||||
@property (nonatomic, assign) BOOL suggestionBarHasItems;
|
||||
// 注意:功能面板的展示/隐藏由外部控制器决定,此处不再直接管理显隐
|
||||
@end
|
||||
@implementation KBKeyBoardMainView
|
||||
@@ -31,9 +34,17 @@
|
||||
self.topBar = [[KBToolBar alloc] init];
|
||||
self.topBar.delegate = self;
|
||||
[self addSubview:self.topBar];
|
||||
|
||||
// 联想栏
|
||||
self.suggestionBar = [[KBSuggestionBarView alloc] init];
|
||||
self.suggestionBar.delegate = self;
|
||||
self.suggestionBar.hidden = YES;
|
||||
[self addSubview:self.suggestionBar];
|
||||
|
||||
// 键盘区域(高度按照设计值做等比缩放,避免不同机型上按键被压缩/拉伸)
|
||||
CGFloat keyboardAreaHeight = KBFit(200.0f);
|
||||
CGFloat bottomInset = KBFit(4.0f);
|
||||
CGFloat topBarHeight = KBFit(40.0f);
|
||||
CGFloat barSpacing = KBFit(6.0f);
|
||||
|
||||
self.keyboardView = [[KBKeyboardView alloc] init];
|
||||
@@ -57,7 +68,17 @@
|
||||
[self.topBar mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.right.equalTo(self);
|
||||
make.top.equalTo(self.mas_top).offset(0);
|
||||
make.bottom.equalTo(self.keyboardView.mas_top).offset(0);
|
||||
make.height.mas_equalTo(topBarHeight);
|
||||
}];
|
||||
|
||||
[self.suggestionBar mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.right.equalTo(self);
|
||||
make.top.equalTo(self.topBar);
|
||||
make.bottom.equalTo(self.topBar);
|
||||
}];
|
||||
|
||||
[self.keyboardView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.topBar.mas_bottom).offset(barSpacing);
|
||||
}];
|
||||
// 功能面板切换交由外部控制器处理;此处不直接创建/管理
|
||||
}
|
||||
@@ -74,17 +95,24 @@
|
||||
} else {
|
||||
self.keyboardView.hidden = NO;
|
||||
self.topBar.hidden = NO;
|
||||
self.suggestionBar.hidden = !self.suggestionBarHasItems;
|
||||
}
|
||||
|
||||
void (^changes)(void) = ^{
|
||||
self.emojiView.alpha = visible ? 1.0 : 0.0;
|
||||
self.keyboardView.alpha = visible ? 0.0 : 1.0;
|
||||
self.topBar.alpha = visible ? 0.0 : 1.0;
|
||||
self.suggestionBar.alpha = visible ? 0.0 : (self.suggestionBarHasItems ? 1.0 : 0.0);
|
||||
};
|
||||
void (^completion)(BOOL) = ^(BOOL finished) {
|
||||
self.emojiView.hidden = !visible;
|
||||
self.keyboardView.hidden = visible;
|
||||
self.topBar.hidden = visible;
|
||||
if (visible) {
|
||||
self.suggestionBar.hidden = YES;
|
||||
} else {
|
||||
self.suggestionBar.hidden = !self.suggestionBarHasItems;
|
||||
}
|
||||
};
|
||||
|
||||
if (animated) {
|
||||
@@ -211,10 +239,34 @@
|
||||
if ([self.topBar respondsToSelector:@selector(kb_applyTheme)]) {
|
||||
[self.topBar kb_applyTheme];
|
||||
}
|
||||
[self.suggestionBar applyTheme:mgr.current];
|
||||
[self.keyboardView reloadKeys];
|
||||
if (self.emojiView) {
|
||||
[self.emojiView applyTheme:mgr.current];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Suggestions
|
||||
|
||||
- (void)kb_setSuggestions:(NSArray<NSString *> *)suggestions {
|
||||
self.suggestionBarHasItems = (suggestions.count > 0);
|
||||
[self.suggestionBar updateSuggestions:suggestions];
|
||||
|
||||
if (self.emojiPanelVisible) {
|
||||
self.suggestionBar.hidden = YES;
|
||||
self.suggestionBar.alpha = 0.0;
|
||||
} else {
|
||||
self.suggestionBar.hidden = !self.suggestionBarHasItems;
|
||||
self.suggestionBar.alpha = self.suggestionBarHasItems ? 1.0 : 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - KBSuggestionBarViewDelegate
|
||||
|
||||
- (void)suggestionBarView:(KBSuggestionBarView *)view didSelectSuggestion:(NSString *)suggestion {
|
||||
if ([self.delegate respondsToSelector:@selector(keyBoardMainView:didSelectSuggestion:)]) {
|
||||
[self.delegate keyBoardMainView:self didSelectSuggestion:suggestion];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
26
CustomKeyboard/View/KBSuggestionBarView.h
Normal file
26
CustomKeyboard/View/KBSuggestionBarView.h
Normal file
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// KBSuggestionBarView.h
|
||||
// CustomKeyboard
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class KBSuggestionBarView;
|
||||
@class KBSkinTheme;
|
||||
|
||||
@protocol KBSuggestionBarViewDelegate <NSObject>
|
||||
- (void)suggestionBarView:(KBSuggestionBarView *)view didSelectSuggestion:(NSString *)suggestion;
|
||||
@end
|
||||
|
||||
@interface KBSuggestionBarView : UIView
|
||||
|
||||
@property (nonatomic, weak) id<KBSuggestionBarViewDelegate> delegate;
|
||||
|
||||
- (void)updateSuggestions:(NSArray<NSString *> *)suggestions;
|
||||
- (void)applyTheme:(KBSkinTheme *)theme;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
114
CustomKeyboard/View/KBSuggestionBarView.m
Normal file
114
CustomKeyboard/View/KBSuggestionBarView.m
Normal file
@@ -0,0 +1,114 @@
|
||||
//
|
||||
// KBSuggestionBarView.m
|
||||
// CustomKeyboard
|
||||
//
|
||||
|
||||
#import "KBSuggestionBarView.h"
|
||||
#import "Masonry.h"
|
||||
#import "KBSkinManager.h"
|
||||
|
||||
@interface KBSuggestionBarView ()
|
||||
@property (nonatomic, strong) UIScrollView *scrollView;
|
||||
@property (nonatomic, strong) UIStackView *stackView;
|
||||
@property (nonatomic, copy) NSArray<NSString *> *items;
|
||||
@property (nonatomic, strong) UIColor *pillColor;
|
||||
@property (nonatomic, strong) UIColor *textColor;
|
||||
@end
|
||||
|
||||
@implementation KBSuggestionBarView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
[self setupUI];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setupUI {
|
||||
[self addSubview:self.scrollView];
|
||||
[self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self);
|
||||
}];
|
||||
|
||||
[self.scrollView addSubview:self.stackView];
|
||||
[self.stackView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self.scrollView).insets(UIEdgeInsetsMake(0, 8, 0, 8));
|
||||
make.height.equalTo(self.scrollView);
|
||||
}];
|
||||
|
||||
[self applyTheme:[KBSkinManager shared].current];
|
||||
}
|
||||
|
||||
- (void)updateSuggestions:(NSArray<NSString *> *)suggestions {
|
||||
self.items = suggestions ?: @[];
|
||||
|
||||
for (UIView *view in self.stackView.arrangedSubviews) {
|
||||
[self.stackView removeArrangedSubview:view];
|
||||
[view removeFromSuperview];
|
||||
}
|
||||
|
||||
for (NSString *item in self.items) {
|
||||
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
btn.layer.cornerRadius = 12.0;
|
||||
btn.layer.masksToBounds = YES;
|
||||
btn.backgroundColor = self.pillColor ?: [UIColor colorWithWhite:1 alpha:0.9];
|
||||
btn.titleLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
|
||||
[btn setTitle:item forState:UIControlStateNormal];
|
||||
[btn setTitleColor:self.textColor ?: [UIColor blackColor] forState:UIControlStateNormal];
|
||||
btn.contentEdgeInsets = UIEdgeInsetsMake(4, 10, 4, 10);
|
||||
[btn addTarget:self action:@selector(onTapSuggestion:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.stackView addArrangedSubview:btn];
|
||||
}
|
||||
|
||||
self.hidden = (self.items.count == 0);
|
||||
}
|
||||
|
||||
- (void)applyTheme:(KBSkinTheme *)theme {
|
||||
UIColor *bg = theme.keyBackground ?: [UIColor whiteColor];
|
||||
UIColor *text = theme.keyTextColor ?: [UIColor blackColor];
|
||||
UIColor *barBg = theme.keyboardBackground ?: [UIColor colorWithWhite:0.95 alpha:1.0];
|
||||
self.backgroundColor = barBg;
|
||||
self.pillColor = bg;
|
||||
self.textColor = text;
|
||||
|
||||
for (UIView *view in self.stackView.arrangedSubviews) {
|
||||
if (![view isKindOfClass:[UIButton class]]) { continue; }
|
||||
UIButton *btn = (UIButton *)view;
|
||||
btn.backgroundColor = self.pillColor;
|
||||
[btn setTitleColor:self.textColor forState:UIControlStateNormal];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)onTapSuggestion:(UIButton *)sender {
|
||||
NSString *title = sender.currentTitle ?: @"";
|
||||
if (title.length == 0) { return; }
|
||||
if ([self.delegate respondsToSelector:@selector(suggestionBarView:didSelectSuggestion:)]) {
|
||||
[self.delegate suggestionBarView:self didSelectSuggestion:title];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (UIScrollView *)scrollView {
|
||||
if (!_scrollView) {
|
||||
_scrollView = [[UIScrollView alloc] init];
|
||||
_scrollView.showsHorizontalScrollIndicator = NO;
|
||||
_scrollView.alwaysBounceHorizontal = YES;
|
||||
}
|
||||
return _scrollView;
|
||||
}
|
||||
|
||||
- (UIStackView *)stackView {
|
||||
if (!_stackView) {
|
||||
_stackView = [[UIStackView alloc] init];
|
||||
_stackView.axis = UILayoutConstraintAxisHorizontal;
|
||||
_stackView.alignment = UIStackViewAlignmentCenter;
|
||||
_stackView.spacing = 8.0;
|
||||
}
|
||||
return _stackView;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -172,6 +172,9 @@
|
||||
04C6EACE2EAF87020089C901 /* CustomKeyboard.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 04C6EAC62EAF87020089C901 /* CustomKeyboard.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
04C6EAD82EAF870B0089C901 /* KeyboardViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 04C6EAD62EAF870B0089C901 /* KeyboardViewController.m */; };
|
||||
04C6EADD2EAF8CEB0089C901 /* KBToolBar.m in Sources */ = {isa = PBXBuildFile; fileRef = 04C6EADC2EAF8CEB0089C901 /* KBToolBar.m */; };
|
||||
A1B2C3ED2F20000000000001 /* kb_words.txt in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3EC2F20000000000001 /* kb_words.txt */; };
|
||||
A1B2C3EA2F20000000000001 /* KBSuggestionEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3E72F20000000000001 /* KBSuggestionEngine.m */; };
|
||||
A1B2C3EB2F20000000000001 /* KBSuggestionBarView.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3E92F20000000000001 /* KBSuggestionBarView.m */; };
|
||||
04D1F6B22EDFF10A00B12345 /* KBSkinInstallBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D1F6B12EDFF10A00B12345 /* KBSkinInstallBridge.m */; };
|
||||
04D1F6B32EDFF10A00B12345 /* KBSkinInstallBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D1F6B12EDFF10A00B12345 /* KBSkinInstallBridge.m */; };
|
||||
04FC95672EB0546C007BD342 /* KBKey.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95652EB0546C007BD342 /* KBKey.m */; };
|
||||
@@ -503,6 +506,10 @@
|
||||
04A9A67D2EB9E1690023B8F4 /* KBResponderUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBResponderUtils.h; sourceTree = "<group>"; };
|
||||
04A9FE102EB4D0D20020DB6D /* KBFullAccessManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFullAccessManager.h; sourceTree = "<group>"; };
|
||||
04A9FE112EB4D0D20020DB6D /* KBFullAccessManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFullAccessManager.m; sourceTree = "<group>"; };
|
||||
A1B2C3E62F20000000000001 /* KBSuggestionEngine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSuggestionEngine.h; sourceTree = "<group>"; };
|
||||
A1B2C3E72F20000000000001 /* KBSuggestionEngine.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSuggestionEngine.m; sourceTree = "<group>"; };
|
||||
A1B2C3E82F20000000000001 /* KBSuggestionBarView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSuggestionBarView.h; sourceTree = "<group>"; };
|
||||
A1B2C3E92F20000000000001 /* KBSuggestionBarView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSuggestionBarView.m; sourceTree = "<group>"; };
|
||||
04A9FE142EB873C80020DB6D /* UIViewController+Extension.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIViewController+Extension.h"; sourceTree = "<group>"; };
|
||||
04A9FE152EB873C80020DB6D /* UIViewController+Extension.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+Extension.m"; sourceTree = "<group>"; };
|
||||
04A9FE182EB892460020DB6D /* KBLocalizationManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBLocalizationManager.h; sourceTree = "<group>"; };
|
||||
@@ -524,6 +531,7 @@
|
||||
04C6EAD42EAF870B0089C901 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
04C6EAD52EAF870B0089C901 /* KeyboardViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyboardViewController.h; sourceTree = "<group>"; };
|
||||
04C6EAD62EAF870B0089C901 /* KeyboardViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KeyboardViewController.m; sourceTree = "<group>"; };
|
||||
A1B2C3EC2F20000000000001 /* kb_words.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = kb_words.txt; sourceTree = "<group>"; };
|
||||
04C6EADB2EAF8CEB0089C901 /* KBToolBar.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBToolBar.h; sourceTree = "<group>"; };
|
||||
04C6EADC2EAF8CEB0089C901 /* KBToolBar.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBToolBar.m; sourceTree = "<group>"; };
|
||||
04C6EADE2EAF8D680089C901 /* PrefixHeader.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PrefixHeader.pch; sourceTree = "<group>"; };
|
||||
@@ -657,6 +665,7 @@
|
||||
041007D02ECE010100D203BB /* Resource */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A1B2C3EC2F20000000000001 /* kb_words.txt */,
|
||||
0498BDF42EEC50EE006CC1D5 /* emoji_categories.json */,
|
||||
041007D12ECE012000D203BB /* KBSkinIconMap.strings */,
|
||||
041007D32ECE012500D203BB /* 002.zip */,
|
||||
@@ -1086,6 +1095,8 @@
|
||||
children = (
|
||||
04A9FE102EB4D0D20020DB6D /* KBFullAccessManager.h */,
|
||||
04A9FE112EB4D0D20020DB6D /* KBFullAccessManager.m */,
|
||||
A1B2C3E62F20000000000001 /* KBSuggestionEngine.h */,
|
||||
A1B2C3E72F20000000000001 /* KBSuggestionEngine.m */,
|
||||
04FEDA9F2EEDB00100123456 /* KBEmojiDataProvider.h */,
|
||||
04FEDAA02EEDB00100123456 /* KBEmojiDataProvider.m */,
|
||||
);
|
||||
@@ -1154,6 +1165,8 @@
|
||||
046131132ECF454500A6FADF /* KBKeyPreviewView.m */,
|
||||
04FC95772EB09BC8007BD342 /* KBKeyBoardMainView.h */,
|
||||
04FC95782EB09BC8007BD342 /* KBKeyBoardMainView.m */,
|
||||
A1B2C3E82F20000000000001 /* KBSuggestionBarView.h */,
|
||||
A1B2C3E92F20000000000001 /* KBSuggestionBarView.m */,
|
||||
04FC956E2EB09516007BD342 /* KBFunctionView.h */,
|
||||
04FC956F2EB09516007BD342 /* KBFunctionView.m */,
|
||||
04FC95712EB09570007BD342 /* KBFunctionBarView.h */,
|
||||
@@ -1773,6 +1786,7 @@
|
||||
04A9FE202EB893F10020DB6D /* Localizable.strings in Resources */,
|
||||
041007D42ECE012500D203BB /* 002.zip in Resources */,
|
||||
041007D22ECE012000D203BB /* KBSkinIconMap.strings in Resources */,
|
||||
A1B2C3ED2F20000000000001 /* kb_words.txt in Resources */,
|
||||
04791FFB2ED5EAB8004E8522 /* fense.zip in Resources */,
|
||||
0498BDF52EEC50EE006CC1D5 /* emoji_categories.json in Resources */,
|
||||
04791FF72ED5B985004E8522 /* Christmas.zip in Resources */,
|
||||
@@ -1877,6 +1891,7 @@
|
||||
0450AC4A2EF2C3ED00B6AF06 /* KBKeyboardSubscriptionOptionCell.m in Sources */,
|
||||
04A9FE0F2EB481100020DB6D /* KBHUD.m in Sources */,
|
||||
04C6EADD2EAF8CEB0089C901 /* KBToolBar.m in Sources */,
|
||||
A1B2C3EB2F20000000000001 /* KBSuggestionBarView.m in Sources */,
|
||||
04FC95792EB09BC8007BD342 /* KBKeyBoardMainView.m in Sources */,
|
||||
04FEDAB32EEDB05000123456 /* KBEmojiPanelView.m in Sources */,
|
||||
04FEDB032EFE000000123456 /* KBEmojiBottomBarView.m in Sources */,
|
||||
@@ -1889,6 +1904,7 @@
|
||||
04FC95762EB095DE007BD342 /* KBFunctionPasteView.m in Sources */,
|
||||
A1B2C3D42EB0A0A100000001 /* KBFunctionTagCell.m in Sources */,
|
||||
04A9FE1A2EB892460020DB6D /* KBLocalizationManager.m in Sources */,
|
||||
A1B2C3EA2F20000000000001 /* KBSuggestionEngine.m in Sources */,
|
||||
A1B2C3E22EB0C0A100000001 /* KBNetworkManager.m in Sources */,
|
||||
049FB2352EC45C6A00FAB05D /* NetworkStreamHandler.m in Sources */,
|
||||
04FC956A2EB05497007BD342 /* KBKeyButton.m in Sources */,
|
||||
|
||||
Reference in New Issue
Block a user