377 lines
15 KiB
Mathematica
377 lines
15 KiB
Mathematica
|
|
//
|
|||
|
|
// KeyboardViewController+Theme.m
|
|||
|
|
// CustomKeyboard
|
|||
|
|
//
|
|||
|
|
// Created by Codex on 2026/02/22.
|
|||
|
|
//
|
|||
|
|
|
|||
|
|
#import "KeyboardViewController+Private.h"
|
|||
|
|
|
|||
|
|
#import "KBFunctionView.h"
|
|||
|
|
#import "KBKeyBoardMainView.h"
|
|||
|
|
#import "KBSkinInstallBridge.h"
|
|||
|
|
#import "KBSkinManager.h"
|
|||
|
|
#import "UIImage+KBColor.h"
|
|||
|
|
#import <QuartzCore/QuartzCore.h>
|
|||
|
|
|
|||
|
|
static NSString *const kKBDefaultSkinIdLight = @"normal_them";
|
|||
|
|
static NSString *const kKBDefaultSkinZipNameLight = @"normal_them";
|
|||
|
|
static NSString *const kKBDefaultSkinIdDark = @"normal_hei_them";
|
|||
|
|
static NSString *const kKBDefaultSkinZipNameDark = @"normal_hei_them";
|
|||
|
|
|
|||
|
|
// 提前声明一个类别,使编译器在 static 回调中识别 kb_consumePendingShopSkin 方法。
|
|||
|
|
@interface KeyboardViewController (KBSkinShopBridge)
|
|||
|
|
- (void)kb_consumePendingShopSkin;
|
|||
|
|
@end
|
|||
|
|
|
|||
|
|
static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||
|
|
void *observer, CFStringRef name,
|
|||
|
|
const void *object,
|
|||
|
|
CFDictionaryRef userInfo) {
|
|||
|
|
KeyboardViewController *strongSelf =
|
|||
|
|
(__bridge KeyboardViewController *)observer;
|
|||
|
|
if (!strongSelf) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|||
|
|
if ([strongSelf respondsToSelector:@selector(kb_consumePendingShopSkin)]) {
|
|||
|
|
[strongSelf kb_consumePendingShopSkin];
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@implementation KeyboardViewController (Theme)
|
|||
|
|
|
|||
|
|
- (void)kb_registerDarwinSkinInstallObserver {
|
|||
|
|
CFNotificationCenterAddObserver(
|
|||
|
|
CFNotificationCenterGetDarwinNotifyCenter(),
|
|||
|
|
(__bridge const void *)(self), KBSkinInstallNotificationCallback,
|
|||
|
|
(__bridge CFStringRef)KBDarwinSkinInstallRequestNotification, NULL,
|
|||
|
|
CFNotificationSuspensionBehaviorDeliverImmediately);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
- (void)kb_unregisterDarwinSkinInstallObserver {
|
|||
|
|
CFNotificationCenterRemoveObserver(
|
|||
|
|
CFNotificationCenterGetDarwinNotifyCenter(),
|
|||
|
|
(__bridge const void *)(self),
|
|||
|
|
(__bridge CFStringRef)KBDarwinSkinInstallRequestNotification, NULL);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
- (void)kb_applyTheme {
|
|||
|
|
@autoreleasepool {
|
|||
|
|
KBSkinTheme *t = [KBSkinManager shared].current;
|
|||
|
|
UIImage *img = nil;
|
|||
|
|
BOOL isDefaultTheme = [self kb_isDefaultKeyboardTheme:t];
|
|||
|
|
BOOL isDarkMode = [self kb_isDarkModeActive];
|
|||
|
|
|
|||
|
|
NSString *skinId = t.skinId ?: @"";
|
|||
|
|
NSString *themeKey =
|
|||
|
|
[NSString stringWithFormat:@"%@|default=%d|dark=%d", skinId,
|
|||
|
|
isDefaultTheme, isDarkMode];
|
|||
|
|
BOOL themeChanged =
|
|||
|
|
(self.kb_lastAppliedThemeKey.length == 0 ||
|
|||
|
|
![self.kb_lastAppliedThemeKey isEqualToString:themeKey]);
|
|||
|
|
if (themeChanged) {
|
|||
|
|
self.kb_lastAppliedThemeKey = themeKey;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
CGSize size = self.bgImageView.bounds.size;
|
|||
|
|
if (isDefaultTheme) {
|
|||
|
|
if (isDarkMode) {
|
|||
|
|
// 暗黑模式:直接使用背景色,不使用图片渲染
|
|||
|
|
// 这样可以避免图片渲染时的色彩空间转换导致颜色不一致
|
|||
|
|
img = nil;
|
|||
|
|
self.bgImageView.image = nil;
|
|||
|
|
[self.kb_defaultGradientLayer removeFromSuperlayer];
|
|||
|
|
self.kb_defaultGradientLayer = nil;
|
|||
|
|
// 使用与系统键盘底部完全相同的颜色
|
|||
|
|
if (@available(iOS 13.0, *)) {
|
|||
|
|
// iOS 系统键盘使用的实际颜色 (RGB: 44, 44, 46 in sRGB, 或 #2C2C2E)
|
|||
|
|
// 但为了完美匹配,我们使用动态颜色并直接设置为背景
|
|||
|
|
UIColor *kbBgColor =
|
|||
|
|
[UIColor colorWithDynamicProvider:^UIColor *_Nonnull(
|
|||
|
|
UITraitCollection *_Nonnull traitCollection) {
|
|||
|
|
if (traitCollection.userInterfaceStyle ==
|
|||
|
|
UIUserInterfaceStyleDark) {
|
|||
|
|
// 暗黑模式下系统键盘实际背景色
|
|||
|
|
return [UIColor colorWithRed:43.0 / 255.0
|
|||
|
|
green:43.0 / 255.0
|
|||
|
|
blue:43.0 / 255.0
|
|||
|
|
alpha:1.0];
|
|||
|
|
} else {
|
|||
|
|
return [UIColor colorWithRed:209.0 / 255.0
|
|||
|
|
green:211.0 / 255.0
|
|||
|
|
blue:219.0 / 255.0
|
|||
|
|
alpha:1.0];
|
|||
|
|
}
|
|||
|
|
}];
|
|||
|
|
self.contentView.backgroundColor = kbBgColor;
|
|||
|
|
self.bgImageView.backgroundColor = kbBgColor;
|
|||
|
|
} else {
|
|||
|
|
UIColor *darkColor = [UIColor colorWithRed:43.0 / 255.0
|
|||
|
|
green:43.0 / 255.0
|
|||
|
|
blue:43.0 / 255.0
|
|||
|
|
alpha:1.0];
|
|||
|
|
self.contentView.backgroundColor = darkColor;
|
|||
|
|
self.bgImageView.backgroundColor = darkColor;
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 浅色模式:使用渐变层(避免生成大位图导致内存上涨)
|
|||
|
|
if (size.width <= 0 || size.height <= 0) {
|
|||
|
|
[self.view layoutIfNeeded];
|
|||
|
|
size = self.bgImageView.bounds.size;
|
|||
|
|
}
|
|||
|
|
if (size.width <= 0 || size.height <= 0) {
|
|||
|
|
size = self.view.bounds.size;
|
|||
|
|
}
|
|||
|
|
if (size.width <= 0 || size.height <= 0) {
|
|||
|
|
size = [UIScreen mainScreen].bounds.size;
|
|||
|
|
}
|
|||
|
|
UIColor *topColor = [UIColor colorWithHex:0xDEDFE4];
|
|||
|
|
UIColor *bottomColor = [UIColor colorWithHex:0xD1D3DB];
|
|||
|
|
UIColor *resolvedTopColor = topColor;
|
|||
|
|
UIColor *resolvedBottomColor = bottomColor;
|
|||
|
|
if (@available(iOS 13.0, *)) {
|
|||
|
|
resolvedTopColor =
|
|||
|
|
[topColor resolvedColorWithTraitCollection:self.traitCollection];
|
|||
|
|
resolvedBottomColor =
|
|||
|
|
[bottomColor resolvedColorWithTraitCollection:self.traitCollection];
|
|||
|
|
}
|
|||
|
|
CAGradientLayer *layer = self.kb_defaultGradientLayer;
|
|||
|
|
if (!layer) {
|
|||
|
|
layer = [CAGradientLayer layer];
|
|||
|
|
layer.startPoint = CGPointMake(0.5, 0.0);
|
|||
|
|
layer.endPoint = CGPointMake(0.5, 1.0);
|
|||
|
|
[self.bgImageView.layer insertSublayer:layer atIndex:0];
|
|||
|
|
self.kb_defaultGradientLayer = layer;
|
|||
|
|
}
|
|||
|
|
layer.colors =
|
|||
|
|
@[ (id)resolvedTopColor.CGColor, (id)resolvedBottomColor.CGColor ];
|
|||
|
|
layer.frame = (CGRect){CGPointZero, size};
|
|||
|
|
img = nil;
|
|||
|
|
self.bgImageView.image = nil;
|
|||
|
|
self.contentView.backgroundColor = [UIColor clearColor];
|
|||
|
|
self.bgImageView.backgroundColor = [UIColor clearColor];
|
|||
|
|
}
|
|||
|
|
NSLog(@"===");
|
|||
|
|
} else {
|
|||
|
|
// 自定义皮肤:清除背景色,使用皮肤图片
|
|||
|
|
self.contentView.backgroundColor = [UIColor clearColor];
|
|||
|
|
self.bgImageView.backgroundColor = [UIColor clearColor];
|
|||
|
|
[self.kb_defaultGradientLayer removeFromSuperlayer];
|
|||
|
|
self.kb_defaultGradientLayer = nil;
|
|||
|
|
img = [[KBSkinManager shared] currentBackgroundImage];
|
|||
|
|
}
|
|||
|
|
NSLog(@"⌨️[Keyboard] apply theme id=%@ hasBg=%d", t.skinId, (img != nil));
|
|||
|
|
[self kb_logSkinDiagnosticsWithTheme:t backgroundImage:img];
|
|||
|
|
self.bgImageView.image = img;
|
|||
|
|
|
|||
|
|
// 触发键区按主题重绘
|
|||
|
|
if (themeChanged &&
|
|||
|
|
[self.keyBoardMainView respondsToSelector:@selector(kb_applyTheme)]) {
|
|||
|
|
// method declared in KBKeyBoardMainView.h
|
|||
|
|
#pragma clang diagnostic push
|
|||
|
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|||
|
|
[self.keyBoardMainView performSelector:@selector(kb_applyTheme)];
|
|||
|
|
#pragma clang diagnostic pop
|
|||
|
|
}
|
|||
|
|
// 注意:这里不能直接访问 self.functionView,否则会导致功能面板提前创建,占用内存。
|
|||
|
|
KBFunctionView *functionView = [self kb_functionViewIfCreated];
|
|||
|
|
if (themeChanged && functionView &&
|
|||
|
|
[functionView respondsToSelector:@selector(kb_applyTheme)]) {
|
|||
|
|
#pragma clang diagnostic push
|
|||
|
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|||
|
|
[functionView performSelector:@selector(kb_applyTheme)];
|
|||
|
|
#pragma clang diagnostic pop
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
- (BOOL)kb_isDefaultKeyboardTheme:(KBSkinTheme *)theme {
|
|||
|
|
NSString *skinId = theme.skinId ?: @"";
|
|||
|
|
if (skinId.length == 0 || [skinId isEqualToString:@"default"]) {
|
|||
|
|
return YES;
|
|||
|
|
}
|
|||
|
|
if ([skinId isEqualToString:kKBDefaultSkinIdLight]) {
|
|||
|
|
return YES;
|
|||
|
|
}
|
|||
|
|
return [skinId isEqualToString:kKBDefaultSkinIdDark];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
- (BOOL)kb_isDarkModeActive {
|
|||
|
|
if (@available(iOS 13.0, *)) {
|
|||
|
|
return self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark;
|
|||
|
|
}
|
|||
|
|
return NO;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
- (NSString *)kb_defaultSkinIdForCurrentStyle {
|
|||
|
|
return [self kb_isDarkModeActive] ? kKBDefaultSkinIdDark
|
|||
|
|
: kKBDefaultSkinIdLight;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
- (NSString *)kb_defaultSkinZipNameForCurrentStyle {
|
|||
|
|
return [self kb_isDarkModeActive] ? kKBDefaultSkinZipNameDark
|
|||
|
|
: kKBDefaultSkinZipNameLight;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
- (UIImage *)kb_defaultGradientImageWithSize:(CGSize)size
|
|||
|
|
topColor:(UIColor *)topColor
|
|||
|
|
bottomColor:(UIColor *)bottomColor {
|
|||
|
|
if (size.width <= 0 || size.height <= 0) {
|
|||
|
|
return nil;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 尺寸未变则复用缓存,避免反复创建图片撑爆键盘扩展内存
|
|||
|
|
if (self.kb_cachedGradientImage &&
|
|||
|
|
CGSizeEqualToSize(self.kb_cachedGradientSize, size)) {
|
|||
|
|
return self.kb_cachedGradientImage;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
UIColor *resolvedTopColor = topColor;
|
|||
|
|
UIColor *resolvedBottomColor = bottomColor;
|
|||
|
|
if (@available(iOS 13.0, *)) {
|
|||
|
|
resolvedTopColor =
|
|||
|
|
[topColor resolvedColorWithTraitCollection:self.traitCollection];
|
|||
|
|
resolvedBottomColor =
|
|||
|
|
[bottomColor resolvedColorWithTraitCollection:self.traitCollection];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
CAGradientLayer *layer = [CAGradientLayer layer];
|
|||
|
|
layer.frame = CGRectMake(0, 0, size.width, size.height);
|
|||
|
|
layer.startPoint = CGPointMake(0.5, 0.0);
|
|||
|
|
layer.endPoint = CGPointMake(0.5, 1.0);
|
|||
|
|
layer.colors =
|
|||
|
|
@[ (id)resolvedTopColor.CGColor, (id)resolvedBottomColor.CGColor ];
|
|||
|
|
|
|||
|
|
UIGraphicsBeginImageContextWithOptions(size, YES, 0);
|
|||
|
|
[layer renderInContext:UIGraphicsGetCurrentContext()];
|
|||
|
|
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
|
|||
|
|
UIGraphicsEndImageContext();
|
|||
|
|
|
|||
|
|
self.kb_cachedGradientImage = image;
|
|||
|
|
self.kb_cachedGradientSize = size;
|
|||
|
|
return image;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
- (void)kb_logSkinDiagnosticsWithTheme:(KBSkinTheme *)theme
|
|||
|
|
backgroundImage:(UIImage *)image {
|
|||
|
|
#if DEBUG
|
|||
|
|
NSString *skinId = theme.skinId ?: @"";
|
|||
|
|
NSString *name = theme.name ?: @"";
|
|||
|
|
NSMutableArray<NSString *> *roots = [NSMutableArray array];
|
|||
|
|
NSURL *containerURL = [[NSFileManager defaultManager]
|
|||
|
|
containerURLForSecurityApplicationGroupIdentifier:AppGroup];
|
|||
|
|
if (containerURL.path.length > 0) {
|
|||
|
|
[roots addObject:containerURL.path];
|
|||
|
|
}
|
|||
|
|
NSString *cacheRoot = NSSearchPathForDirectoriesInDomains(
|
|||
|
|
NSCachesDirectory, NSUserDomainMask, YES)
|
|||
|
|
.firstObject;
|
|||
|
|
if (cacheRoot.length > 0) {
|
|||
|
|
[roots addObject:cacheRoot];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
NSFileManager *fm = [NSFileManager defaultManager];
|
|||
|
|
NSMutableArray<NSString *> *lines = [NSMutableArray array];
|
|||
|
|
for (NSString *root in roots) {
|
|||
|
|
NSString *iconsDir = [[root stringByAppendingPathComponent:@"Skins"]
|
|||
|
|
stringByAppendingPathComponent:skinId];
|
|||
|
|
iconsDir = [iconsDir stringByAppendingPathComponent:@"icons"];
|
|||
|
|
BOOL isDir = NO;
|
|||
|
|
BOOL exists = [fm fileExistsAtPath:iconsDir isDirectory:&isDir] && isDir;
|
|||
|
|
NSArray *contents =
|
|||
|
|
exists ? [fm contentsOfDirectoryAtPath:iconsDir error:nil] : nil;
|
|||
|
|
NSUInteger count = contents.count;
|
|||
|
|
BOOL hasQ =
|
|||
|
|
exists &&
|
|||
|
|
[fm fileExistsAtPath:[iconsDir
|
|||
|
|
stringByAppendingPathComponent:@"key_q.png"]];
|
|||
|
|
BOOL hasQUp =
|
|||
|
|
exists && [fm fileExistsAtPath:[iconsDir stringByAppendingPathComponent:
|
|||
|
|
@"key_q_up.png"]];
|
|||
|
|
BOOL hasDel =
|
|||
|
|
exists && [fm fileExistsAtPath:[iconsDir stringByAppendingPathComponent:
|
|||
|
|
@"key_del.png"]];
|
|||
|
|
BOOL hasShift =
|
|||
|
|
exists &&
|
|||
|
|
[fm fileExistsAtPath:[iconsDir
|
|||
|
|
stringByAppendingPathComponent:@"key_up.png"]];
|
|||
|
|
BOOL hasShiftUpper =
|
|||
|
|
exists && [fm fileExistsAtPath:[iconsDir stringByAppendingPathComponent:
|
|||
|
|
@"key_up_upper.png"]];
|
|||
|
|
NSString *line = [NSString
|
|||
|
|
stringWithFormat:@"root=%@ icons=%@ exist=%d count=%tu key_q=%d "
|
|||
|
|
@"key_q_up=%d key_del=%d key_up=%d key_up_upper=%d",
|
|||
|
|
root, iconsDir, exists, count, hasQ, hasQUp, hasDel,
|
|||
|
|
hasShift, hasShiftUpper];
|
|||
|
|
[lines addObject:line];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
NSLog(@"[Keyboard] theme id=%@ name=%@ hasBg=%d\n%@", skinId, name,
|
|||
|
|
(image != nil), [lines componentsJoinedByString:@"\n"]);
|
|||
|
|
#endif
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
- (void)kb_consumePendingShopSkin {
|
|||
|
|
KBWeakSelf [KBSkinInstallBridge
|
|||
|
|
consumePendingRequestFromBundle:NSBundle.mainBundle
|
|||
|
|
completion:^(BOOL success,
|
|||
|
|
NSError *_Nullable error) {
|
|||
|
|
if (!success) {
|
|||
|
|
if (error) {
|
|||
|
|
NSLog(@"[Keyboard] skin request failed: %@",
|
|||
|
|
error);
|
|||
|
|
[KBHUD
|
|||
|
|
showInfo:
|
|||
|
|
KBLocalized(
|
|||
|
|
@"皮肤资源准备失败,请稍后再试")];
|
|||
|
|
}
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
[weakSelf kb_applyTheme];
|
|||
|
|
[KBHUD showInfo:KBLocalized(
|
|||
|
|
@"皮肤已更新,立即体验吧")];
|
|||
|
|
}];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
- (void)kb_applyDefaultSkinIfNeeded {
|
|||
|
|
NSDictionary *pending = [KBSkinInstallBridge pendingRequestPayload];
|
|||
|
|
if (pending.count > 0) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
NSString *currentId = [KBSkinManager shared].current.skinId ?: @"";
|
|||
|
|
BOOL isDefault =
|
|||
|
|
(currentId.length == 0 || [currentId isEqualToString:@"default"]);
|
|||
|
|
BOOL isLightDefault = [currentId isEqualToString:kKBDefaultSkinIdLight];
|
|||
|
|
BOOL isDarkDefault = [currentId isEqualToString:kKBDefaultSkinIdDark];
|
|||
|
|
if (!isDefault && !isLightDefault && !isDarkDefault) {
|
|||
|
|
// 用户已应用自定义皮肤:不随深色模式切换默认皮肤
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
NSString *targetId = [self kb_defaultSkinIdForCurrentStyle];
|
|||
|
|
NSString *targetZip = [self kb_defaultSkinZipNameForCurrentStyle];
|
|||
|
|
if (currentId.length > 0 && [currentId isEqualToString:targetId]) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
NSError *applyError = nil;
|
|||
|
|
if ([KBSkinInstallBridge applyInstalledSkinWithId:targetId error:&applyError]) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[KBSkinInstallBridge publishBundleSkinRequestWithId:targetId
|
|||
|
|
name:targetId
|
|||
|
|
zipName:targetZip
|
|||
|
|
iconShortNames:nil];
|
|||
|
|
[KBSkinInstallBridge
|
|||
|
|
consumePendingRequestFromBundle:NSBundle.mainBundle
|
|||
|
|
completion:^(__unused BOOL success,
|
|||
|
|
__unused NSError *_Nullable error) {
|
|||
|
|
// 已通过通知触发主题刷新,这里无需额外处理
|
|||
|
|
}];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@end
|