新增键盘删除长按删除

This commit is contained in:
2025-12-19 18:08:51 +08:00
parent 639ce7eafd
commit e0d5ae0257

View File

@@ -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<UITextDocumentProxy> 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<UITextDocumentProxy> 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++) {
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];
}
guard += count;
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