diff --git a/CustomKeyboard/View/KBKeyboardView.m b/CustomKeyboard/View/KBKeyboardView.m index e4c8c84..63bcd48 100644 --- a/CustomKeyboard/View/KBKeyboardView.m +++ b/CustomKeyboard/View/KBKeyboardView.m @@ -22,6 +22,10 @@ static const NSTimeInterval kKBBackspaceChunkRepeatInterval = 0.1; static const NSTimeInterval kKBBackspaceChunkFastDelay = 1.4; static const NSInteger kKBBackspaceChunkSize = 6; static const NSInteger kKBBackspaceChunkSizeFast = 12; +static const CGFloat kKBBackspaceClearLabelCornerRadius = 8.0; +static const CGFloat kKBBackspaceClearLabelHeight = 26.0; +static const CGFloat kKBBackspaceClearLabelPaddingX = 10.0; +static const CGFloat kKBBackspaceClearLabelTopGap = 6.0; static const NSTimeInterval kKBPreviewShowDuration = 0.08; static const NSTimeInterval kKBPreviewHideDuration = 0.06; @@ -50,6 +54,10 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { // 长按退格的一次次删除控制标记(不使用 NSTimer,仅用 GCD 递归调度) @property (nonatomic, assign) BOOL backspaceHoldActive; @property (nonatomic, assign) NSTimeInterval backspaceHoldStartTime; +@property (nonatomic, assign) BOOL backspaceChunkModeActive; +@property (nonatomic, assign) BOOL backspaceClearHighlighted; +@property (nonatomic, weak) KBKeyButton *backspaceButton; +@property (nonatomic, strong) UILabel *backspaceClearLabel; @property (nonatomic, strong) KBKeyPreviewView *previewView; @end @@ -110,6 +118,10 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { #pragma mark - Public - (void)reloadKeys { + self.backspaceButton = nil; + self.backspaceChunkModeActive = NO; + self.backspaceClearHighlighted = NO; + [self kb_hideBackspaceClearLabel]; // 移除旧按钮 for (UIView *row in @[self.row1, self.row2, self.row3, self.row4]) { [row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; @@ -391,8 +403,10 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier { [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onBackspaceLongPress:)]; lp.minimumPressDuration = kKBBackspaceLongPressMinDuration; + lp.allowableMovement = CGFLOAT_MAX; lp.cancelsTouchesInView = YES; // 被识别为长按时,取消普通点击 [btn addGestureRecognizer:lp]; + self.backspaceButton = btn; } // Shift 按钮选中态随大小写状态变化 @@ -686,12 +700,18 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier { case UIGestureRecognizerStateBegan: { self.backspaceHoldActive = YES; self.backspaceHoldStartTime = [NSDate date].timeIntervalSinceReferenceDate; + self.backspaceChunkModeActive = NO; + [self kb_setBackspaceClearHighlighted:NO]; + [self kb_hideBackspaceClearLabel]; [self kb_backspaceStep]; } break; + case UIGestureRecognizerStateChanged: { + [self kb_handleBackspaceLongPressChanged:gr]; + } break; case UIGestureRecognizerStateEnded: case UIGestureRecognizerStateCancelled: case UIGestureRecognizerStateFailed: { - self.backspaceHoldActive = NO; + [self kb_handleBackspaceLongPressEnded:gr]; } break; default: break; } @@ -709,6 +729,10 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier { if (before.length <= 0) { self.backspaceHoldActive = NO; return; } NSTimeInterval elapsed = [NSDate date].timeIntervalSinceReferenceDate - self.backspaceHoldStartTime; NSInteger deleteCount = [self kb_backspaceDeleteCountForContext:before elapsed:elapsed]; + if (!self.backspaceChunkModeActive && elapsed >= kKBBackspaceChunkStartDelay) { + self.backspaceChunkModeActive = YES; + [self kb_showBackspaceClearLabelIfNeeded]; + } for (NSInteger i = 0; i < deleteCount; i++) { [proxy deleteBackward]; } @@ -784,6 +808,144 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier { return MAX(deleteCount, 1); } +- (void)kb_handleBackspaceLongPressChanged:(UILongPressGestureRecognizer *)gr { + if (!self.backspaceHoldActive) { return; } + NSTimeInterval elapsed = [NSDate date].timeIntervalSinceReferenceDate - self.backspaceHoldStartTime; + if (elapsed < kKBBackspaceChunkStartDelay) { return; } + [self kb_showBackspaceClearLabelIfNeeded]; + CGPoint point = [gr locationInView:self]; + BOOL inside = [self kb_isPointInsideBackspaceClearLabel:point]; + [self kb_setBackspaceClearHighlighted:inside]; +} + +- (void)kb_handleBackspaceLongPressEnded:(UILongPressGestureRecognizer *)gr { + BOOL shouldClear = self.backspaceClearHighlighted; + if (!shouldClear && gr) { + CGPoint point = [gr locationInView:self]; + shouldClear = [self kb_isPointInsideBackspaceClearLabel:point]; + } + self.backspaceHoldActive = NO; + self.backspaceChunkModeActive = NO; + [self kb_hideBackspaceClearLabel]; + if (shouldClear) { + [self kb_clearAllInput]; + } +} + +- (void)kb_showBackspaceClearLabelIfNeeded { + if (!self.backspaceButton) { return; } + UILabel *label = self.backspaceClearLabel; + [self kb_refreshBackspaceClearLabelColors]; + if (!label.superview) { + [self addSubview:label]; + } + [self kb_updateBackspaceClearLabelFrame]; + [self bringSubviewToFront:label]; + if (label.hidden) { + label.alpha = 0.0; + label.hidden = NO; + [UIView animateWithDuration:0.12 animations:^{ + label.alpha = 1.0; + }]; + } +} + +- (void)kb_hideBackspaceClearLabel { + if (!_backspaceClearLabel || _backspaceClearLabel.hidden) { return; } + _backspaceClearLabel.hidden = YES; + _backspaceClearLabel.alpha = 1.0; + [self kb_setBackspaceClearHighlighted:NO]; +} + +- (void)kb_updateBackspaceClearLabelFrame { + if (!self.backspaceButton || !self.backspaceClearLabel) { return; } + CGRect btnFrame = [self.backspaceButton convertRect:self.backspaceButton.bounds toView:self]; + UILabel *label = self.backspaceClearLabel; + CGSize textSize = [label sizeThatFits:CGSizeMake(CGFLOAT_MAX, kKBBackspaceClearLabelHeight)]; + CGFloat width = MAX(textSize.width + kKBBackspaceClearLabelPaddingX * 2.0, 60.0); + CGFloat height = kKBBackspaceClearLabelHeight; + CGFloat x = CGRectGetMidX(btnFrame) - width * 0.5; + CGFloat y = CGRectGetMinY(btnFrame) - height - kKBBackspaceClearLabelTopGap; + if (x < kKBRowHorizontalInset) { x = kKBRowHorizontalInset; } + if (x + width > CGRectGetWidth(self.bounds) - kKBRowHorizontalInset) { + x = CGRectGetWidth(self.bounds) - kKBRowHorizontalInset - width; + } + if (y < 0) { y = 0; } + label.frame = CGRectIntegral(CGRectMake(x, y, width, height)); +} + +- (BOOL)kb_isPointInsideBackspaceClearLabel:(CGPoint)point { + if (!self.backspaceClearLabel || self.backspaceClearLabel.hidden) { return NO; } + [self layoutIfNeeded]; + return CGRectContainsPoint(self.backspaceClearLabel.frame, point); +} + +- (void)kb_setBackspaceClearHighlighted:(BOOL)highlighted { + if (self.backspaceClearHighlighted == highlighted) { return; } + self.backspaceClearHighlighted = highlighted; + [self kb_refreshBackspaceClearLabelColors]; +} + +- (void)kb_refreshBackspaceClearLabelColors { + UILabel *label = self.backspaceClearLabel; + label.textColor = [KBSkinManager shared].current.keyTextColor ?: UIColor.blackColor; + label.backgroundColor = self.backspaceClearHighlighted + ? [self kb_backspaceClearLabelHighlightedColor] + : [self kb_backspaceClearLabelNormalColor]; +} + +- (UIColor *)kb_backspaceClearLabelNormalColor { + KBSkinTheme *t = [KBSkinManager shared].current; + return t.keyHighlightBackground ?: [UIColor colorWithWhite:0.9 alpha:1.0]; +} + +- (UIColor *)kb_backspaceClearLabelHighlightedColor { + KBSkinTheme *t = [KBSkinManager shared].current; + return t.accentColor ?: t.keyHighlightBackground ?: [UIColor colorWithWhite:0.8 alpha:1.0]; +} + +- (UILabel *)backspaceClearLabel { + if (!_backspaceClearLabel) { + UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero]; + label.text = @"立刻清空"; + label.textAlignment = NSTextAlignmentCenter; + label.font = [UIFont systemFontOfSize:12 weight:UIFontWeightSemibold]; + label.textColor = [KBSkinManager shared].current.keyTextColor ?: UIColor.blackColor; + label.backgroundColor = [self kb_backspaceClearLabelNormalColor]; + label.layer.cornerRadius = kKBBackspaceClearLabelCornerRadius; + label.layer.masksToBounds = YES; + label.hidden = YES; + label.userInteractionEnabled = NO; + _backspaceClearLabel = label; + } + return _backspaceClearLabel; +} + +- (void)kb_clearAllInput { + UIInputViewController *ivc = KBFindInputViewController(self); + if (!ivc) { return; } + id proxy = ivc.textDocumentProxy; + NSInteger guard = 0; // 上限保护,避免极端情况下长时间阻塞 + while (guard < 10000) { + NSString *before = proxy.documentContextBeforeInput ?: @""; + NSInteger count = before.length; + if (count <= 0) { break; } + for (NSInteger i = 0; i < count; i++) { + [proxy deleteBackward]; + } + guard += count; + } +} + +#pragma mark - Layout + +- (void)layoutSubviews { + [super layoutSubviews]; + if (self.backspaceClearLabel && !self.backspaceClearLabel.hidden) { + [self kb_updateBackspaceClearLabelFrame]; + } +} + #pragma mark - Lazy - (UIView *)row1 { if (!_row1) _row1 = [UIView new]; return _row1; }