diff --git a/CustomKeyboard/KeyboardViewController.m b/CustomKeyboard/KeyboardViewController.m index db717ed..30154a8 100644 --- a/CustomKeyboard/KeyboardViewController.m +++ b/CustomKeyboard/KeyboardViewController.m @@ -83,6 +83,11 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center, // NSLog(@"[Keyboard] 读取到的数据: %@", value); } +- (void)viewWillAppear:(BOOL)animated{ + [super viewWillAppear:animated]; + [[KBLocalizationManager shared] reloadFromSharedStorageIfNeeded]; +} + - (void)setupUI { self.view.translatesAutoresizingMaskIntoConstraints = NO; diff --git a/Shared/KBLocalizationManager.h b/Shared/KBLocalizationManager.h index ba6430c..2785d91 100644 --- a/Shared/KBLocalizationManager.h +++ b/Shared/KBLocalizationManager.h @@ -47,6 +47,8 @@ extern NSString * const KBLocalizationDidChangeNotification; /// 基于一组“偏好语言”计算最佳支持语言。 - (NSString *)bestSupportedLanguageForPreferred:(NSArray *)preferred; +- (void)reloadFromSharedStorageIfNeeded; + @end /// 便捷宏:与 NSLocalizedString 类似,但遵循 KBLocalizationManager 当前语言 diff --git a/Shared/KBLocalizationManager.m b/Shared/KBLocalizationManager.m index 899964b..936a222 100644 --- a/Shared/KBLocalizationManager.m +++ b/Shared/KBLocalizationManager.m @@ -177,4 +177,14 @@ static inline NSMutableDictionary *KBLocBaseKCQuery(void) { return lang; } +- (void)reloadFromSharedStorageIfNeeded { + NSString *saved = [[self class] kc_read]; + if (saved.length == 0) return; + if ([saved isEqualToString:self.currentLanguageCode]) return; + + [self applyLanguage:saved]; + [[NSNotificationCenter defaultCenter] postNotificationName:KBLocalizationDidChangeNotification + object:nil]; +} + @end diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index c1b3cf0..ccbc5cf 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -54,6 +54,8 @@ 04791FF72ED5B985004E8522 /* Christmas.zip in Resources */ = {isa = PBXBuildFile; fileRef = 04791FF62ED5B985004E8522 /* Christmas.zip */; }; 04791FFB2ED5EAB8004E8522 /* fense.zip in Resources */ = {isa = PBXBuildFile; fileRef = 04791FFA2ED5EAB8004E8522 /* fense.zip */; }; 04791FFC2ED71D17004E8522 /* UIColor+Extension.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95E42EB220B5007BD342 /* UIColor+Extension.m */; }; + 04791FFF2ED830FA004E8522 /* KBKeyboardMaskView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791FFE2ED830FA004E8522 /* KBKeyboardMaskView.m */; }; + 047920032ED8343D004E8522 /* kb_guide_keyboard.gif in Resources */ = {isa = PBXBuildFile; fileRef = 047920022ED8343D004E8522 /* kb_guide_keyboard.gif */; }; 047C650D2EBC8A840035E841 /* KBPanModalView.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C650C2EBC8A840035E841 /* KBPanModalView.m */; }; 047C65102EBCA8DD0035E841 /* HomeRankContentVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C650F2EBCA8DD0035E841 /* HomeRankContentVC.m */; }; 047C65502EBCBA9E0035E841 /* KBShopVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C654F2EBCBA9E0035E841 /* KBShopVC.m */; }; @@ -265,6 +267,9 @@ 04791F972ED49CE7004E8522 /* KBFont.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFont.m; sourceTree = ""; }; 04791FF62ED5B985004E8522 /* Christmas.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = Christmas.zip; sourceTree = ""; }; 04791FFA2ED5EAB8004E8522 /* fense.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = fense.zip; sourceTree = ""; }; + 04791FFD2ED830FA004E8522 /* KBKeyboardMaskView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardMaskView.h; sourceTree = ""; }; + 04791FFE2ED830FA004E8522 /* KBKeyboardMaskView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardMaskView.m; sourceTree = ""; }; + 047920022ED8343D004E8522 /* kb_guide_keyboard.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = kb_guide_keyboard.gif; sourceTree = ""; }; 047C650B2EBC8A840035E841 /* KBPanModalView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBPanModalView.h; sourceTree = ""; }; 047C650C2EBC8A840035E841 /* KBPanModalView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBPanModalView.m; sourceTree = ""; }; 047C650E2EBCA8DD0035E841 /* HomeRankContentVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HomeRankContentVC.h; sourceTree = ""; }; @@ -619,17 +624,10 @@ path = FunctionTest; sourceTree = ""; }; - 047C652B2EBCAAAC0035E841 /* Images */ = { - isa = PBXGroup; - children = ( - ); - path = Images; - sourceTree = ""; - }; 047C652C2EBCAAAC0035E841 /* Resource */ = { isa = PBXGroup; children = ( - 047C652B2EBCAAAC0035E841 /* Images */, + 047920022ED8343D004E8522 /* kb_guide_keyboard.gif */, 04286A122ECDEBF900CE730C /* KBSkinIconMap.strings */, 04286A0E2ECDA71B00CE730C /* 001.zip */, ); @@ -1171,6 +1169,8 @@ 04FC97022EB30A00007BD342 /* KBGuideKFCell.m */, 04FC97042EB30A00007BD342 /* KBGuideUserCell.h */, 04FC97052EB30A00007BD342 /* KBGuideUserCell.m */, + 04791FFD2ED830FA004E8522 /* KBKeyboardMaskView.h */, + 04791FFE2ED830FA004E8522 /* KBKeyboardMaskView.m */, ); path = V; sourceTree = ""; @@ -1466,6 +1466,7 @@ 04286A0F2ECDA71B00CE730C /* 001.zip in Resources */, 04C6EABA2EAF86530089C901 /* Assets.xcassets in Resources */, 04A9FE212EB893F10020DB6D /* Localizable.strings in Resources */, + 047920032ED8343D004E8522 /* kb_guide_keyboard.gif in Resources */, 04C6EABC2EAF86530089C901 /* LaunchScreen.storyboard in Resources */, 04286A132ECDEBF900CE730C /* KBSkinIconMap.strings in Resources */, 04C6EABD2EAF86530089C901 /* Main.storyboard in Resources */, @@ -1608,6 +1609,7 @@ 049FB20E2EC1CD2800FAB05D /* KBAlert.m in Sources */, 04A9FE162EB873C80020DB6D /* UIViewController+Extension.m in Sources */, 04C6EABE2EAF86530089C901 /* AppDelegate.m in Sources */, + 04791FFF2ED830FA004E8522 /* KBKeyboardMaskView.m in Sources */, 04FC95F12EB339A7007BD342 /* LoginViewController.m in Sources */, 048908E92EBF843000FABA60 /* KBSkinDetailHeaderCell.m in Sources */, 04FC96142EB34E00007BD342 /* KBLoginSheetViewController.m in Sources */, diff --git a/keyBoard/Class/Guard/V/KBKeyboardMaskView.h b/keyBoard/Class/Guard/V/KBKeyboardMaskView.h new file mode 100644 index 0000000..6be367d --- /dev/null +++ b/keyBoard/Class/Guard/V/KBKeyboardMaskView.h @@ -0,0 +1,29 @@ +// +// KBKeyboardMaskView.h +// keyBoard +// +// Created by Mac on 2025/11/27. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN +/// 覆盖在 KBGuideVC 上方的“请选择自家键盘”蒙层 +/// - 左上角:返回箭头 +/// - 中间:播放 GIF 的区域(宽=屏幕宽,高=300) +/// - 点击任意空白区域:回调给外部(用于激活输入框) +@interface KBKeyboardMaskView : UIView +@property (nonatomic, strong, readonly) UIButton *backButton; +@property (nonatomic, strong, readonly) FLAnimatedImageView *gifView; + +/// 点击蒙层空白时回调(不包括 backButton) +@property (nonatomic, copy) void (^tapHandler)(void); + +/// 更新内部 GIF 与键盘的相对位置,保证不被遮挡 +- (void)updateForKeyboardHeight:(CGFloat)kbHeight + duration:(NSTimeInterval)duration + curve:(UIViewAnimationOptions)curve; +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/Guard/V/KBKeyboardMaskView.m b/keyBoard/Class/Guard/V/KBKeyboardMaskView.m new file mode 100644 index 0000000..c2681b8 --- /dev/null +++ b/keyBoard/Class/Guard/V/KBKeyboardMaskView.m @@ -0,0 +1,125 @@ +// +// KBKeyboardMaskView.m +// keyBoard +// +// Created by Mac on 2025/11/27. +// + +#import "KBKeyboardMaskView.h" + +@interface KBKeyboardMaskView () +@property (nonatomic, strong) UIButton *backButton; +@property (nonatomic, strong) FLAnimatedImageView *gifView; +@property (nonatomic, assign) CGFloat keyboardHeight; +@end + +@implementation KBKeyboardMaskView + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (!self) return nil; + self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.55]; + self.userInteractionEnabled = YES; + + // 返回按钮 + _backButton = [UIButton buttonWithType:UIButtonTypeCustom]; + UIImage *backImg = [UIImage imageNamed:@"close_white2_icon"]; + [_backButton setImage:backImg forState:UIControlStateNormal]; + [self addSubview:_backButton]; + + [_backButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self).offset(9); + if (@available(iOS 11.0, *)) { + make.top.equalTo(self.mas_safeAreaLayoutGuideTop).offset(4); + } else { + make.top.equalTo(self).offset(40); + } + make.width.height.mas_equalTo(40); + }]; + + // GIF 区域 + _gifView = [FLAnimatedImageView new]; + _gifView.contentMode = UIViewContentModeScaleAspectFit; + _gifView.clipsToBounds = YES; + [self addSubview:_gifView]; + + // 尺寸固定:宽=屏幕宽,高=300;位置在 layoutSubviews 里根据键盘高度动态计算 + CGFloat screenW = UIScreen.mainScreen.bounds.size.width; + [_gifView mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerX.equalTo(self); + make.width.mas_equalTo(screenW); + make.height.mas_equalTo(300); + // 竖直方向不在这里约束,由 layoutSubviews 手动布局 + }]; + + // 整个蒙层点击:激活输入框 + UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTapMask:)]; + [self addGestureRecognizer:tap]; + + // 加载 GIF 资源(占位名,可按需更换为实际文件名) + NSString *gifPath = [[NSBundle mainBundle] pathForResource:@"kb_guide_keyboard" ofType:@"gif"]; + if (gifPath.length > 0) { + NSData *data = [NSData dataWithContentsOfFile:gifPath]; + if (data.length > 0) { + FLAnimatedImage *img = [FLAnimatedImage animatedImageWithGIFData:data]; + _gifView.animatedImage = img; + } + } + + return self; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + // 根据键盘高度,保证 GIF 不被遮挡: + // - 无键盘:居中显示; + // - 有键盘:底部距离键盘上方 20pt,若空间不足则向上顶到顶部预留的 safe 区域。 + CGFloat viewH = CGRectGetHeight(self.bounds); + CGFloat gifH = 300.0; + CGFloat topMargin = 80.0; // 预留给返回按钮和标题等 + CGFloat bottomMargin = 20.0; + + CGFloat y = 0; + if (self.keyboardHeight <= 0) { + // 无键盘:垂直居中 + y = (viewH - gifH) * 0.5; + if (y < topMargin) y = topMargin; + } else { + CGFloat maxBottom = viewH - self.keyboardHeight - bottomMargin; + y = maxBottom - gifH; + if (y < topMargin) y = topMargin; + } + + CGRect frame = self.gifView.frame; + frame.origin.y = y; + self.gifView.frame = frame; +} + +- (void)onTapMask:(UITapGestureRecognizer *)gr { + CGPoint p = [gr locationInView:self]; + // 如果点在返回按钮区域内,则不走 tapHandler(交由按钮自己的事件处理) + if (CGRectContainsPoint(self.backButton.frame, p)) { + return; + } + if (self.tapHandler) { + self.tapHandler(); + } +} + +- (void)updateForKeyboardHeight:(CGFloat)kbHeight + duration:(NSTimeInterval)duration + curve:(UIViewAnimationOptions)curve { + self.keyboardHeight = MAX(kbHeight, 0); + // 触发布局刷新,以便在 layoutSubviews 里根据最新键盘高度重算 gifView 的 Y 值 + [self setNeedsLayout]; + [UIView animateWithDuration:duration + delay:0 + options:curve + animations:^{ + [self layoutIfNeeded]; + } + completion:nil]; +} + +@end diff --git a/keyBoard/Class/Guard/VC/KBGuideVC.m b/keyBoard/Class/Guard/VC/KBGuideVC.m index 72e44c9..da458ce 100644 --- a/keyBoard/Class/Guard/VC/KBGuideVC.m +++ b/keyBoard/Class/Guard/VC/KBGuideVC.m @@ -11,6 +11,7 @@ #import "KBGuideUserCell.h" #import "KBPermissionViewController.h" #import "KBKeyboardPermissionManager.h" +#import "KBKeyboardMaskView.h" typedef NS_ENUM(NSInteger, KBGuideItemType) { KBGuideItemTypeTop = 0, // 顶部固定卡片 @@ -32,6 +33,12 @@ typedef NS_ENUM(NSInteger, KBGuideItemType) { /// 记录上一次的输入法标识,避免重复提示 @property (nonatomic, copy, nullable) NSString *kb_lastInputModeIdentifier; +/// 当当前系统键盘不是自家键盘时展示的蒙层(带返回箭头 + GIF) +@property (nonatomic, strong, nullable) KBKeyboardMaskView *kbKeyboardMaskView; + +/// 最近一次已知的键盘高度(用于初次展示蒙层时的 GIF 位置计算) +@property (nonatomic, assign) CGFloat kb_currentKeyboardHeight; + @end @implementation KBGuideVC @@ -94,8 +101,7 @@ typedef NS_ENUM(NSInteger, KBGuideItemType) { // 提前创建并铺满权限引导页(默认隐藏),避免后续显示时出现布局进场感 [self kb_preparePermissionOverlayIfNeeded]; - - + } - (void)dealloc { @@ -194,6 +200,7 @@ typedef NS_ENUM(NSInteger, KBGuideItemType) { CGRect endFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue]; CGFloat screenH = UIScreen.mainScreen.bounds.size.height; CGFloat kbHeight = MAX(0, screenH - endFrame.origin.y); + self.kb_currentKeyboardHeight = kbHeight; CGFloat safeBtm = 0; if (@available(iOS 11.0, *)) { safeBtm = self.view.safeAreaInsets.bottom; } @@ -212,6 +219,13 @@ typedef NS_ENUM(NSInteger, KBGuideItemType) { // 键盘位置变化后,尝试检测是否发生了输入法切换 [self kb_evaluateCurrentInputModeAndNotifyIfNeeded]; }]; + + // 更新蒙层内部 GIF 与键盘的相对位置,保证不被遮挡 + if (self.kbKeyboardMaskView) { + [self.kbKeyboardMaskView updateForKeyboardHeight:kbHeight + duration:duration + curve:curve]; + } } - (void)scrollToBottomAnimated:(BOOL)animated { @@ -272,7 +286,9 @@ typedef NS_ENUM(NSInteger, KBGuideItemType) { self.kb_lastInputModeIdentifier = currId; BOOL isMine = [currId rangeOfString:KB_KEYBOARD_EXTENSION_BUNDLE_ID].location != NSNotFound; - [KBHUD showInfo:(isMine ? KBLocalized(@"是自己的键盘") : KBLocalized(@"❎不是自己的键盘"))]; + + // 根据是否为自家键盘,更新遮罩层显示/隐藏 + [self kb_updateKeyboardMaskForIsMyKeyboard:isMine]; } /// 当权限满足时,尽力激活输入框,从而触发键盘挂载与输入法检测 @@ -290,6 +306,76 @@ typedef NS_ENUM(NSInteger, KBGuideItemType) { }); } +/// 根据当前是否为自家键盘,展示/隐藏键盘指导蒙层 +- (void)kb_updateKeyboardMaskForIsMyKeyboard:(BOOL)isMine { + // 权限引导显示期间不再额外显示键盘蒙层 + if (self.permVC && self.permVC.view.hidden == NO) { + if (self.kbKeyboardMaskView) { + self.kbKeyboardMaskView.hidden = YES; + } + return; + } + + BOOL shouldShow = !isMine; + + if (shouldShow) { + // 按需创建并添加蒙层 + if (!self.kbKeyboardMaskView) { + KBKeyboardMaskView *mask = [[KBKeyboardMaskView alloc] initWithFrame:self.view.bounds]; + __weak typeof(self) weakSelf = self; + mask.tapHandler = ^{ + __strong typeof(weakSelf) self = weakSelf; + if (!self) return; + // 点击蒙层:在“激活/收起”之间切换 textField 的第一响应状态 + if ([self.textField isFirstResponder]) { + // 当前是第一响应者 -> 收起键盘 + [self.view endEditing:YES]; + } else { + // 当前不是第一响应者 -> 激活,弹出键盘 + [self.textField becomeFirstResponder]; + } + }; + // 左上角返回按钮:退出当前 KBGuideVC + [mask.backButton addTarget:self action:@selector(kb_onMaskBack) forControlEvents:UIControlEventTouchUpInside]; + + self.kbKeyboardMaskView = mask; + + if (self.permVC && self.permVC.view.superview == self.view) { + // 确保权限页在最上层,键盘蒙层位于其下方 + [self.view insertSubview:mask belowSubview:self.permVC.view]; + } else { + [self.view addSubview:mask]; + } + [mask mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.equalTo(self.view); + }]; + + // 创建时立即根据当前键盘高度调整 GIF 位置,避免首次展示被遮挡 + [mask updateForKeyboardHeight:self.kb_currentKeyboardHeight + duration:0 + curve:UIViewAnimationOptionCurveEaseInOut]; + } + } + + if (!self.kbKeyboardMaskView) return; + + BOOL currentlyVisible = !self.kbKeyboardMaskView.hidden && self.kbKeyboardMaskView.alpha > 0.01; + if (shouldShow == currentlyVisible) return; + + self.kbKeyboardMaskView.hidden = NO; + CGFloat targetAlpha = shouldShow ? 1.0 : 0.0; + [UIView animateWithDuration:0.25 animations:^{ + self.kbKeyboardMaskView.alpha = targetAlpha; + } completion:^(BOOL finished) { + self.kbKeyboardMaskView.hidden = !shouldShow; + }]; +} + +/// 蒙层左上角返回按钮事件:让 KBGuideVC 退出 +- (void)kb_onMaskBack { + [self.navigationController popViewControllerAnimated:YES]; +} + #pragma mark - UITableView - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { diff --git a/keyBoard/Class/Resource/kb_guide_keyboard.gif b/keyBoard/Class/Resource/kb_guide_keyboard.gif new file mode 100644 index 0000000..fe63b90 Binary files /dev/null and b/keyBoard/Class/Resource/kb_guide_keyboard.gif differ