美文网首页其它技术点iOS效果原理分析iOS知识点
完美解决UIButton拖动响应事件距离问题

完美解决UIButton拖动响应事件距离问题

作者: 喵子G | 来源:发表于2017-03-22 14:48 被阅读1994次

    1,点击事件和touch事件的关系

    自定义UIButton并在其中重写以下方法:

    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
        BOOL isInside = [super pointInside:point withEvent:event];
        NSLog(@"Button is inside: %zd", isInside);
        return isInside;
    }
    
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
        UIView *view = [super hitTest:point withEvent:event];
        NSLog(@"Button hit: %@", view);
        return view;
    }
    
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"Button touches began");
        [super touchesBegan:touches withEvent:event];
    }
    
    - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"Button touches moved");
        [super touchesMoved:touches withEvent:event];
    }
    
    - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"Button touches ended");
        [super touchesEnded:touches withEvent:event];
    }
    
    - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"Button touches cancelled");
        [super touchesCancelled:touches withEvent:event];
    }
    

    添加UIButton并监听UIControlEventTouchDown和UIControlEventTouchUpInside事件:

    - (void)viewDidLoad {
        [super viewDidLoad];
        JKRButton *button = [JKRButton buttonWithType:UIButtonTypeCustom];
        [button setBackgroundColor:[UIColor blueColor]];
        [button setTitle:@"normal" forState:UIControlStateNormal];
        [button setTitle:@"highlighted" forState:UIControlStateHighlighted];
        button.frame = CGRectMake(100, 100, 100, 40);
    
        [button addTarget:self action:@selector(touchDownAction) forControlEvents:UIControlEventTouchDown];
        [button addTarget:self action:@selector(touchUpInsideAction) forControlEvents:UIControlEventTouchUpInside];
    
        [self.view addSubview:button];
    }
    
    - (void)touchDownAction {
        NSLog(@"Action touch down");
    }
    
    - (void)touchUpInsideAction {
        NSLog(@"Action touch up inside");
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"rootview touchBegan");
    }
    

    点击按钮查看输出:

    Button is inside: 1
    Button hit: <JKRButton: 0x7fcef9508ae0; baseClass = UIButton; frame = (100 100; 100 40); opaque = NO; layer = <CALayer: 0x608000023d80>>
    Button touches began
    Action touch down
    Button touches ended
    Action touch up inside
    

    点击按钮后,首先通过输出可以看到首先通过响应者遍历找到UIButton,触发Button的touches began方法,Button的TouchDown事件触发并调用touchDownAction方法。
    松开按钮后,首先出发UIButton的touches ended方法,Button的TouchUpInside事件触发调用touchUpInsideAction方法。
    Button按钮的点击事件阻断它的父视图的touch方法,所以控制器的touches began方法并没有调用。

    现在测试UIButton的点击和touch事件的关系:

    测试一:重写Button的pointInside方法返回NO,使Button不能响应touch事件:

    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
        BOOL isInside = [super pointInside:point withEvent:event];
        isInside = false;
        NSLog(@"Button is inside: %zd", isInside);
        return isInside;
    }
    

    点击按钮查看输出:

    Button is inside: 0
    Button hit: (null)
    rootview touchBegan
    

    当UIButton不能称为touch事件响应者时,UIButton不能够被点击,并且父视图响应到touch事件。

    结论:UIButton的点击事件是通过touch事件来响应的,并且它并没有向上将事件传递给上一级响应者。

    测试二:注释掉UIButton的touches began的super方法

    点击按钮查看输出:

    Button is inside: 1
    Button hit: <JKRButton: 0x7fb81b408fe0; baseClass = UIButton; frame = (100 100; 100 40); opaque = NO; layer = <CALayer: 0x60800003f840>>
    Button touches began
    Button touches ended
    

    这里看到,按钮不能够被点击

    测试三:注释掉UIButton的touches ended的super方法点击按钮查看输出:

    记得去掉touches began的注释打开super方法,输出如下:

    Button is inside: 1
    Button hit: <JKRButton: 0x7fed2950e140; baseClass = UIButton; frame = (100 100; 100 40); opaque = NO; layer = <CALayer: 0x60000002be60>>
    Button touches began
    Action touch down
    Button touches ended
    

    这里看到,按钮被点击,但是松开按钮后,按钮不能够从高亮状态恢复:

    按钮高亮状态无法恢复
    结论:UIButton的touch down事件和高亮状态的转换取决于touches began方法的处理,touch up inside事件是否触发取决于touch down事件是否触发。
    结论:UIButton的touch up inside事件和从高亮状态恢复到normal状态取决于touches ended方法的处理。(如果高亮状态下,没有走touches ended方法,直接走了touchesCancelled方法,touchesCancelled也会做高亮状态恢复的处理,后面UIButton和手势那里有测试)

    2,UIButton的使用

    传递UIButton的点击事件给上一级响应者。

    上面看到,UIButton会阻断父视图的响应链,这里尝试测试以下代码:

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"Button touches began");
        [super touchesBegan:touches withEvent:event];
        [self.nextResponder touchesBegan:touches withEvent:event];
    }
    
    - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"Button touches moved");
        [super touchesMoved:touches withEvent:event];
        [self.nextResponder touchesMoved:touches withEvent:event];
    }
    
    - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"Button touches ended");
        [super touchesEnded:touches withEvent:event];
        [self.nextResponder touchesEnded:touches withEvent:event];
    }
    
    - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"Button touches cancelled");
        [super touchesCancelled:touches withEvent:event];
        [self.nextResponder touchesCancelled:touches withEvent:event];
    }
    

    重新点击按钮发现,touches began、touches ended方法可以传递给它的父视图,但是touches moved方法只能传递一次,这里原因还不清楚。

    触发UIButton的点击方法

    上面看到,UIButton通过接收到ControlEvent事件来触发点击方法,这里通过给UIButton发送一个事件来触发UIButton的点击方法:

    //触发touchDown事件:
    [self.button sendActionsForControlEvents:UIControlEventTouchDown];
    //触发touchUpInside事件:
    [self.button sendActionsForControlEvents:UIControlEventTouchUpInside];
    

    3,UIButton和手势

    给UIButton添加一个Tap手势:

    JKRTapGestureRecognizer *tap = [[JKRTapGestureRecognizer alloc] initWithTarget:self action:@selector(otherAction)];
    [button addGestureRecognizer:tap];
    
    - (void)otherAction {
        NSLog(@"Tap action");
    }
    

    点击按钮,查看log:

    Button is inside: 1
    Button hit: <JKRButton: 0x7ff598d0bbc0; baseClass = UIButton; frame = (100 100; 100 40); opaque = NO; gestureRecognizers = <NSArray: 0x600000241470>; layer = <CALayer: 0x608000029000>>
    Tap touchBegan
    Button touches began
    Action touch down
    Tap touchEnded
    Tap RecognizerShouldBegin
    Tap action
    Button touches cancelled
    

    这里的输出顺序和UIView添加手势的顺序一样,手势的touch方法先于UIButton的touch一样。这里注意的就是,UIButton的touches began方法调用后,会马上出发UIButton的touch down,所有按钮的touch down事件优先于手势事件处理。在touch ended方法的方法调用中,依然和UIView添加手势的顺序一样,手势的touch ended方法优先执行。这时,识别到手势,触发Tap action方法,然后取消UIButton的touch事件,所以UIButton调用touches cancelled方法。上面说到,按钮的高亮在touches began调用,touches ended恢复,这里由于没有走touches ended。所以可以知道,touches cancelled在没有调用touches ended的情况下,完成了按钮高亮的恢复。

    测试一:修改tap手势的delaysTouchesBegan为YES

    点击按钮输出:

    Button is inside: 1
    Button hit: <JKRButton: 0x7fdd9360aab0; baseClass = UIButton; frame = (100 100; 100 40); opaque = NO; gestureRecognizers = <NSArray: 0x608000058420>; layer = <CALayer: 0x608000029600>>
    Tap touchesBegan
    Tap touchesEnded
    Tap RecognizerShouldBegin
    Tap action
    

    手势成果的阻断了按钮的点击事件。

    点击按钮并滑动输出:

    Button is inside: 1
    Button hit: <JKRButton: 0x7fdd9360aab0; baseClass = UIButton; frame = (100 100; 100 40); opaque = NO; gestureRecognizers = <NSArray: 0x608000058420>; layer = <CALayer: 0x608000029600>>
    Tap touchesBegan
    Tap touchesMoved
    Tap touchesMoved
    Tap touchesMoved
    Button touches began
    Action touch down
    Button touches moved
    Button touches moved
    Button touches ended
    Action touch up inside
    

    手势没有识别,UIButton在tap手势没有识别后,延时执行touch事件,并调用了按钮点击的方法。

    测试二:注释掉UIButton的touches cancelled方法中的super调用

    Button is inside: 1
    Button hit: <JKRButton: 0x7ffa48d0f3d0; baseClass = UIButton; frame = (100 100; 100 40); opaque = NO; gestureRecognizers = <NSArray: 0x6080002408a0>; layer = <CALayer: 0x608000036cc0>>
    Tap touchesBegan
    Button touches began
    Action touch down
    Tap touchesEnded
    Tap RecognizerShouldBegin
    Tap action
    Button touches cancelled
    

    点击按钮后发现,按钮停留在高亮状态无法恢复,验证了之前的想法:按钮的高亮在touches began调用,touches ended恢复,这里由于没有走touches ended。所以可以知道,touches cancelled在没有调用touches ended的情况下,完成了按钮高亮的恢复。

    4,UIButton的事件的详细解析

    UIControlEventTouchDown:按钮点下就调用
    UIControlEventTouchUpInside:在按钮范围内松开手指调用
    UIControlEventTouchUpOutside:在按钮范围外松开手指调用
    UIControlEventTouchCancel:按钮touch事件被取消调用
    UIControlEventTouchDragInside:点击按钮后,在按钮范围内拖动反复调用
    UIControlEventTouchDragOutside:点击按钮后,在按钮范围外拖动反复调用
    UIControlEventTouchDragEnter:点按钮后,拖动到按钮范围外又拖动回按钮返回内跨越边界时调用一次
    UIControlEventTouchDragExit:点击按钮,从按钮范围内拖动到按钮范围外跨越边界时调用一次

    上面所说的按钮范围比实际按钮的尺寸要大,大约是按钮的尺寸加上一70px的边距。

    5,深入理解按钮的事件

    深入理解按钮事件,必须先了解UIButton的继承结构,UIButton继承自UIControl,UIButton的事件(UIControlEvent)和触发(addTarget)也是基于UIControl。UIControl的事件监听和发送基于以下几个方法:

    - (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
    - (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
    - (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event; 
    - (void)cancelTrackingWithEvent:(nullable UIEvent *)event;   
    

    beginTrackingWithTouch:该方法并不是在按钮中开始拖动时调用,而是touchesBegan后马上调用。这个方法的返回值决定了是否要追踪点击事件并进行event事件的处理,如果返回NO,那么UIControl的事件都不会被处理。

    continueTrackingWithTouch:该方法是在touchesMoved调用后调用,决定了按钮拖动后的事件追踪并进行event事件的处理,如果返回为NO,那么拖动之后的所有事件都不会处理,包括除UIControlEventTouchDown之外的所有事件的处理和高亮状态的恢复都不会进行。它会调用的事件:UIControlEventTouchDragInside、UIControlEventTouchDragOutside、UIControlEventTouchDragEnter、UIControlEventTouchDragExit。

    endTrackingWithTouch:该方法在touchesEnded调用后调用,决定了按钮松开后的事件追踪和event事件的处理。它和UIControlEventTouchUpInside、UIControlEventTouchUpOutside相关。

    cancelTrackingWithEvent:该方法在touchesCanceled调用后调用,当按钮的touch事件被取消或者手动调用该方法后调用。该方法会取消UIControl事件的监听,并让按钮从高亮状态恢复到正常状态,并发送UIControlEventTouchCancel事件。如果按钮的touch事件是被主动取消的(例如被其它手势对象识别并取消touch事件),该方法会调用但是不会发送UIControlEventTouchCancel事件。

    按钮中的track相关方法是连续的,如果中途有中断,那么按钮之后的所有点击处理都不能继续执行,例如在点击按钮后拖动过程continueTrackingWithTouch事件中返回NO,那么按钮之后的所有事件和UI处理都不会继续进行,UIControlEventTouchUpInside、UIControlEventTouchUpOutside、UIControlEventTouchDragInside、UIControlEventTouchDragOutside、UIControlEventTouchDragEnter、
    UIControlEventTouchDragExit事件以及高亮状态的恢复都不会执行。重写该方法要记得调用super方法,按钮touch事件取消时的高亮状态恢复是在这里执行的。

    按钮的UI状态、事件的处理,是通过touch事件和UIControl的track相关方法共同完成的,测试中发现并不能对它的事件发送做过多的干涉,否则会造成UI状态和事件处理的中断,所以需要反复调试找到合理的方案。

    6,重定义按钮事件:创建一个手指拖动到按钮外就取消touch响应的按钮

    1,touch事件简单处理

    首先这个操作需要在TouchUpInside事件去处理,点击后松开手指如果手指在按钮范围外就不执行这个事件。但是默认按钮的实际滑动范围的比按钮的尺寸大70的边距。所以这里做的就是重写按钮的touchesMoved方法,监听到拖动的点出了按钮的范围,就直接touchCancelled:

    - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"Button touches moved");
        UITouch *touch = touches.anyObject;
        CGPoint touchPoint = [touch locationInView:self];
        NSLog(@"%@ -- %@", NSStringFromCGRect(self.bounds), NSStringFromCGPoint(touchPoint));
        BOOL cancel = !CGRectContainsPoint(self.bounds, touchPoint);
        if (cancel) {
            [self touchesCancelled:touches withEvent:nil];
        } else {
            [super touchesMoved:touches withEvent:event];
        }
    }
    
    - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        [super touchesCancelled:touches withEvent:event];
    }
    

    现在,点击按钮后,按钮执行UIControlEventTouchDown事件,按钮内拖动,按钮执行UIControlEventTouchDragInside事件,按钮拖动到按钮尺寸范围后,直接调用按钮的touchCancelled方法,并执行按钮的UIControlEventTouchCancel事件。
    只有在按钮点击后,中途没有拖动到按钮外,并在按钮范围内松开,按钮才会响应UIControlEventTouchUpInside事件。
    这样修改后,按钮的UIControlEventTouchUpOutside事件不会触发了,因为滑出按钮范围,直接就走了UIControlEventTouchCancel事件。
    所以该修改只能让按钮点击拖出范围后马上取消事件处理并恢复高亮状态到默认状态,并不能实现重新拖回按钮内再次响应。

    2,UIControl层次的轨迹监听处理

    - (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) { // 之前在里边
                // 从里边滑动到外边
                [self sendActionsForControlEvents:UIControlEventTouchDragExit];
                [self touchesCancelled:[NSSet setWithObject:touch] withEvent:event];
                return NO;
            } else {  // 之前在外边
                // 在按钮外拖动
                [self touchesCancelled:[NSSet setWithObject:touch] withEvent:event];
                return NO;
            }
        } else { // 现在在里边
            if (!isPreInRect) { // 之前在外边
                // 从外边滑动到里边
                [self sendActionsForControlEvents:UIControlEventTouchDragEnter];
                return [super continueTrackingWithTouch:touch withEvent:event];
            } else { // 之前在里边
                // 在按钮内拖动
                return [super continueTrackingWithTouch:touch withEvent:event];
            }
        }
        
        return [super continueTrackingWithTouch:touch withEvent:event];
    }
    

    该方案的效果和上面一样,但是面临几个问题:
    1,该方法返回NO后,UIControl的事件监听也不会进行了,按钮外的滑动不会触发UIControlEventTouchDragOutside事件。
    2,原因同上,按钮从外部滑动会内部,也不会重新恢复高亮状态,UIControlEventTouchDragEnter、UIControlEventTouchDragInside、UIControlEventTouchUpInside事件也不会重新处理。

    3,优化处理第一步,按钮外滑动重新出发UIControlEventTouchDragOutside事件

    既然UIControl的事件无法处理,continueTrackingWithTouch不会再调用,那么我们尝试在touchesMoved方法中手动调用:

    - (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 {  // 之前在外边
                // 在按钮外拖动
                [self sendActionsForControlEvents:UIControlEventTouchDragOutside];
            }
        } else { // 现在在里边
            if (!isPreInRect) { // 之前在外边
                // 从外边滑动到里边
            
            } else { // 之前在里边
                // 在按钮内拖动
            }
        }
        [super touchesMoved:touches withEvent:event];
        
    }
    

    下面发现,按钮在外部滑动,也会触发UIControlEventTouchDragOutside事件了。

    4,优化处理第二部,滑动会按钮重新进入点击状态并触发相应事件。

    上面分析得出,UIControl的事件监听被截断了,而它的开始是从beginTrackingWithTouch方法开始的,尝试在touchesMoved方法中当滑动回按钮范围内的时刻,重新开始UIControl事件的监听,即手动调用beginTrackingWithTouch方法:

    - (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 {  // 之前在外边
                // 在按钮外拖动
                [self sendActionsForControlEvents:UIControlEventTouchDragOutside];
            }
        } else { // 现在在里边
            if (!isPreInRect) { // 之前在外边
                // 从外边滑动到里边
                [self beginTrackingWithTouch:touch withEvent:event];
            } else { // 之前在里边
                // 在按钮内拖动
            }
        }
        [super touchesMoved:touches withEvent:event];
    }
    

    运行测试发现并没有起效果,而之前已经分析出beginTrackingWithTouch方法是在touchesBegan方法之后调用的,可能是缺少了touchesBegan方法中的相应处理,尝试直接调用touchesBegan:

    - (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 {  // 之前在外边
                // 在按钮外拖动
                [self sendActionsForControlEvents:UIControlEventTouchDragOutside];
            }
        } else { // 现在在里边
            if (!isPreInRect) { // 之前在外边
                // 从外边滑动到里边
                //[self beginTrackingWithTouch:touch withEvent:event];
                [self touchesBegan:[NSSet setWithObject:touch] withEvent:event];
            } else { // 之前在里边
                // 在按钮内拖动
            }
        }
        [super touchesMoved:touches withEvent:event];
    }
    

    运行发现,按钮重新滑动回来,响应的事件可以正常触发,并且按钮可以重新恢复成高亮状态!

    5,最后的优化,按钮范围外松开手指的事件触发

    现在按钮已经满足除了按钮范围外松开手指的事件UIControlEventTouchUpOutside的其它所有事件的完美触发。
    之所以不会触发这个事件,是因为上面我们其实在滑动出按钮范围后,就已经截断了UIControl的事件处理,UIControlEventTouchDragOutside的事件是我们在touchesMoved方法中手动触发的。
    现在我们也在touch方法中,手动触发这个事件。因为我们之前已经分析出:UIControlEventTouchUpInside、UIControlEventTouchUpOutside都是在touchesEnded方法后触发的,所重写这个方法,当松开手指后的点在按钮范围外,就手动发送这个事件:

    - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"touchesEnded");
        UITouch *touch = touches.anyObject;
        CGPoint point = [touch locationInView:self];
        if (!CGRectContainsPoint(self.bounds, point)) {
            [self sendActionsForControlEvents:UIControlEventTouchUpOutside];
        }
        [super touchesEnded:touches withEvent:event];
    }
    
    完整代码如下:
    /// 修改按钮滑动范围
    - (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];
    }
    

    运行效果:

    buttonEvent.gif

    6,仍然存在的问题

    最后唯一存在的问题就是从按钮范围内拖动出按钮范围外的时候,因为手动调用了touchesCancelled方法,导致按钮多余的发送了一次UIControlEventTouchCancel事件。

    Demo:https://github.com/Joker-388/JKRButtonWithDragCancel

    获取授权

    相关文章

      网友评论

      • 观星:触摸事件分发机制遇到按钮就有点问题了,给按钮的superview加一个点击手势,点击按钮时,这个tap收到了touch竟然不能被触发:sweat:
        d4efe0349e4b:@喵子G 按钮应该是能被触发吧 tap不会触发
        喵子G:@观星 是按钮不能触发吗?

      本文标题:完美解决UIButton拖动响应事件距离问题

      本文链接:https://www.haomeiwen.com/subject/nptgnttx.html