377 lines
15 KiB
Objective-C
377 lines
15 KiB
Objective-C
//
|
||
// 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
|