// // 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 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 *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 *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