// // KBKeyboardView.m // CustomKeyboard // #import "KBKeyboardView.h" #import "KBKeyButton.h" #import "KBKey.h" #import "KBSkinManager.h" #import "KBKeyPreviewView.h" #import "KBBackspaceLongPressHandler.h" #import "KBKeyboardLayoutConfig.h" // UI 常量统一管理,方便后续调试样式(以 375 宽设计稿为基准,通过 KBFit 做等比缩放) #define kKBRowVerticalSpacing KBFit(8.0f) #define kKBRowHorizontalInset KBFit(6.0f) #define kKBRowHeight KBFit(40.0f) static const NSTimeInterval kKBPreviewShowDuration = 0.08; static const NSTimeInterval kKBPreviewHideDuration = 0.06; static const CGFloat kKBSpecialKeySquareMultiplier = 1.2; static const CGFloat kKBReturnWidthMultiplier = 2.4; static const CGFloat kKBSpaceWidthMultiplier = 3.0; // 第二行字母行的左右占位比例(用于居中) static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5; @interface KBKeyboardView () @property (nonatomic, strong) UIView *row1; @property (nonatomic, strong) UIView *row2; @property (nonatomic, strong) UIView *row3; @property (nonatomic, strong) UIView *row4; @property (nonatomic, strong) NSArray *> *keysForRows; @property (nonatomic, strong) KBBackspaceLongPressHandler *backspaceHandler; @property (nonatomic, strong) KBKeyPreviewView *previewView; @property (nonatomic, strong) KBKeyboardLayoutConfig *layoutConfig; @end @implementation KBKeyboardView - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { self.backgroundColor = [KBSkinManager shared].current.keyboardBackground; _layoutStyle = KBKeyboardLayoutStyleLetters; // 默认小写:与需求一致,初始不开启 Shift _shiftOn = NO; _symbolsMoreOn = NO; // 数字面板默认第一页(123) self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig]; self.backspaceHandler = [[KBBackspaceLongPressHandler alloc] initWithContainerView:self]; [self buildBase]; [self reloadKeys]; } return self; } // 当切换大布局(字母/数字)时,重置数字二级页状态 - (void)setLayoutStyle:(KBKeyboardLayoutStyle)layoutStyle { _layoutStyle = layoutStyle; if (_layoutStyle != KBKeyboardLayoutStyleNumbers) { _symbolsMoreOn = NO; } } #pragma mark - Base Layout - (void)buildBase { [self addSubview:self.row1]; [self addSubview:self.row2]; [self addSubview:self.row3]; [self addSubview:self.row4]; KBKeyboardLayoutConfig *config = [self kb_layoutConfig]; KBKeyboardLayout *layout = [self kb_layoutForName:@"letters"]; NSArray *rows = layout.rows ?: @[]; CGFloat rowSpacing = [self kb_metricValue:config.metrics.rowSpacing fallback:nil defaultValue:8.0]; CGFloat topInset = [self kb_metricValue:config.metrics.topInset fallback:nil defaultValue:8.0]; CGFloat bottomInset = [self kb_metricValue:config.metrics.bottomInset fallback:nil defaultValue:6.0]; CGFloat row1Height = [self kb_rowHeightForRow:(rows.count > 0 ? rows[0] : nil)]; CGFloat row2Height = [self kb_rowHeightForRow:(rows.count > 1 ? rows[1] : nil)]; CGFloat row3Height = [self kb_rowHeightForRow:(rows.count > 2 ? rows[2] : nil)]; CGFloat row4Height = [self kb_rowHeightForRow:(rows.count > 3 ? rows[3] : nil)]; [self.row1 mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.mas_top).offset(topInset); make.left.right.equalTo(self); make.height.mas_equalTo(row1Height); }]; [self.row2 mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.row1.mas_bottom).offset(rowSpacing); make.left.right.equalTo(self); make.height.mas_equalTo(row2Height); }]; [self.row3 mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.row2.mas_bottom).offset(rowSpacing); make.left.right.equalTo(self); make.height.mas_equalTo(row3Height); }]; [self.row4 mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.row3.mas_bottom).offset(rowSpacing); make.left.right.equalTo(self); make.height.mas_equalTo(row4Height); make.bottom.equalTo(self.mas_bottom).offset(-bottomInset); }]; } #pragma mark - Public - (void)reloadKeys { [self.backspaceHandler bindDeleteButton:nil showClearLabel:NO]; // 移除旧按钮 for (UIView *row in @[self.row1, self.row2, self.row3, self.row4]) { [row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; } KBKeyboardLayout *layout = [self kb_currentLayout]; NSArray *rows = layout.rows ?: @[]; if (rows.count < 4) { [self kb_buildLegacyLayout]; return; } [self buildRow:self.row1 withRowConfig:rows[0]]; [self buildRow:self.row2 withRowConfig:rows[1]]; [self buildRow:self.row3 withRowConfig:rows[2]]; [self buildRow:self.row4 withRowConfig:rows[3]]; } #pragma mark - Hit Test - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView *hit = [super hitTest:point withEvent:event]; if ([hit isKindOfClass:[KBKeyButton class]]) { return hit; } if ([self kb_isHitInsideKeyRows:hit]) { KBKeyButton *btn = [self kb_nearestKeyButtonForPoint:point]; if (btn) { return btn; } } return hit; } - (BOOL)kb_isHitInsideKeyRows:(UIView *)hitView { if (!hitView) { return NO; } if (hitView == self) { return YES; } if ([hitView isDescendantOfView:self.row1]) { return YES; } if ([hitView isDescendantOfView:self.row2]) { return YES; } if ([hitView isDescendantOfView:self.row3]) { return YES; } if ([hitView isDescendantOfView:self.row4]) { return YES; } return NO; } - (KBKeyButton *)kb_nearestKeyButtonForPoint:(CGPoint)point { KBKeyButton *best = nil; CGFloat bestDistance = CGFLOAT_MAX; NSArray *rows = @[self.row1, self.row2, self.row3, self.row4]; UIView *targetRow = nil; for (UIView *row in rows) { CGRect rowFrame = [self convertRect:row.bounds fromView:row]; if (CGRectContainsPoint(rowFrame, point)) { targetRow = row; break; } } NSArray *candidateRows = targetRow ? @[targetRow] : rows; for (UIView *row in candidateRows) { NSArray *buttons = [self kb_collectKeyButtonsInView:row]; for (KBKeyButton *btn in buttons) { CGRect frame = [self convertRect:btn.frame fromView:btn.superview]; CGFloat dx = point.x - CGRectGetMidX(frame); CGFloat dy = point.y - CGRectGetMidY(frame); CGFloat dist = (dx * dx) + (dy * dy); if (dist < bestDistance) { bestDistance = dist; best = btn; } } } return best; } - (NSArray *)kb_collectKeyButtonsInView:(UIView *)view { if (!view) { return @[]; } NSMutableArray *buttons = [NSMutableArray array]; [self kb_collectKeyButtonsInView:view into:buttons]; return buttons.copy; } - (void)kb_collectKeyButtonsInView:(UIView *)view into:(NSMutableArray *)buttons { for (UIView *sub in view.subviews) { if ([sub isKindOfClass:[KBKeyButton class]]) { [buttons addObject:(KBKeyButton *)sub]; continue; } if (sub.subviews.count > 0) { [self kb_collectKeyButtonsInView:sub into:buttons]; } } } #pragma mark - Key Model Construction // 创建当前布局下各行的 KBKey 列表 - (NSArray *> *)buildKeysForCurrentLayout { if (self.layoutStyle == KBKeyboardLayoutStyleNumbers) { return [self buildKeysForNumbersLayout]; } else { return [self buildKeysForLettersLayout]; } } #pragma mark - Letters Layout - (NSArray *> *)buildKeysForLettersLayout { // 字母布局(QWERTY) NSArray *r1Letters = @[ @"q", @"w", @"e", @"r", @"t", @"y", @"u", @"i", @"o", @"p" ]; NSArray *r2Letters = @[ @"a", @"s", @"d", @"f", @"g", @"h", @"j", @"k", @"l" ]; NSArray *r3Letters = @[ @"z", @"x", @"c", @"v", @"b", @"n", @"m" ]; NSMutableArray *row1 = [NSMutableArray arrayWithCapacity:r1Letters.count]; for (NSString *s in r1Letters) { [row1 addObject:[self kb_letterKeyWithChar:s]]; } NSMutableArray *row2 = [NSMutableArray arrayWithCapacity:r2Letters.count]; for (NSString *s in r2Letters) { [row2 addObject:[self kb_letterKeyWithChar:s]]; } // 第三行:Shift + Z...M + Backspace NSMutableArray *row3 = [NSMutableArray array]; KBKey *shift = [KBKey keyWithIdentifier:@"shift" title:@"⇧" output:@"" type:KBKeyTypeShift]; // Shift 键也支持大小写两套皮肤图: // - shift.caseVariant = Lower 时,使用 KBSkinIconMap 中 "shift" 对应的短名; // - shift.caseVariant = Upper 时,使用 "shift_upper" 对应的短名。 shift.caseVariant = self.shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower; [row3 addObject:shift]; for (NSString *s in r3Letters) { [row3 addObject:[self kb_letterKeyWithChar:s]]; } KBKey *backspace = [KBKey keyWithIdentifier:@"backspace" title:@"⌫" output:@"" type:KBKeyTypeBackspace]; [row3 addObject:backspace]; NSArray *row4 = [self kb_bottomControlRowKeysForLettersLayout]; return @[row1.copy, row2.copy, row3.copy, row4]; } #pragma mark - Numbers / Symbols Layout - (NSArray *> *)buildKeysForNumbersLayout { // 数字/符号布局:3 行主键 + 底部控制行 NSArray *r1 = nil; NSArray *r2 = nil; NSArray *r3 = nil; if (!self.symbolsMoreOn) { // 数字第一页(123) r1 = @[ [KBKey keyWithIdentifier:@"digit_1" title:@"1" output:@"1" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"digit_2" title:@"2" output:@"2" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"digit_3" title:@"3" output:@"3" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"digit_4" title:@"4" output:@"4" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"digit_5" title:@"5" output:@"5" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"digit_6" title:@"6" output:@"6" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"digit_7" title:@"7" output:@"7" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"digit_8" title:@"8" output:@"8" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"digit_9" title:@"9" output:@"9" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"digit_0" title:@"0" output:@"0" type:KBKeyTypeCharacter] ]; r2 = @[ [KBKey keyWithIdentifier:@"sym_minus" title:@"-" output:@"-" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_slash" title:@"/" output:@"/" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_colon" title:@":" output:@":" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_semicolon" title:@";" output:@";" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_paren_l" title:@"(" output:@"(" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_paren_r" title:@")" output:@")" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_money" title:@"¥" output:@"¥" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_amp" title:@"&" output:@"&" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_at" title:@"@" output:@"@" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_quote_double" title:@"\"" output:@"\"" type:KBKeyTypeCharacter] ]; r3 = [self kb_symbolsCommonThirdRowWithToggleIsMore:NO]; } else { // 数字第二页(#+=):前两行替换为更多符号,左下角按钮文案改为“123” r1 = @[ [KBKey keyWithIdentifier:@"sym_bracket_l" title:@"[" output:@"[" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_bracket_r" title:@"]" output:@"]" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_brace_l" title:@"{" output:@"{" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_brace_r" title:@"}" output:@"}" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_hash" title:@"#" output:@"#" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_percent" title:@"%" output:@"%" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_caret" title:@"^" output:@"^" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_asterisk" title:@"*" output:@"*" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_plus" title:@"+" output:@"+" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_equal" title:@"=" output:@"=" type:KBKeyTypeCharacter] ]; r2 = @[ [KBKey keyWithIdentifier:@"sym_underscore" title:@"_" output:@"_" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_backslash" title:@"\\" output:@"\\" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_pipe" title:@"|" output:@"|" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_tilde" title:@"~" output:@"~" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_lt" title:@"<" output:@"<" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_gt" title:@">" output:@">" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_dollar" title:@"$" output:@"$" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_euro" title:@"€" output:@"€" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_pound" title:@"£" output:@"£" type:KBKeyTypeCharacter], [KBKey keyWithIdentifier:@"sym_bullet" title:@"•" output:@"•" type:KBKeyTypeCharacter] ]; r3 = [self kb_symbolsCommonThirdRowWithToggleIsMore:YES]; } NSArray *r4 = [self kb_bottomControlRowKeysForNumbersLayout]; return @[r1, r2, r3, r4]; } #pragma mark - Key Factories // 字母键工厂:根据 shiftOn 决定显示与输出大小写 - (KBKey *)kb_letterKeyWithChar:(NSString *)charString { NSParameterAssert(charString.length == 1); NSString *lower = charString.lowercaseString; NSString *upper = charString.uppercaseString; NSString *shown = self.shiftOn ? upper : lower; NSString *identifier = [NSString stringWithFormat:@"letter_%@", lower]; KBKey *k = [KBKey keyWithIdentifier:identifier title:shown output:shown type:KBKeyTypeCharacter]; k.caseVariant = self.shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower; return k; } // 数字布局第三行公共部分(左下角是 123 或 #+=) - (NSArray *)kb_symbolsCommonThirdRowWithToggleIsMore:(BOOL)isMorePage { NSString *identifier = isMorePage ? @"symbols_toggle_123" : @"symbols_toggle_more"; NSString *title = isMorePage ? @"123" : @"#+="; KBKey *toggle = [KBKey keyWithIdentifier:identifier title:title output:@"" type:KBKeyTypeSymbolsToggle]; KBKey *comma = [KBKey keyWithIdentifier:@"sym_comma" title:@"," output:@"," type:KBKeyTypeCharacter]; KBKey *dot = [KBKey keyWithIdentifier:@"sym_dot" title:@"." output:@"." type:KBKeyTypeCharacter]; KBKey *q = [KBKey keyWithIdentifier:@"sym_question" title:@"?" output:@"?" type:KBKeyTypeCharacter]; KBKey *ex = [KBKey keyWithIdentifier:@"sym_exclam" title:@"!" output:@"!" type:KBKeyTypeCharacter]; KBKey *quote = [KBKey keyWithIdentifier:@"sym_quote_single" title:@"'" output:@"'" type:KBKeyTypeCharacter]; KBKey *back = [KBKey keyWithIdentifier:@"backspace" title:@"⌫" output:@"" type:KBKeyTypeBackspace]; return @[ toggle, comma, dot, q, ex, quote, back ]; } // 底部控制行(字母布局) - (NSArray *)kb_bottomControlRowKeysForLettersLayout { KBKey *mode123 = [KBKey keyWithIdentifier:@"mode_123" title:@"123" output:@"" type:KBKeyTypeModeChange]; KBKey *emoji = [KBKey keyWithIdentifier:KBKeyIdentifierEmojiPanel title:@"😊" output:@"" type:KBKeyTypeCustom]; KBKey *space = [KBKey keyWithIdentifier:@"space" title:@"space" output:@" " type:KBKeyTypeSpace]; KBKey *ret = [KBKey keyWithIdentifier:@"return" title:KBLocalized(@"Send") output:@"\n" type:KBKeyTypeReturn]; return @[ mode123, emoji, space, ret ]; } // 底部控制行(数字布局) - (NSArray *)kb_bottomControlRowKeysForNumbersLayout { KBKey *modeABC = [KBKey keyWithIdentifier:@"mode_abc" title:@"abc" output:@"" type:KBKeyTypeModeChange]; KBKey *emoji = [KBKey keyWithIdentifier:KBKeyIdentifierEmojiPanel title:@"😊" output:@"" type:KBKeyTypeCustom]; KBKey *space = [KBKey keyWithIdentifier:@"space" title:@"space" output:@" " type:KBKeyTypeSpace]; KBKey *ret = [KBKey keyWithIdentifier:@"return" title:KBLocalized(@"Send") output:@"\n" type:KBKeyTypeReturn]; return @[ modeABC, emoji, space, ret ]; } #pragma mark - Row Building - (void)buildRow:(UIView *)row withRowConfig:(KBKeyboardRowConfig *)rowConfig { if (!row || !rowConfig) { return; } CGFloat gap = [self kb_gapForRow:rowConfig]; CGFloat insetLeft = [self kb_insetLeftForRow:rowConfig]; CGFloat insetRight = [self kb_insetRightForRow:rowConfig]; if (rowConfig.segments) { KBKeyboardRowSegments *segments = rowConfig.segments; NSArray *leftItems = [segments leftItems]; NSArray *centerItems = [segments centerItems]; NSArray *rightItems = [segments rightItems]; UIView *leftContainer = [UIView new]; UIView *centerContainer = [UIView new]; UIView *rightContainer = [UIView new]; [row addSubview:leftContainer]; [row addSubview:centerContainer]; [row addSubview:rightContainer]; [leftContainer mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(row.mas_left).offset(insetLeft); make.top.bottom.equalTo(row); }]; [rightContainer mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(row.mas_right).offset(-insetRight); make.top.bottom.equalTo(row); }]; [centerContainer mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(row); make.top.bottom.equalTo(row); make.left.greaterThanOrEqualTo(leftContainer.mas_right).offset(gap); make.right.lessThanOrEqualTo(rightContainer.mas_left).offset(-gap); }]; if (leftItems.count == 0) { [leftContainer mas_makeConstraints:^(MASConstraintMaker *make) { make.width.mas_equalTo(0); }]; } if (centerItems.count == 0) { [centerContainer mas_makeConstraints:^(MASConstraintMaker *make) { make.width.mas_equalTo(0); }]; } if (rightItems.count == 0) { [rightContainer mas_makeConstraints:^(MASConstraintMaker *make) { make.width.mas_equalTo(0); }]; } [self kb_buildButtonsInContainer:leftContainer items:leftItems gap:gap insetLeft:0 insetRight:0 alignCenter:NO]; [self kb_buildButtonsInContainer:centerContainer items:centerItems gap:gap insetLeft:0 insetRight:0 alignCenter:NO]; [self kb_buildButtonsInContainer:rightContainer items:rightItems gap:gap insetLeft:0 insetRight:0 alignCenter:NO]; return; } BOOL alignCenter = [rowConfig.align.lowercaseString isEqualToString:@"center"]; [self kb_buildButtonsInContainer:row items:[rowConfig resolvedItems] gap:gap insetLeft:insetLeft insetRight:insetRight alignCenter:alignCenter]; } - (void)kb_buildButtonsInContainer:(UIView *)container items:(NSArray *)items gap:(CGFloat)gap insetLeft:(CGFloat)insetLeft insetRight:(CGFloat)insetRight alignCenter:(BOOL)alignCenter { if (items.count == 0) { return; } UIView *leftSpacer = nil; UIView *rightSpacer = nil; if (alignCenter) { leftSpacer = [UIView new]; rightSpacer = [UIView new]; [container addSubview:leftSpacer]; [container addSubview:rightSpacer]; [leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(container.mas_left).offset(insetLeft); make.top.bottom.equalTo(container); }]; [rightSpacer mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(container.mas_right).offset(-insetRight); make.top.bottom.equalTo(container); }]; [leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) { make.width.equalTo(rightSpacer); }]; } KBKeyButton *previous = nil; for (KBKeyboardRowItem *item in items) { KBKeyButton *btn = [self kb_buttonForItem:item]; if (!btn) { continue; } [container addSubview:btn]; [btn mas_makeConstraints:^(MASConstraintMaker *make) { make.top.bottom.equalTo(container); if (previous) { make.left.equalTo(previous.mas_right).offset(gap); } else { if (leftSpacer) { make.left.equalTo(leftSpacer.mas_right).offset(gap); } else { make.left.equalTo(container.mas_left).offset(insetLeft); } } }]; CGFloat width = [self kb_widthForItem:item key:btn.key]; if (width > 0.0) { [btn mas_makeConstraints:^(MASConstraintMaker *make) { make.width.mas_equalTo(width); }]; } previous = btn; } if (!previous) { return; } [previous mas_makeConstraints:^(MASConstraintMaker *make) { if (rightSpacer) { make.right.equalTo(rightSpacer.mas_left).offset(-gap); } else { make.right.equalTo(container.mas_right).offset(-insetRight); } }]; } - (void)buildRow:(UIView *)row withKeys:(NSArray *)keys { [self buildRow:row withKeys:keys edgeSpacerMultiplier:0.0]; } - (void)buildRow:(UIView *)row withKeys:(NSArray *)keys edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier { // 第 4 行(底部控制行)使用单独的布局规则: // 123/ABC、Emoji、Send 给定尺寸,Space 自动吃掉剩余宽度。 BOOL isBottomControlRow = [self kb_isBottomControlRowWithKeys:keys]; CGFloat spacing = 0; // 键与键之间的间距 UIView *previous = nil; UIView *leftSpacer = nil; UIView *rightSpacer = nil; if (edgeSpacerMultiplier > 0.0) { leftSpacer = [UIView new]; rightSpacer = [UIView new]; leftSpacer.backgroundColor = [UIColor clearColor]; rightSpacer.backgroundColor = [UIColor clearColor]; [row addSubview:leftSpacer]; [row addSubview:rightSpacer]; [leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(row.mas_left).offset(kKBRowHorizontalInset); make.centerY.equalTo(row); make.height.mas_equalTo(1); }]; [rightSpacer mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(row.mas_right).offset(-kKBRowHorizontalInset); make.centerY.equalTo(row); make.height.mas_equalTo(1); }]; } for (NSInteger i = 0; i < keys.count; i++) { KBKey *key = keys[i]; KBKeyButton *btn = [[KBKeyButton alloc] init]; btn.key = key; [btn setTitle:key.title forState:UIControlStateNormal]; // 在设置完标题后,按当前皮肤应用图标与文字显隐 [btn applyThemeForCurrentKey]; [btn addTarget:self action:@selector(onKeyTapped:) forControlEvents:UIControlEventTouchUpInside]; [row addSubview:btn]; if (key.type == KBKeyTypeBackspace) { [self.backspaceHandler bindDeleteButton:btn showClearLabel:YES]; } // Shift 按钮选中态随大小写状态变化 if (key.type == KBKeyTypeShift) { btn.selected = self.shiftOn; } [btn mas_makeConstraints:^(MASConstraintMaker *make) { make.top.bottom.equalTo(row); if (previous) { make.left.equalTo(previous.mas_right).offset(spacing); } else { if (leftSpacer) { make.left.equalTo(leftSpacer.mas_right).offset(spacing); } else { make.left.equalTo(row.mas_left).offset(kKBRowHorizontalInset); } } }]; // 字符键:等宽 if (key.type == KBKeyTypeCharacter) { if (previous && [previous isKindOfClass:[KBKeyButton class]]) { KBKeyButton *prevBtn = (KBKeyButton *)previous; if (prevBtn.key.type == KBKeyTypeCharacter) { [btn mas_makeConstraints:^(MASConstraintMaker *make) { make.width.equalTo(previous); }]; } } } else { // special keys: 宽度在第二遍统一设置 } previous = btn; } // 右侧使用内边距或右占位 if (previous) { [previous mas_makeConstraints:^(MASConstraintMaker *make) { if (rightSpacer) { make.right.equalTo(rightSpacer.mas_left).offset(-spacing); } else { make.right.equalTo(row.mas_right).offset(-kKBRowHorizontalInset); } }]; } // 底部控制行:在第一轮已完成左右约束的前提下,仅给 123/ABC、Emoji、Send 指定宽度, // Space 不加宽度约束,让其自动填充剩余空间。 if (isBottomControlRow) { [self kb_applyBottomControlRowWidthInRow:row]; return; } // 第二遍:以首个字符键为基准,统一设置特殊键宽度倍数 KBKeyButton *firstChar = nil; BOOL hasCharacterInRow = NO; for (UIView *v in row.subviews) { if (![v isKindOfClass:[KBKeyButton class]]) continue; KBKeyButton *b = (KBKeyButton *)v; if (b.key.type == KBKeyTypeCharacter) { firstChar = b; hasCharacterInRow = YES; break; } } // 若该行没有字符键(例如底部控制行之外的特殊行),则使用行内第一个按钮作为基准宽度 if (!firstChar) { for (UIView *v in row.subviews) { if ([v isKindOfClass:[KBKeyButton class]]) { firstChar = (KBKeyButton *)v; break; } } } if (firstChar) { // 如果该行本身没有字符键(如底部控制行),且基准按钮是 123/ABC/#+= 等, // 也将其约束为 1:1,避免 123/ABC 不是正方形。 if (!hasCharacterInRow && (firstChar.key.type == KBKeyTypeModeChange || firstChar.key.type == KBKeyTypeSymbolsToggle || firstChar.key.type == KBKeyTypeCustom)) { [firstChar mas_makeConstraints:^(MASConstraintMaker *make) { make.width.equalTo(firstChar.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier); }]; } for (UIView *v in row.subviews) { if (![v isKindOfClass:[KBKeyButton class]]) continue; KBKeyButton *b = (KBKeyButton *)v; // 避免对基准按钮自身添加 self == self * k 的无效约束 if (b == firstChar) continue; if (b.key.type == KBKeyTypeCharacter) continue; BOOL isBottomModeKey = (b.key.type == KBKeyTypeModeChange) || (b.key.type == KBKeyTypeSymbolsToggle) || (b.key.type == KBKeyTypeCustom); // 一类键强制近似正方形(宽 ~ 高) if (b.key.type == KBKeyTypeShift || b.key.type == KBKeyTypeBackspace || isBottomModeKey) { [b mas_makeConstraints:^(MASConstraintMaker *make) { make.width.equalTo(b.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier); }]; continue; } CGFloat multiplier = 1.5; // Space:宽度更大 if (b.key.type == KBKeyTypeSpace) { multiplier = kKBSpaceWidthMultiplier; } // Send 按钮:宽度为基准键的 2.4 倍 else if (b.key.type == KBKeyTypeReturn) { multiplier = kKBReturnWidthMultiplier; } // 其它特殊键(如 Globe)保持适度放大 else if (b.key.type == KBKeyTypeGlobe) { multiplier = 1.5; } [b mas_makeConstraints:^(MASConstraintMaker *make) { make.width.equalTo(firstChar).multipliedBy(multiplier); }]; } // 如果有左右占位,则把占位宽度设置为字符键宽度的一定倍数,以实现整体居中; // 同时强约束左右占位宽度相等,避免在某些系统上由于布局冲突导致只压缩一侧, // 出现“左侧有空隙,右侧无空隙”的情况。 if (leftSpacer && rightSpacer) { // 1) 左右占位宽度必须相等 [leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) { make.width.equalTo(rightSpacer); }]; // 2) 同时都接近字符键宽度的 edgeSpacerMultiplier 倍数 [rightSpacer mas_makeConstraints:^(MASConstraintMaker *make) { make.width.equalTo(firstChar).multipliedBy(edgeSpacerMultiplier); }]; } } } #pragma mark - Row Helpers (Bottom Control Row) // 判断是否为底部控制行:包含 Space + Return,且有 ModeChange/SymbolsToggle, // 并且不再含字符键。 - (BOOL)kb_isBottomControlRowWithKeys:(NSArray *)keys { BOOL hasSpace = NO; BOOL hasReturn = NO; BOOL hasModeOrSymbols = NO; for (KBKey *k in keys) { if (k.type == KBKeyTypeCharacter) { return NO; } if (k.type == KBKeyTypeSpace) { hasSpace = YES; } else if (k.type == KBKeyTypeReturn) { hasReturn = YES; } else if (k.type == KBKeyTypeModeChange || k.type == KBKeyTypeSymbolsToggle) { hasModeOrSymbols = YES; } } return hasSpace && hasReturn && hasModeOrSymbols; } // 为底部控制行设置宽度: // - 123/ABC、Emoji:正方形(宽 = 行高 * multiplier) // - Send:宽 = 模式键宽度的 2 倍 // - Space:不加宽度约束,依靠左右约束自动填充剩余空间。 - (void)kb_applyBottomControlRowWidthInRow:(UIView *)row { KBKeyButton *modeBtn = nil; NSMutableArray *customButtons = [NSMutableArray array]; KBKeyButton *spaceBtn = nil; KBKeyButton *retBtn = nil; for (UIView *v in row.subviews) { if (![v isKindOfClass:[KBKeyButton class]]) continue; KBKeyButton *b = (KBKeyButton *)v; switch (b.key.type) { case KBKeyTypeModeChange: case KBKeyTypeSymbolsToggle: modeBtn = b; break; case KBKeyTypeCustom: [customButtons addObject:b]; break; case KBKeyTypeSpace: spaceBtn = b; break; case KBKeyTypeReturn: retBtn = b; break; default: break; } } if (!modeBtn || customButtons.count == 0 || !spaceBtn || !retBtn) { return; } [modeBtn mas_makeConstraints:^(MASConstraintMaker *make) { make.width.equalTo(row.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier); }]; for (KBKeyButton *custom in customButtons) { [custom mas_makeConstraints:^(MASConstraintMaker *make) { make.width.equalTo(row.mas_height).multipliedBy(kKBSpecialKeySquareMultiplier); }]; } [retBtn mas_makeConstraints:^(MASConstraintMaker *make) { make.width.equalTo(modeBtn.mas_width).multipliedBy(2.0); }]; // Space 不设置宽度;通过此前已建立的左右约束自动占满剩余宽度。 } #pragma mark - Config Helpers - (KBKeyboardLayoutConfig *)kb_layoutConfig { if (!self.layoutConfig) { self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig]; } return self.layoutConfig; } - (KBKeyboardLayout *)kb_layoutForName:(NSString *)name { return [[self kb_layoutConfig] layoutForName:name]; } - (KBKeyboardLayout *)kb_currentLayout { if (self.layoutStyle == KBKeyboardLayoutStyleNumbers) { return [self kb_layoutForName:(self.symbolsMoreOn ? @"symbolsMore" : @"numbers")]; } return [self kb_layoutForName:@"letters"]; } - (void)kb_buildLegacyLayout { self.keysForRows = [self buildKeysForCurrentLayout]; if (self.keysForRows.count < 4) { return; } [self buildRow:self.row1 withKeys:self.keysForRows[0]]; CGFloat row2Spacer = (self.layoutStyle == KBKeyboardLayoutStyleLetters) ? kKBLettersRow2EdgeSpacerMultiplier : 0.0; [self buildRow:self.row2 withKeys:self.keysForRows[1] edgeSpacerMultiplier:row2Spacer]; [self buildRow:self.row3 withKeys:self.keysForRows[2]]; [self buildRow:self.row4 withKeys:self.keysForRows[3]]; } - (CGFloat)kb_scaledValue:(CGFloat)designValue { KBKeyboardLayoutConfig *config = [self kb_layoutConfig]; if (config) { return [config scaledValue:designValue]; } return KBFit(designValue); } - (CGFloat)kb_numberValue:(NSNumber *)value defaultValue:(CGFloat)defaultValue { if ([value isKindOfClass:[NSNumber class]]) { return value.doubleValue; } return defaultValue; } - (CGFloat)kb_metricValue:(NSNumber *)value fallback:(NSNumber *)fallback defaultValue:(CGFloat)defaultValue { CGFloat v = [self kb_numberValue:value defaultValue:-1.0]; if (v < 0.0) { v = [self kb_numberValue:fallback defaultValue:defaultValue]; } if (v < 0.0) { v = defaultValue; } return [self kb_scaledValue:v]; } - (CGFloat)kb_rowHeightForRow:(KBKeyboardRowConfig *)row { KBKeyboardLayoutConfig *config = [self kb_layoutConfig]; NSNumber *height = row.height ?: config.metrics.keyHeight; CGFloat value = [self kb_numberValue:height defaultValue:40.0]; return [self kb_scaledValue:value]; } - (CGFloat)kb_gapForRow:(KBKeyboardRowConfig *)row { KBKeyboardLayoutConfig *config = [self kb_layoutConfig]; return [self kb_metricValue:row.gap fallback:config.metrics.gap defaultValue:5.0]; } - (CGFloat)kb_insetLeftForRow:(KBKeyboardRowConfig *)row { KBKeyboardLayoutConfig *config = [self kb_layoutConfig]; return [self kb_metricValue:row.insetLeft fallback:config.metrics.edgeInset defaultValue:0.0]; } - (CGFloat)kb_insetRightForRow:(KBKeyboardRowConfig *)row { KBKeyboardLayoutConfig *config = [self kb_layoutConfig]; return [self kb_metricValue:row.insetRight fallback:config.metrics.edgeInset defaultValue:0.0]; } - (KBKeyButton *)kb_buttonForItem:(KBKeyboardRowItem *)item { if (item.itemId.length == 0) { return nil; } KBKeyboardKeyDef *def = [[self kb_layoutConfig] keyDefForIdentifier:item.itemId]; KBKey *key = [self kb_keyForItemId:item.itemId]; if (!key) { return nil; } KBKeyButton *btn = [[KBKeyButton alloc] init]; btn.key = key; [btn setTitle:key.title forState:UIControlStateNormal]; UIColor *bgColor = [self kb_backgroundColorForItem:item keyDef:def]; if (bgColor) { btn.customBackgroundColor = bgColor; } CGFloat fontSize = [self kb_fontSizeForItem:item key:key]; if (fontSize > 0.0) { btn.titleLabel.font = [UIFont systemFontOfSize:fontSize weight:UIFontWeightSemibold]; } [btn applyThemeForCurrentKey]; [btn addTarget:self action:@selector(onKeyTapped:) forControlEvents:UIControlEventTouchUpInside]; if (key.type == KBKeyTypeBackspace) { [self.backspaceHandler bindDeleteButton:btn showClearLabel:YES]; } if (key.type == KBKeyTypeShift) { btn.selected = self.shiftOn; } [self kb_applySymbolIfNeededForButton:btn keyDef:def fontSize:fontSize]; return btn; } - (void)kb_applySymbolIfNeededForButton:(KBKeyButton *)button keyDef:(KBKeyboardKeyDef *)def fontSize:(CGFloat)fontSize { if (!button || !def) { return; } if (button.iconView.image != nil) { return; } NSString *symbolName = button.isSelected ? def.selectedSymbolName : def.symbolName; if (symbolName.length == 0) { return; } UIImage *image = [UIImage systemImageNamed:symbolName]; if (!image) { return; } UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:fontSize weight:UIFontWeightSemibold]; image = [image imageWithConfiguration:config]; button.iconView.image = image; button.iconView.hidden = NO; button.iconView.contentMode = UIViewContentModeCenter; button.titleLabel.hidden = YES; UIColor *textColor = [KBSkinManager shared].current.keyTextColor ?: [UIColor blackColor]; button.iconView.tintColor = button.isSelected ? [UIColor blackColor] : textColor; } - (UIColor *)kb_backgroundColorForItem:(KBKeyboardRowItem *)item keyDef:(KBKeyboardKeyDef *)def { NSString *hex = def.backgroundColor; if (hex.length == 0) { hex = [self kb_layoutConfig].defaultKeyBackground; } if (hex.length == 0) { return nil; } return [KBSkinManager colorFromHexString:hex defaultColor:nil]; } - (CGFloat)kb_metricWidthForKey:(NSString *)key { KBKeyboardLayoutMetrics *m = [self kb_layoutConfig].metrics; if ([key isEqualToString:@"letterWidth"]) { return m.letterWidth.doubleValue; } if ([key isEqualToString:@"controlWidth"]) { return m.controlWidth.doubleValue; } if ([key isEqualToString:@"sendWidth"]) { return m.sendWidth.doubleValue; } if ([key isEqualToString:@"symbolsWideWidth"]) { return m.symbolsWideWidth.doubleValue; } if ([key isEqualToString:@"symbolsSideWidth"]) { return m.symbolsSideWidth.doubleValue; } return 0.0; } - (CGFloat)kb_widthForItem:(KBKeyboardRowItem *)item key:(KBKey *)key { CGFloat width = 0.0; if (item.widthValue.doubleValue > 0.0) { width = item.widthValue.doubleValue; } else if (item.width.length > 0) { if ([item.width.lowercaseString isEqualToString:@"flex"]) { return 0.0; } width = [self kb_metricWidthForKey:item.width]; if (width <= 0.0) { width = item.width.doubleValue; } } if (width <= 0.0) { KBKeyboardLayoutMetrics *m = [self kb_layoutConfig].metrics; if ([item.itemId hasPrefix:@"letter:"] || [item.itemId hasPrefix:@"digit:"] || [item.itemId hasPrefix:@"sym:"]) { width = m.letterWidth.doubleValue; } else if (key.type == KBKeyTypeReturn) { width = m.sendWidth.doubleValue; } else if (key.type == KBKeyTypeSpace) { return 0.0; } else { width = m.controlWidth.doubleValue; } } if (width <= 0.0) { if ([item.itemId hasPrefix:@"letter:"] || [item.itemId hasPrefix:@"digit:"] || [item.itemId hasPrefix:@"sym:"]) { width = 32.0; } else if (key.type == KBKeyTypeReturn) { width = 88.0; } else if (key.type == KBKeyTypeSpace) { return 0.0; } else { width = 41.0; } } return width > 0.0 ? [self kb_scaledValue:width] : 0.0; } - (CGFloat)kb_fontSizeForItem:(KBKeyboardRowItem *)item key:(KBKey *)key { NSString *fontKey = nil; if ([item.itemId hasPrefix:@"letter:"]) { fontKey = @"letter"; } else if ([item.itemId hasPrefix:@"digit:"]) { fontKey = @"digit"; } else if ([item.itemId hasPrefix:@"sym:"]) { fontKey = @"symbol"; } else { KBKeyboardKeyDef *def = [[self kb_layoutConfig] keyDefForIdentifier:item.itemId]; fontKey = def.font; } if (fontKey.length == 0) { switch (key.type) { case KBKeyTypeModeChange: case KBKeyTypeSymbolsToggle: fontKey = @"mode"; break; case KBKeyTypeSpace: fontKey = @"space"; break; case KBKeyTypeReturn: fontKey = @"send"; break; default: fontKey = @"symbol"; break; } } return [self kb_fontSizeForFontKey:fontKey]; } - (CGFloat)kb_fontSizeForFontKey:(NSString *)fontKey { KBKeyboardLayoutFonts *fonts = [self kb_layoutConfig].fonts; CGFloat size = 0.0; if ([fontKey isEqualToString:@"letter"]) { size = fonts.letter.doubleValue; } else if ([fontKey isEqualToString:@"digit"]) { size = fonts.digit.doubleValue; } else if ([fontKey isEqualToString:@"symbol"]) { size = fonts.symbol.doubleValue; } else if ([fontKey isEqualToString:@"mode"]) { size = fonts.mode.doubleValue; } else if ([fontKey isEqualToString:@"space"]) { size = fonts.space.doubleValue; } else if ([fontKey isEqualToString:@"send"]) { size = fonts.send.doubleValue; } if (size <= 0.0) { size = 18.0; } return [self kb_scaledValue:size]; } - (KBKey *)kb_keyForItemId:(NSString *)itemId { if (itemId.length == 0) { return nil; } KBKeyboardKeyDef *def = [[self kb_layoutConfig] keyDefForIdentifier:itemId]; if (def) { return [self kb_keyFromDef:def identifier:itemId]; } NSRange range = [itemId rangeOfString:@":"]; if (range.location != NSNotFound) { NSString *prefix = [itemId substringToIndex:range.location]; NSString *value = [itemId substringFromIndex:range.location + 1]; if ([prefix isEqualToString:@"letter"]) { if (value.length == 1) { return [self kb_letterKeyWithChar:value]; } return nil; } if ([prefix isEqualToString:@"digit"]) { NSString *identifier = [NSString stringWithFormat:@"digit_%@", value]; KBKey *k = [KBKey keyWithIdentifier:identifier title:value output:value type:KBKeyTypeCharacter]; k.caseVariant = KBKeyCaseVariantNone; return k; } if ([prefix isEqualToString:@"sym"]) { NSString *identifier = [self kb_identifierForSymbol:value]; KBKey *k = [KBKey keyWithIdentifier:identifier title:value output:value type:KBKeyTypeCharacter]; k.caseVariant = KBKeyCaseVariantNone; return k; } } return nil; } - (KBKey *)kb_keyFromDef:(KBKeyboardKeyDef *)def identifier:(NSString *)identifier { KBKeyType type = [self kb_keyTypeForDef:def]; NSString *title = def.title ?: @""; if (type == KBKeyTypeShift && self.shiftOn && def.selectedTitle.length > 0) { title = def.selectedTitle; } NSString *output = @""; switch (type) { case KBKeyTypeSpace: output = @" "; break; case KBKeyTypeReturn: output = @"\n"; break; default: output = @""; break; } NSString *finalId = identifier; if ([identifier isEqualToString:@"emoji"]) { finalId = KBKeyIdentifierEmojiPanel; } else if ([identifier isEqualToString:@"send"]) { finalId = @"return"; } KBKey *k = [KBKey keyWithIdentifier:finalId title:title output:output type:type]; if (type == KBKeyTypeShift) { k.caseVariant = self.shiftOn ? KBKeyCaseVariantUpper : KBKeyCaseVariantLower; } else { k.caseVariant = KBKeyCaseVariantNone; } return k; } - (KBKeyType)kb_keyTypeForDef:(KBKeyboardKeyDef *)def { NSString *type = def.type.lowercaseString; if ([type isEqualToString:@"shift"]) return KBKeyTypeShift; if ([type isEqualToString:@"backspace"]) return KBKeyTypeBackspace; if ([type isEqualToString:@"mode"]) return KBKeyTypeModeChange; if ([type isEqualToString:@"symbolstoggle"]) return KBKeyTypeSymbolsToggle; if ([type isEqualToString:@"space"]) return KBKeyTypeSpace; if ([type isEqualToString:@"return"]) return KBKeyTypeReturn; if ([type isEqualToString:@"globe"]) return KBKeyTypeGlobe; return KBKeyTypeCustom; } - (NSString *)kb_identifierForSymbol:(NSString *)symbol { if (symbol.length == 0) { return nil; } static NSDictionary *map = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ map = @{ @"-": @"sym_minus", @"/": @"sym_slash", @":": @"sym_colon", @";": @"sym_semicolon", @"(": @"sym_paren_l", @")": @"sym_paren_r", @"¥": @"sym_money", @"¥": @"sym_money", @"&": @"sym_amp", @"@": @"sym_at", @"\"": @"sym_quote_double", @"“": @"sym_quote_double", @"”": @"sym_quote_double", @".": @"sym_dot", @",": @"sym_comma", @"?": @"sym_question", @"!": @"sym_exclam", @"'": @"sym_quote_single", @"‘": @"sym_quote_single", @"’": @"sym_quote_single", @"[": @"sym_bracket_l", @"]": @"sym_bracket_r", @"{": @"sym_brace_l", @"}": @"sym_brace_r", @"#": @"sym_hash", @"%": @"sym_percent", @"^": @"sym_caret", @"*": @"sym_asterisk", @"+": @"sym_plus", @"=": @"sym_equal", @"_": @"sym_underscore", @"\\": @"sym_backslash", @"|": @"sym_pipe", @"~": @"sym_tilde", @"<": @"sym_lt", @">": @"sym_gt", @"€": @"sym_euro", @"$": @"sym_dollar", @"·": @"sym_bullet" }; }); return map[symbol]; } #pragma mark - Actions - (void)onKeyTapped:(KBKeyButton *)sender { KBKey *key = sender.key; if (key.type == KBKeyTypeShift) { self.shiftOn = !self.shiftOn; [self reloadKeys]; return; } if (key.type == KBKeyTypeSymbolsToggle) { // 在数字布局内切换 123 <-> #+= self.symbolsMoreOn = !self.symbolsMoreOn; [self reloadKeys]; return; } if ([self.delegate respondsToSelector:@selector(keyboardView:didTapKey:)]) { [self.delegate keyboardView:self didTapKey:key]; } } // 在字符键按下时,显示一个上方气泡预览(类似系统键盘)。 - (void)showPreviewForButton:(KBKeyButton *)button { KBKey *key = button.key; if (key.type != KBKeyTypeCharacter) return; if (!self.previewView) { self.previewView = [[KBKeyPreviewView alloc] initWithFrame:CGRectZero]; self.previewView.hidden = YES; [self addSubview:self.previewView]; } [self.previewView configureWithKey:key icon:button.iconView.image]; // 计算预览视图位置:在按钮上方稍微偏上 CGRect btnFrameInSelf = [button convertRect:button.bounds toView:self]; // CGFloat previewWidth = MAX(CGRectGetWidth(btnFrameInSelf) * 1.4, 42.0); CGFloat previewWidth = 42; CGFloat previewHeight = CGRectGetHeight(btnFrameInSelf) * 1.2; CGFloat centerX = CGRectGetMidX(btnFrameInSelf); CGFloat centerY = CGRectGetMinY(btnFrameInSelf) - previewHeight * 0.6; // 修复:原来写死 40,这里用真正计算出的 previewWidth self.previewView.frame = CGRectMake(0, 0, previewWidth, previewHeight); self.previewView.center = CGPointMake(centerX, centerY); self.previewView.alpha = 0.0; self.previewView.hidden = NO; [UIView animateWithDuration:kKBPreviewShowDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseOut animations:^{ self.previewView.alpha = 1.0; } completion:nil]; } - (void)hidePreview { if (!self.previewView || self.previewView.isHidden) return; [UIView animateWithDuration:kKBPreviewHideDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn animations:^{ self.previewView.alpha = 0.0; } completion:^(BOOL finished) { self.previewView.hidden = YES; }]; } #pragma mark - Lazy - (UIView *)row1 { if (!_row1) _row1 = [UIView new]; return _row1; } - (UIView *)row2 { if (!_row2) _row2 = [UIView new]; return _row2; } - (UIView *)row3 { if (!_row3) _row3 = [UIView new]; return _row3; } - (UIView *)row4 { if (!_row4) _row4 = [UIView new]; return _row4; } @end