UIButton点击事件Drag实际范围的偏差处理

1,276 阅读3分钟

相信做过这个需求的都会被这个问题困扰过,具体问题如下;

当点击一个按钮后不松开,继续滑动手指的话,如果手指滑动出按钮的范围,按钮应该从高亮状态恢复到正常状态,并且在按钮范围之外松开手指,按钮应该对应的ControlEvents事件应该是:UIControlEventTouchUpOutside。

但是实际上UIButton并不是这样的,它在这种情况下,会有一个按钮范围之外的外围,在这个范围内拖动,让然会当左在按钮范围内处理,在这个范围内松开手指,收到的事件是:UIControlEventTouchUpInside。

我们需要的场景应该是这样的:

当然可以自定义一个控件来实现,但是如果要做到跟UIButton一样,支持各种事件的监听、控件对应的UI状态变化等是一个特别大工程,而且最后还不一定比UIButton好用。UIButton已经提供一套很完善的按钮功能,这里优先的方案肯定是解决掉UIButton的这个任性的小毛病。

网上也搜索过很多,不过基本都没有完美的,要么就是解决滑出的监听,但解决不了再次滑入的恢复,要么就是会丢失调用一个Event事件的回调。

具体调试和实验的过程非常繁琐,而且都是API的调用,并没有太多值得说的地方,直接就列出来代码了,拷贝到自己的继承自UIButton的子类就可以了:

/// 修改按钮滑动范围
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesMoved");
    UITouch *touch = touches.anyObject;
    BOOL isCurInRect = CGRectContainsPoint(self.bounds, [touch locationInView:self]);
    BOOL isPreInRect = CGRectContainsPoint(self.bounds, [touch previousLocationInView:self]);
    if (!isCurInRect) { // 现在在外面
        if (isPreInRect) { // 之前在里边
            // 从里边滑动到外边
            
        } else {  // 之前在外边
            // 在按钮外拖动
            // 在按钮范围外拖动手动发送UIControlEventTouchDragOutside事件
            [self sendActionsForControlEvents:UIControlEventTouchDragOutside];
        }
    } else { // 现在在里边
        if (!isPreInRect) { // 之前在外边
            // 从外边滑动到里边
            // 从按钮范围外滑动回按钮范围内,需要手动调用touchesBegan方法,让按钮进入高亮状态,并开启UIControl的事件监听
            //[self beginTrackingWithTouch:touch withEvent:event];
            [self touchesBegan:[NSSet setWithObject:touch] withEvent:event];
        } else { // 之前在里边
            // 在按钮内拖动
        }
    }
    [super touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesEnded");
    UITouch *touch = touches.anyObject;
    CGPoint point = [touch locationInView:self];
    // 如果松开手指后在按钮范围之外
    if (!CGRectContainsPoint(self.bounds, point)) {
        // 手动触发UIControlEventTouchUpOutside事件
        [self sendActionsForControlEvents:UIControlEventTouchUpOutside];
    }
    [super touchesEnded:touches withEvent:event];
}

- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    NSLog(@"continueTrackingWithTouch");
    BOOL isCurInRect = CGRectContainsPoint(self.bounds, [touch locationInView:self]);
    BOOL isPreInRect = CGRectContainsPoint(self.bounds, [touch previousLocationInView:self]);
    if (!isCurInRect) { // 现在在外面
        if (isPreInRect) { // 之前在里边
            // 从里边滑动到外边
            // 从按钮范围内滑动到按钮范围外手动触发UIControlEventTouchDragExit事件并阻断按钮默认事件的执行
            [self sendActionsForControlEvents:UIControlEventTouchDragExit];
            // 阻断按钮默认事件的事件的执行后,需要手动触发touchesCancelled方法,让按钮从高亮状态变成默认状态
            [self touchesCancelled:[NSSet setWithObject:touch] withEvent:event];
            return NO;
        } else {  // 之前在外边
            // 在按钮外拖动
            // 在按钮范围外滑动时,需要手动触发touchesCancelled方法,让按钮从高亮状态变成默认状态,并阻断按钮默认事件的执行
            [self touchesCancelled:[NSSet setWithObject:touch] withEvent:event];
            return NO;
        }
    } else { // 现在在里边
        if (!isPreInRect) { // 之前在外边
            // 从外边滑动到里边
            // 从按钮范围外滑动到按钮范围内,需要手动触发UIControlEventTouchDragEnter事件
            [self sendActionsForControlEvents:UIControlEventTouchDragEnter];
            return [super continueTrackingWithTouch:touch withEvent:event];
        } else { // 之前在里边
            // 在按钮内拖动
            return [super continueTrackingWithTouch:touch withEvent:event];
        }
    }
    return [super continueTrackingWithTouch:touch withEvent:event];
}

现在,以下的ControlEvents全部能够完美的得到正确的回调:

[button addTarget:self action:@selector(touchDownAction) forControlEvents:UIControlEventTouchDown];
[button addTarget:self action:@selector(touchUpInsideAction) forControlEvents:UIControlEventTouchUpInside];
[button addTarget:self action:@selector(touchUpOutsideAction) forControlEvents:UIControlEventTouchUpOutside];
[button addTarget:self action:@selector(touchCancelAction) forControlEvents:UIControlEventTouchCancel];
[button addTarget:self action:@selector(touchDragInsideAction) forControlEvents:UIControlEventTouchDragInside];
[button addTarget:self action:@selector(touchDragOutsideAction) forControlEvents:UIControlEventTouchDragOutside];
[button addTarget:self action:@selector(touchDragEnterAction) forControlEvents:UIControlEventTouchDragEnter];
[button addTarget:self action:@selector(touchDragExitAction) forControlEvents:UIControlEventTouchDragExit];