2025-10-28 10:18:10 +08:00
|
|
|
|
//
|
|
|
|
|
|
// KBKeyButton.m
|
|
|
|
|
|
// CustomKeyboard
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
|
|
#import "KBKeyButton.h"
|
|
|
|
|
|
#import "KBKey.h"
|
2025-11-04 21:01:46 +08:00
|
|
|
|
#import "KBSkinManager.h"
|
2025-10-28 10:18:10 +08:00
|
|
|
|
|
2025-11-18 20:53:47 +08:00
|
|
|
|
@interface KBKeyButton ()
|
2025-11-20 21:11:27 +08:00
|
|
|
|
// 内部缓存:便于从按钮查找到所属的 KBKeyboardView
|
|
|
|
|
|
@property (nonatomic, weak, readonly) UIView *kb_keyboardContainer;
|
2025-11-21 19:40:57 +08:00
|
|
|
|
@property (nonatomic, strong) UIImageView *normalImageView; /// 没有皮肤的时候展示
|
|
|
|
|
|
@property (nonatomic, strong) UIColor *baseBackgroundColor; /// 无按下状态下,由皮肤/主题决定的底色(由 normalImageView 展示)
|
|
|
|
|
|
|
2025-11-18 20:53:47 +08:00
|
|
|
|
@end
|
|
|
|
|
|
|
2025-10-28 10:18:10 +08:00
|
|
|
|
@implementation KBKeyButton
|
|
|
|
|
|
|
|
|
|
|
|
- (instancetype)initWithFrame:(CGRect)frame {
|
|
|
|
|
|
if (self = [super initWithFrame:frame]) {
|
2025-11-21 19:40:57 +08:00
|
|
|
|
[self addSubview:self.normalImageView];
|
|
|
|
|
|
self.normalImageView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
|
|
|
|
[NSLayoutConstraint activateConstraints:@[
|
|
|
|
|
|
[self.normalImageView.topAnchor constraintEqualToAnchor:self.topAnchor],
|
|
|
|
|
|
[self.normalImageView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
|
|
|
|
|
|
[self.normalImageView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:2],
|
|
|
|
|
|
[self.normalImageView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-2],
|
|
|
|
|
|
]];
|
2025-10-28 10:18:10 +08:00
|
|
|
|
[self applyDefaultStyle];
|
|
|
|
|
|
}
|
|
|
|
|
|
return self;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)applyDefaultStyle {
|
2025-11-04 21:01:46 +08:00
|
|
|
|
self.titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold];
|
|
|
|
|
|
KBSkinTheme *t = [KBSkinManager shared].current;
|
|
|
|
|
|
[self setTitleColor:t.keyTextColor forState:UIControlStateNormal];
|
|
|
|
|
|
[self setTitleColor:t.keyTextColor forState:UIControlStateHighlighted];
|
2025-11-21 19:40:57 +08:00
|
|
|
|
// 颜色由 normalImageView 控制,按钮本身保持透明,避免叠加背景影响视觉
|
|
|
|
|
|
self.backgroundColor = [UIColor clearColor];
|
2025-10-28 10:18:10 +08:00
|
|
|
|
self.layer.cornerRadius = 6.0; // 圆角
|
|
|
|
|
|
self.layer.masksToBounds = NO;
|
|
|
|
|
|
self.layer.shadowColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor; // 阴影效果
|
|
|
|
|
|
self.layer.shadowOpacity = 1.0;
|
|
|
|
|
|
self.layer.shadowOffset = CGSizeMake(0, 1);
|
|
|
|
|
|
self.layer.shadowRadius = 1.5;
|
2025-11-21 19:40:57 +08:00
|
|
|
|
|
|
|
|
|
|
// 初始状态下根据主题设置底色(给没有皮肤图的按键使用)
|
2025-10-28 20:11:40 +08:00
|
|
|
|
[self refreshStateAppearance];
|
2025-11-18 20:53:47 +08:00
|
|
|
|
|
|
|
|
|
|
// 懒创建图标视图,用于后续皮肤按键小图标展示
|
|
|
|
|
|
if (!self.iconView) {
|
|
|
|
|
|
UIImageView *iv = [[UIImageView alloc] initWithFrame:CGRectZero];
|
|
|
|
|
|
// 作为按键的整块皮肤背景,铺满整个按钮区域
|
2025-11-20 19:57:11 +08:00
|
|
|
|
iv.contentMode = UIViewContentModeScaleToFill;
|
2025-11-18 20:53:47 +08:00
|
|
|
|
iv.clipsToBounds = YES;
|
|
|
|
|
|
iv.translatesAutoresizingMaskIntoConstraints = NO;
|
|
|
|
|
|
[self addSubview:iv];
|
|
|
|
|
|
// 让皮肤图片撑满整个按钮
|
|
|
|
|
|
[NSLayoutConstraint activateConstraints:@[
|
|
|
|
|
|
[iv.topAnchor constraintEqualToAnchor:self.topAnchor],
|
|
|
|
|
|
[iv.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
|
2025-11-20 21:53:46 +08:00
|
|
|
|
[iv.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:2],
|
|
|
|
|
|
[iv.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-2],
|
2025-11-18 20:53:47 +08:00
|
|
|
|
]];
|
|
|
|
|
|
self.iconView = iv;
|
|
|
|
|
|
|
|
|
|
|
|
// 文字保持居中;若需要显示文字,则覆盖在皮肤图片之上
|
|
|
|
|
|
self.titleEdgeInsets = UIEdgeInsetsZero;
|
|
|
|
|
|
[self bringSubviewToFront:self.titleLabel];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setKey:(KBKey *)key {
|
|
|
|
|
|
_key = key;
|
2025-10-28 10:18:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setHighlighted:(BOOL)highlighted {
|
|
|
|
|
|
[super setHighlighted:highlighted];
|
2025-11-20 20:36:31 +08:00
|
|
|
|
// 按下时整体做一个等比缩放动画,不改背景色和透明度。
|
|
|
|
|
|
// 这样无论是纯文字键还是整块皮肤图,都有统一的“按下”视觉反馈。
|
|
|
|
|
|
CGFloat scale = highlighted ? 0.9 : 1.0; // 可根据手感微调 0.9~0.95
|
2025-11-21 19:40:57 +08:00
|
|
|
|
|
|
|
|
|
|
// 没有皮肤图片时,normalImageView 做灰度按下效果
|
|
|
|
|
|
BOOL hasIcon = (self.iconView.image != nil);
|
|
|
|
|
|
UIColor *normalBgColor = self.baseBackgroundColor ?: [UIColor whiteColor];
|
|
|
|
|
|
UIColor *highlightBgColor = [self kb_darkerColorForColor:normalBgColor];
|
|
|
|
|
|
|
2025-11-20 20:36:31 +08:00
|
|
|
|
[UIView animateWithDuration:0.08
|
|
|
|
|
|
delay:0
|
|
|
|
|
|
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseOut
|
|
|
|
|
|
animations:^{
|
2025-11-21 19:40:57 +08:00
|
|
|
|
if (!hasIcon && !self.normalImageView.hidden) {
|
|
|
|
|
|
self.normalImageView.backgroundColor = highlighted ? highlightBgColor : normalBgColor;
|
|
|
|
|
|
}
|
2025-11-20 20:36:31 +08:00
|
|
|
|
self.transform = CGAffineTransformMakeScale(scale, scale);
|
|
|
|
|
|
}
|
|
|
|
|
|
completion:nil];
|
2025-11-20 21:11:27 +08:00
|
|
|
|
|
|
|
|
|
|
// 将“按下/抬起”事件转发给键盘视图,用于显示/隐藏顶部预览气泡。
|
|
|
|
|
|
UIView *container = self.kb_keyboardContainer;
|
|
|
|
|
|
if ([container respondsToSelector:@selector(showPreviewForButton:)] &&
|
|
|
|
|
|
[container respondsToSelector:@selector(hidePreview)]) {
|
|
|
|
|
|
if (highlighted) {
|
|
|
|
|
|
#pragma clang diagnostic push
|
|
|
|
|
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
|
|
|
|
[container performSelector:@selector(showPreviewForButton:) withObject:self];
|
|
|
|
|
|
#pragma clang diagnostic pop
|
|
|
|
|
|
} else {
|
|
|
|
|
|
#pragma clang diagnostic push
|
|
|
|
|
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
|
|
|
|
[container performSelector:@selector(hidePreview)];
|
|
|
|
|
|
#pragma clang diagnostic pop
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-28 20:11:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setSelected:(BOOL)selected {
|
|
|
|
|
|
[super setSelected:selected];
|
|
|
|
|
|
[self refreshStateAppearance];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)refreshStateAppearance {
|
|
|
|
|
|
// 选中态用于 Shift/CapsLock 等特殊按键的高亮显示
|
2025-11-04 21:01:46 +08:00
|
|
|
|
KBSkinTheme *t = [KBSkinManager shared].current;
|
2025-11-21 19:40:57 +08:00
|
|
|
|
UIColor *base = nil;
|
2025-10-28 20:11:40 +08:00
|
|
|
|
if (self.isSelected) {
|
2025-11-21 19:40:57 +08:00
|
|
|
|
base = t.keyHighlightBackground ?: t.keyBackground;
|
2025-10-28 20:11:40 +08:00
|
|
|
|
} else {
|
2025-11-21 19:40:57 +08:00
|
|
|
|
base = t.keyBackground;
|
2025-10-28 20:11:40 +08:00
|
|
|
|
}
|
2025-11-21 19:40:57 +08:00
|
|
|
|
if (!base) {
|
|
|
|
|
|
base = [UIColor whiteColor];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.baseBackgroundColor = base;
|
|
|
|
|
|
// 按键背景统一由 normalImageView 控制,按钮本身透明
|
|
|
|
|
|
self.backgroundColor = [UIColor clearColor];
|
|
|
|
|
|
|
|
|
|
|
|
// 有皮肤图时仅展示 icon,不再显示普通背景色
|
|
|
|
|
|
if (self.iconView.image != nil || self.normalImageView.hidden) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.normalImageView.backgroundColor = base;
|
2025-10-28 10:18:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-18 20:53:47 +08:00
|
|
|
|
- (void)applyThemeForCurrentKey {
|
|
|
|
|
|
// 根据皮肤映射加载图标(若有),支持大小写变体:
|
|
|
|
|
|
// - identifier: 逻辑按键标识(如 letter_q)
|
|
|
|
|
|
// - caseVariant: 0/1/2 => 无变体/小写/大写
|
2025-11-19 16:13:30 +08:00
|
|
|
|
NSString *identifier = self.key.identifier;
|
2025-11-18 20:53:47 +08:00
|
|
|
|
NSInteger variant = (NSInteger)self.key.caseVariant;
|
2025-12-16 20:20:57 +08:00
|
|
|
|
KBSkinManager *skinManager = [KBSkinManager shared];
|
|
|
|
|
|
UIImage *iconImg = [skinManager iconImageForKeyIdentifier:identifier caseVariant:variant];
|
|
|
|
|
|
|
|
|
|
|
|
if (!iconImg && [identifier isEqualToString:@"ai"]) {
|
|
|
|
|
|
NSString *skinId = skinManager.current.skinId ?: @"";
|
|
|
|
|
|
BOOL usingDefaultSkin = (skinId.length == 0 || [skinId isEqualToString:@"default"]);
|
|
|
|
|
|
if (usingDefaultSkin) {
|
|
|
|
|
|
iconImg = [UIImage imageNamed:@"ai_key_icon"];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-19 16:13:30 +08:00
|
|
|
|
|
|
|
|
|
|
// 设置整块按键背景图(若有)
|
2025-11-18 20:53:47 +08:00
|
|
|
|
self.iconView.image = iconImg;
|
|
|
|
|
|
self.iconView.hidden = (iconImg == nil);
|
2025-11-19 16:13:30 +08:00
|
|
|
|
|
|
|
|
|
|
BOOL hasIcon = (iconImg != nil);
|
2025-11-21 19:40:57 +08:00
|
|
|
|
self.normalImageView.hidden = hasIcon;
|
2025-11-19 16:13:30 +08:00
|
|
|
|
if (hasIcon) {
|
|
|
|
|
|
// 有图标:仅显示图片,完全隐藏文字
|
|
|
|
|
|
[self setTitle:@"" forState:UIControlStateNormal];
|
|
|
|
|
|
[self setTitle:@"" forState:UIControlStateHighlighted];
|
|
|
|
|
|
[self setTitle:@"" forState:UIControlStateSelected];
|
|
|
|
|
|
self.titleLabel.hidden = YES;
|
2025-11-21 19:40:57 +08:00
|
|
|
|
self.normalImageView.backgroundColor = [UIColor clearColor];
|
2025-11-19 16:13:30 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 无图标:按键标题正常显示(使用 key.title),并根据 hidden_keys 决定要不要隐藏
|
|
|
|
|
|
[self setTitle:self.key.title forState:UIControlStateNormal];
|
|
|
|
|
|
BOOL hideTextBySkin = [[KBSkinManager shared] shouldHideKeyTextForIdentifier:identifier];
|
|
|
|
|
|
self.titleLabel.hidden = hideTextBySkin;
|
|
|
|
|
|
}
|
2025-11-18 20:53:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-21 19:40:57 +08:00
|
|
|
|
- (UIImageView *)normalImageView{
|
|
|
|
|
|
if (!_normalImageView) {
|
|
|
|
|
|
_normalImageView = [[UIImageView alloc] init];
|
|
|
|
|
|
// 初始给一个默认色,后续会由 refreshStateAppearance / 皮肤统一覆盖
|
|
|
|
|
|
_normalImageView.backgroundColor = [UIColor whiteColor];
|
|
|
|
|
|
_normalImageView.layer.cornerRadius = 6;
|
|
|
|
|
|
_normalImageView.layer.masksToBounds = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return _normalImageView;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 在当前底色的基础上略微变暗,作为按下时的“灰度”效果
|
|
|
|
|
|
- (UIColor *)kb_darkerColorForColor:(UIColor *)color {
|
|
|
|
|
|
if (!color) return [UIColor colorWithWhite:0.9 alpha:1.0];
|
|
|
|
|
|
|
|
|
|
|
|
CGFloat h = 0, s = 0, b = 0, a = 0;
|
|
|
|
|
|
if ([color getHue:&h saturation:&s brightness:&b alpha:&a]) {
|
|
|
|
|
|
return [UIColor colorWithHue:h saturation:s brightness:MAX(b * 0.9, 0.0) alpha:a];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
CGFloat white = 0;
|
|
|
|
|
|
if ([color getWhite:&white alpha:&a]) {
|
|
|
|
|
|
return [UIColor colorWithWhite:MAX(white * 0.9, 0.0) alpha:a];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return color;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-28 10:18:10 +08:00
|
|
|
|
@end
|
2025-11-20 21:11:27 +08:00
|
|
|
|
|
|
|
|
|
|
@implementation KBKeyButton (KBKeyboardContainer)
|
|
|
|
|
|
|
|
|
|
|
|
- (UIView *)kb_keyboardContainer {
|
|
|
|
|
|
UIView *v = self.superview;
|
|
|
|
|
|
while (v) {
|
|
|
|
|
|
// KBKeyboardView 是当前容器类型,这里用类名字符串避免直接引用头文件死循环
|
|
|
|
|
|
if ([NSStringFromClass(v.class) isEqualToString:@"KBKeyboardView"]) {
|
|
|
|
|
|
return v;
|
|
|
|
|
|
}
|
|
|
|
|
|
v = v.superview;
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@end
|