新增键盘删除长按删除
This commit is contained in:
@@ -26,6 +26,11 @@ static const CGFloat kKBBackspaceClearLabelCornerRadius = 8.0;
|
|||||||
static const CGFloat kKBBackspaceClearLabelHeight = 26.0;
|
static const CGFloat kKBBackspaceClearLabelHeight = 26.0;
|
||||||
static const CGFloat kKBBackspaceClearLabelPaddingX = 10.0;
|
static const CGFloat kKBBackspaceClearLabelPaddingX = 10.0;
|
||||||
static const CGFloat kKBBackspaceClearLabelTopGap = 6.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 kKBPreviewShowDuration = 0.08;
|
||||||
static const NSTimeInterval kKBPreviewHideDuration = 0.06;
|
static const NSTimeInterval kKBPreviewHideDuration = 0.06;
|
||||||
@@ -56,6 +61,10 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
@property (nonatomic, assign) NSTimeInterval backspaceHoldStartTime;
|
@property (nonatomic, assign) NSTimeInterval backspaceHoldStartTime;
|
||||||
@property (nonatomic, assign) BOOL backspaceChunkModeActive;
|
@property (nonatomic, assign) BOOL backspaceChunkModeActive;
|
||||||
@property (nonatomic, assign) BOOL backspaceClearHighlighted;
|
@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, weak) KBKeyButton *backspaceButton;
|
||||||
@property (nonatomic, strong) UILabel *backspaceClearLabel;
|
@property (nonatomic, strong) UILabel *backspaceClearLabel;
|
||||||
@property (nonatomic, strong) KBKeyPreviewView *previewView;
|
@property (nonatomic, strong) KBKeyPreviewView *previewView;
|
||||||
@@ -696,14 +705,22 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier {
|
|||||||
|
|
||||||
// 长按退格:先连续单删,稍后切换为按段删除;松手停止。(不使用 NSTimer/DisplayLink)
|
// 长按退格:先连续单删,稍后切换为按段删除;松手停止。(不使用 NSTimer/DisplayLink)
|
||||||
- (void)onBackspaceLongPress:(UILongPressGestureRecognizer *)gr {
|
- (void)onBackspaceLongPress:(UILongPressGestureRecognizer *)gr {
|
||||||
|
if (gr) {
|
||||||
|
self.backspaceLastTouchPointInSelf = [gr locationInView:self];
|
||||||
|
self.backspaceHasLastTouchPoint = YES;
|
||||||
|
}
|
||||||
switch (gr.state) {
|
switch (gr.state) {
|
||||||
case UIGestureRecognizerStateBegan: {
|
case UIGestureRecognizerStateBegan: {
|
||||||
|
// 递增 token:使上一次长按(可能尚未触发的 dispatch_after)立即失效,
|
||||||
|
// 避免“第一次长按后第二次长按失效/异常加速”等并发问题。
|
||||||
|
self.backspaceHoldToken += 1;
|
||||||
|
NSUInteger token = self.backspaceHoldToken;
|
||||||
self.backspaceHoldActive = YES;
|
self.backspaceHoldActive = YES;
|
||||||
self.backspaceHoldStartTime = [NSDate date].timeIntervalSinceReferenceDate;
|
self.backspaceHoldStartTime = [NSDate date].timeIntervalSinceReferenceDate;
|
||||||
self.backspaceChunkModeActive = NO;
|
self.backspaceChunkModeActive = NO;
|
||||||
[self kb_setBackspaceClearHighlighted:NO];
|
[self kb_setBackspaceClearHighlighted:NO];
|
||||||
[self kb_hideBackspaceClearLabel];
|
[self kb_hideBackspaceClearLabel];
|
||||||
[self kb_backspaceStep];
|
[self kb_backspaceStepForToken:token];
|
||||||
} break;
|
} break;
|
||||||
case UIGestureRecognizerStateChanged: {
|
case UIGestureRecognizerStateChanged: {
|
||||||
[self kb_handleBackspaceLongPressChanged:gr];
|
[self kb_handleBackspaceLongPressChanged:gr];
|
||||||
@@ -720,15 +737,18 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier {
|
|||||||
#pragma mark - Helpers
|
#pragma mark - Helpers
|
||||||
|
|
||||||
// 单步删除并在需要时安排下一次,直到松手或无内容
|
// 单步删除并在需要时安排下一次,直到松手或无内容
|
||||||
- (void)kb_backspaceStep {
|
- (void)kb_backspaceStepForToken:(NSUInteger)token {
|
||||||
if (!self.backspaceHoldActive) { return; }
|
if (!self.backspaceHoldActive) { return; }
|
||||||
|
if (token != self.backspaceHoldToken) { return; }
|
||||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||||
if (!ivc) { self.backspaceHoldActive = NO; return; }
|
if (!ivc) { self.backspaceHoldActive = NO; return; }
|
||||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
||||||
if (before.length <= 0) { self.backspaceHoldActive = NO; return; }
|
|
||||||
NSTimeInterval elapsed = [NSDate date].timeIntervalSinceReferenceDate - self.backspaceHoldStartTime;
|
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) {
|
if (!self.backspaceChunkModeActive && elapsed >= kKBBackspaceChunkStartDelay) {
|
||||||
self.backspaceChunkModeActive = YES;
|
self.backspaceChunkModeActive = YES;
|
||||||
[self kb_showBackspaceClearLabelIfNeeded];
|
[self kb_showBackspaceClearLabelIfNeeded];
|
||||||
@@ -743,7 +763,7 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier {
|
|||||||
(int64_t)(interval * NSEC_PER_SEC)),
|
(int64_t)(interval * NSEC_PER_SEC)),
|
||||||
dispatch_get_main_queue(), ^{
|
dispatch_get_main_queue(), ^{
|
||||||
__strong typeof(weakSelf) selfStrong = weakSelf;
|
__strong typeof(weakSelf) selfStrong = weakSelf;
|
||||||
[selfStrong kb_backspaceStep];
|
[selfStrong kb_backspaceStepForToken:token];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -808,24 +828,86 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier {
|
|||||||
return MAX(deleteCount, 1);
|
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 {
|
- (void)kb_handleBackspaceLongPressChanged:(UILongPressGestureRecognizer *)gr {
|
||||||
if (!self.backspaceHoldActive) { return; }
|
if (!self.backspaceHoldActive) { return; }
|
||||||
NSTimeInterval elapsed = [NSDate date].timeIntervalSinceReferenceDate - self.backspaceHoldStartTime;
|
NSTimeInterval elapsed = [NSDate date].timeIntervalSinceReferenceDate - self.backspaceHoldStartTime;
|
||||||
if (elapsed < kKBBackspaceChunkStartDelay) { return; }
|
if (elapsed < kKBBackspaceChunkStartDelay) { return; }
|
||||||
[self kb_showBackspaceClearLabelIfNeeded];
|
[self kb_showBackspaceClearLabelIfNeeded];
|
||||||
CGPoint point = [gr locationInView:self];
|
CGPoint point = [gr locationInView:self];
|
||||||
|
self.backspaceLastTouchPointInSelf = point;
|
||||||
|
self.backspaceHasLastTouchPoint = YES;
|
||||||
BOOL inside = [self kb_isPointInsideBackspaceClearLabel:point];
|
BOOL inside = [self kb_isPointInsideBackspaceClearLabel:point];
|
||||||
[self kb_setBackspaceClearHighlighted:inside];
|
[self kb_setBackspaceClearHighlighted:inside];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)kb_handleBackspaceLongPressEnded:(UILongPressGestureRecognizer *)gr {
|
- (void)kb_handleBackspaceLongPressEnded:(UILongPressGestureRecognizer *)gr {
|
||||||
BOOL shouldClear = self.backspaceClearHighlighted;
|
BOOL shouldClear = self.backspaceClearHighlighted;
|
||||||
if (!shouldClear && gr) {
|
if (!shouldClear) {
|
||||||
CGPoint point = [gr locationInView:self];
|
CGPoint point = CGPointZero;
|
||||||
|
if (gr) {
|
||||||
|
point = [gr locationInView:self];
|
||||||
|
} else if (self.backspaceHasLastTouchPoint) {
|
||||||
|
point = self.backspaceLastTouchPointInSelf;
|
||||||
|
}
|
||||||
shouldClear = [self kb_isPointInsideBackspaceClearLabel:point];
|
shouldClear = [self kb_isPointInsideBackspaceClearLabel:point];
|
||||||
}
|
}
|
||||||
self.backspaceHoldActive = NO;
|
self.backspaceHoldActive = NO;
|
||||||
self.backspaceChunkModeActive = NO;
|
self.backspaceChunkModeActive = NO;
|
||||||
|
self.backspaceHoldToken += 1; // 结束/取消时也使剩余回调失效
|
||||||
|
self.backspaceHasLastTouchPoint = NO;
|
||||||
[self kb_hideBackspaceClearLabel];
|
[self kb_hideBackspaceClearLabel];
|
||||||
if (shouldClear) {
|
if (shouldClear) {
|
||||||
[self kb_clearAllInput];
|
[self kb_clearAllInput];
|
||||||
@@ -876,8 +958,9 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier {
|
|||||||
|
|
||||||
- (BOOL)kb_isPointInsideBackspaceClearLabel:(CGPoint)point {
|
- (BOOL)kb_isPointInsideBackspaceClearLabel:(CGPoint)point {
|
||||||
if (!self.backspaceClearLabel || self.backspaceClearLabel.hidden) { return NO; }
|
if (!self.backspaceClearLabel || self.backspaceClearLabel.hidden) { return NO; }
|
||||||
[self layoutIfNeeded];
|
[self kb_updateBackspaceClearLabelFrame];
|
||||||
return CGRectContainsPoint(self.backspaceClearLabel.frame, point);
|
CGRect hitFrame = CGRectInset(self.backspaceClearLabel.frame, -12.0, -10.0);
|
||||||
|
return CGRectContainsPoint(hitFrame, point);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)kb_setBackspaceClearHighlighted:(BOOL)highlighted {
|
- (void)kb_setBackspaceClearHighlighted:(BOOL)highlighted {
|
||||||
@@ -922,19 +1005,61 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)kb_clearAllInput {
|
- (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);
|
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||||
if (!ivc) { return; }
|
if (!ivc) { return; }
|
||||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
NSInteger guard = 0; // 上限保护,避免极端情况下长时间阻塞
|
|
||||||
while (guard < 10000) {
|
|
||||||
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
||||||
NSInteger count = before.length;
|
NSInteger count = before.length;
|
||||||
if (count <= 0) { break; }
|
NSInteger batch = 0;
|
||||||
for (NSInteger i = 0; i < count; i++) {
|
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];
|
[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
|
#pragma mark - Layout
|
||||||
|
|||||||
Reference in New Issue
Block a user