新增键盘删除长按删除
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user