解決UIButton拖動響應事件距離問題

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

獲取授權

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容