先提交
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
//
|
||||
// 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
|
||||
Reference in New Issue
Block a user