From e0d5ae0257106c9fbadb2120485815fff9a51134 Mon Sep 17 00:00:00 2001 From: CodeST <694468528@qq.com> Date: Fri, 19 Dec 2025 18:08:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=94=AE=E7=9B=98=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E9=95=BF=E6=8C=89=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CustomKeyboard/View/KBKeyboardView.m | 161 ++++++++++++++++++++++++--- 1 file changed, 143 insertions(+), 18 deletions(-) diff --git a/CustomKeyboard/View/KBKeyboardView.m b/CustomKeyboard/View/KBKeyboardView.m index 63bcd48..f0d0ee2 100644 --- a/CustomKeyboard/View/KBKeyboardView.m +++ b/CustomKeyboard/View/KBKeyboardView.m @@ -26,6 +26,11 @@ 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 NSInteger kKBBackspaceClearBatchSize = 24; +static const NSTimeInterval kKBBackspaceClearBatchInterval = 0.005; +static const NSInteger kKBBackspaceClearMaxDeletes = 10000; +static const NSInteger kKBBackspaceClearEmptyContextMaxRounds = 40; +static const NSInteger kKBBackspaceClearMaxStep = 80; static const NSTimeInterval kKBPreviewShowDuration = 0.08; static const NSTimeInterval kKBPreviewHideDuration = 0.06; @@ -56,6 +61,10 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { @property (nonatomic, assign) NSTimeInterval backspaceHoldStartTime; @property (nonatomic, assign) BOOL backspaceChunkModeActive; @property (nonatomic, assign) BOOL backspaceClearHighlighted; +@property (nonatomic, assign) NSUInteger backspaceHoldToken; +@property (nonatomic, assign) BOOL backspaceHasLastTouchPoint; +@property (nonatomic, assign) CGPoint backspaceLastTouchPointInSelf; +@property (nonatomic, assign) NSUInteger backspaceClearToken; @property (nonatomic, weak) KBKeyButton *backspaceButton; @property (nonatomic, strong) UILabel *backspaceClearLabel; @property (nonatomic, strong) KBKeyPreviewView *previewView; @@ -696,14 +705,22 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier { // 长按退格:先连续单删,稍后切换为按段删除;松手停止。(不使用 NSTimer/DisplayLink) - (void)onBackspaceLongPress:(UILongPressGestureRecognizer *)gr { + if (gr) { + self.backspaceLastTouchPointInSelf = [gr locationInView:self]; + self.backspaceHasLastTouchPoint = YES; + } switch (gr.state) { case UIGestureRecognizerStateBegan: { + // 递增 token:使上一次长按(可能尚未触发的 dispatch_after)立即失效, + // 避免“第一次长按后第二次长按失效/异常加速”等并发问题。 + self.backspaceHoldToken += 1; + NSUInteger token = self.backspaceHoldToken; self.backspaceHoldActive = YES; self.backspaceHoldStartTime = [NSDate date].timeIntervalSinceReferenceDate; self.backspaceChunkModeActive = NO; [self kb_setBackspaceClearHighlighted:NO]; [self kb_hideBackspaceClearLabel]; - [self kb_backspaceStep]; + [self kb_backspaceStepForToken:token]; } break; case UIGestureRecognizerStateChanged: { [self kb_handleBackspaceLongPressChanged:gr]; @@ -720,15 +737,18 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier { #pragma mark - Helpers // 单步删除并在需要时安排下一次,直到松手或无内容 -- (void)kb_backspaceStep { +- (void)kb_backspaceStepForToken:(NSUInteger)token { if (!self.backspaceHoldActive) { return; } + if (token != self.backspaceHoldToken) { return; } UIInputViewController *ivc = KBFindInputViewController(self); if (!ivc) { self.backspaceHoldActive = NO; return; } id proxy = ivc.textDocumentProxy; NSString *before = proxy.documentContextBeforeInput ?: @""; - if (before.length <= 0) { self.backspaceHoldActive = NO; return; } NSTimeInterval elapsed = [NSDate date].timeIntervalSinceReferenceDate - self.backspaceHoldStartTime; - NSInteger deleteCount = [self kb_backspaceDeleteCountForContext:before elapsed:elapsed]; + NSInteger deleteCount = 1; + if (before.length > 0) { + deleteCount = [self kb_backspaceDeleteCountForContext:before elapsed:elapsed]; + } if (!self.backspaceChunkModeActive && elapsed >= kKBBackspaceChunkStartDelay) { self.backspaceChunkModeActive = YES; [self kb_showBackspaceClearLabelIfNeeded]; @@ -743,7 +763,7 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier { (int64_t)(interval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ __strong typeof(weakSelf) selfStrong = weakSelf; - [selfStrong kb_backspaceStep]; + [selfStrong kb_backspaceStepForToken:token]; }); } @@ -808,24 +828,86 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier { return MAX(deleteCount, 1); } +- (NSInteger)kb_clearDeleteCountForContext:(NSString *)context + hitBoundary:(BOOL *)hitBoundary { + if (context.length == 0) { return kKBBackspaceClearBatchSize; } + + static NSCharacterSet *sentenceBoundarySet = nil; + static NSCharacterSet *whitespaceSet = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;。!?;…\n"]; + whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet]; + }); + + NSInteger length = context.length; + NSInteger end = length; + while (end > 0) { + unichar ch = [context characterAtIndex:end - 1]; + if ([whitespaceSet characterIsMember:ch]) { + end -= 1; + } else { + break; + } + } + NSInteger searchEnd = end; + while (searchEnd > 0) { + unichar ch = [context characterAtIndex:searchEnd - 1]; + if ([sentenceBoundarySet characterIsMember:ch]) { + searchEnd -= 1; + } else { + break; + } + } + + NSInteger boundaryIndex = NSNotFound; + for (NSInteger i = searchEnd - 1; i >= 0; i--) { + unichar ch = [context characterAtIndex:i]; + if ([sentenceBoundarySet characterIsMember:ch]) { + boundaryIndex = i; + break; + } + } + + BOOL boundaryFound = (boundaryIndex != NSNotFound); + NSInteger deleteCount = length; + if (boundaryIndex != NSNotFound) { + deleteCount = length - (boundaryIndex + 1); + } + deleteCount = MAX(deleteCount, 1); + if (hitBoundary) { + *hitBoundary = boundaryFound; + } + return MIN(deleteCount, kKBBackspaceClearMaxStep); +} + - (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]; + self.backspaceLastTouchPointInSelf = point; + self.backspaceHasLastTouchPoint = YES; 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]; + if (!shouldClear) { + CGPoint point = CGPointZero; + if (gr) { + point = [gr locationInView:self]; + } else if (self.backspaceHasLastTouchPoint) { + point = self.backspaceLastTouchPointInSelf; + } shouldClear = [self kb_isPointInsideBackspaceClearLabel:point]; } self.backspaceHoldActive = NO; self.backspaceChunkModeActive = NO; + self.backspaceHoldToken += 1; // 结束/取消时也使剩余回调失效 + self.backspaceHasLastTouchPoint = NO; [self kb_hideBackspaceClearLabel]; if (shouldClear) { [self kb_clearAllInput]; @@ -876,8 +958,9 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier { - (BOOL)kb_isPointInsideBackspaceClearLabel:(CGPoint)point { if (!self.backspaceClearLabel || self.backspaceClearLabel.hidden) { return NO; } - [self layoutIfNeeded]; - return CGRectContainsPoint(self.backspaceClearLabel.frame, point); + [self kb_updateBackspaceClearLabelFrame]; + CGRect hitFrame = CGRectInset(self.backspaceClearLabel.frame, -12.0, -10.0); + return CGRectContainsPoint(hitFrame, point); } - (void)kb_setBackspaceClearHighlighted:(BOOL)highlighted { @@ -922,19 +1005,61 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier { } - (void)kb_clearAllInput { + self.backspaceClearToken += 1; + NSUInteger token = self.backspaceClearToken; + [self kb_clearAllInputStepForToken:token guard:0 emptyRounds:0]; +} + +- (void)kb_clearAllInputStepForToken:(NSUInteger)token + guard:(NSInteger)guard + emptyRounds:(NSInteger)emptyRounds { + if (token != self.backspaceClearToken) { return; } 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; + NSString *before = proxy.documentContextBeforeInput ?: @""; + NSInteger count = before.length; + NSInteger batch = 0; + NSInteger nextEmptyRounds = emptyRounds; + BOOL hitBoundary = NO; + if (count > 0) { + batch = [self kb_clearDeleteCountForContext:before hitBoundary:&hitBoundary]; + nextEmptyRounds = 0; + } else { + batch = kKBBackspaceClearBatchSize; + nextEmptyRounds = emptyRounds + 1; } + if (batch <= 0) { batch = 1; } + + if (guard >= kKBBackspaceClearMaxDeletes || + nextEmptyRounds > kKBBackspaceClearEmptyContextMaxRounds) { + return; + } + + for (NSInteger i = 0; i < batch; i++) { + [proxy deleteBackward]; + } + + NSInteger nextGuard = guard + batch; + BOOL shouldContinue = NO; + if (count > 0 && !hitBoundary) { + if (count > batch) { + shouldContinue = YES; + } else if ([proxy hasText]) { + shouldContinue = YES; + } + } + + if (!shouldContinue) { return; } + __weak typeof(self) weakSelf = self; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(kKBBackspaceClearBatchInterval * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + __strong typeof(weakSelf) selfStrong = weakSelf; + [selfStrong kb_clearAllInputStepForToken:token + guard:nextGuard + emptyRounds:nextEmptyRounds]; + }); } #pragma mark - Layout