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

1,點擊事件和touch事件的關(guān)系

自定義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并監(jiān)聽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,觸發(fā)Button的touches began方法,Button的TouchDown事件觸發(fā)并調(diào)用touchDownAction方法。
松開按鈕后,首先出發(fā)UIButton的touches ended方法低散,Button的TouchUpInside事件觸發(fā)調(diào)用touchUpInsideAction方法俯邓。
Button按鈕的點擊事件阻斷它的父視圖的touch方法,所以控制器的touches began方法并沒有調(diào)用熔号。

現(xiàn)在測試UIButton的點擊和touch事件的關(guān)系:

測試一:重寫B(tài)utton的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事件朦蕴。

結(jié)論: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

這里看到赴恨,按鈕被點擊疹娶,但是松開按鈕后,按鈕不能夠從高亮狀態(tài)恢復:

按鈕高亮狀態(tài)無法恢復
結(jié)論:UIButton的touch down事件和高亮狀態(tài)的轉(zhuǎn)換取決于touches began方法的處理伦连,touch up inside事件是否觸發(fā)取決于touch down事件是否觸發(fā)雨饺。
結(jié)論:UIButton的touch up inside事件和從高亮狀態(tài)恢復到normal狀態(tài)取決于touches ended方法的處理。(如果高亮狀態(tài)下除师,沒有走touches ended方法沛膳,直接走了touchesCancelled方法扔枫,touchesCancelled也會做高亮狀態(tài)恢復的處理汛聚,后面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];
}

重新點擊按鈕發(fā)現(xiàn)忍宋,touches began痕貌、touches ended方法可以傳遞給它的父視圖,但是touches moved方法只能傳遞一次糠排,這里原因還不清楚舵稠。

觸發(fā)UIButton的點擊方法

上面看到,UIButton通過接收到ControlEvent事件來觸發(fā)點擊方法入宦,這里通過給UIButton發(fā)送一個事件來觸發(fā)UIButton的點擊方法:

//觸發(fā)touchDown事件:
[self.button sendActionsForControlEvents:UIControlEventTouchDown];
//觸發(fā)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方法調(diào)用后涯肩,會馬上出發(fā)UIButton的touch down轿钠,所有按鈕的touch down事件優(yōu)先于手勢事件處理巢钓。在touch ended方法的方法調(diào)用中,依然和UIView添加手勢的順序一樣疗垛,手勢的touch ended方法優(yōu)先執(zhí)行症汹。這時,識別到手勢贷腕,觸發(fā)Tap action方法烈菌,然后取消UIButton的touch事件,所以UIButton調(diào)用touches cancelled方法花履。上面說到芽世,按鈕的高亮在touches began調(diào)用,touches ended恢復诡壁,這里由于沒有走touches ended济瓢。所以可以知道,touches cancelled在沒有調(diào)用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手勢沒有識別后箕宙,延時執(zhí)行touch事件,并調(diào)用了按鈕點擊的方法铺纽。

測試二:注釋掉UIButton的touches cancelled方法中的super調(diào)用

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

點擊按鈕后發(fā)現(xiàn)柬帕,按鈕停留在高亮狀態(tài)無法恢復,驗證了之前的想法:按鈕的高亮在touches began調(diào)用狡门,touches ended恢復陷寝,這里由于沒有走touches ended。所以可以知道其馏,touches cancelled在沒有調(diào)用touches ended的情況下凤跑,完成了按鈕高亮的恢復。

4叛复,UIButton的事件的詳細解析

UIControlEventTouchDown:按鈕點下就調(diào)用
UIControlEventTouchUpInside:在按鈕范圍內(nèi)松開手指調(diào)用
UIControlEventTouchUpOutside:在按鈕范圍外松開手指調(diào)用
UIControlEventTouchCancel:按鈕touch事件被取消調(diào)用
UIControlEventTouchDragInside:點擊按鈕后仔引,在按鈕范圍內(nèi)拖動反復調(diào)用
UIControlEventTouchDragOutside:點擊按鈕后,在按鈕范圍外拖動反復調(diào)用
UIControlEventTouchDragEnter:點按鈕后褐奥,拖動到按鈕范圍外又拖動回按鈕返回內(nèi)跨越邊界時調(diào)用一次
UIControlEventTouchDragExit:點擊按鈕咖耘,從按鈕范圍內(nèi)拖動到按鈕范圍外跨越邊界時調(diào)用一次

上面所說的按鈕范圍比實際按鈕的尺寸要大,大約是按鈕的尺寸加上一70px的邊距抖僵。

5鲤看,深入理解按鈕的事件

深入理解按鈕事件,必須先了解UIButton的繼承結(jié)構(gòu)耍群,UIButton繼承自UIControl义桂,UIButton的事件(UIControlEvent)和觸發(fā)(addTarget)也是基于UIControl找筝。UIControl的事件監(jiān)聽和發(fā)送基于以下幾個方法:

- (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:該方法并不是在按鈕中開始拖動時調(diào)用,而是touchesBegan后馬上調(diào)用慷吊。這個方法的返回值決定了是否要追蹤點擊事件并進行event事件的處理袖裕,如果返回NO,那么UIControl的事件都不會被處理溉瓶。

continueTrackingWithTouch:該方法是在touchesMoved調(diào)用后調(diào)用急鳄,決定了按鈕拖動后的事件追蹤并進行event事件的處理,如果返回為NO堰酿,那么拖動之后的所有事件都不會處理疾宏,包括除UIControlEventTouchDown之外的所有事件的處理和高亮狀態(tài)的恢復都不會進行。它會調(diào)用的事件:UIControlEventTouchDragInside触创、UIControlEventTouchDragOutside坎藐、UIControlEventTouchDragEnter、UIControlEventTouchDragExit哼绑。

endTrackingWithTouch:該方法在touchesEnded調(diào)用后調(diào)用岩馍,決定了按鈕松開后的事件追蹤和event事件的處理。它和UIControlEventTouchUpInside抖韩、UIControlEventTouchUpOutside相關(guān)蛀恩。

cancelTrackingWithEvent:該方法在touchesCanceled調(diào)用后調(diào)用,當按鈕的touch事件被取消或者手動調(diào)用該方法后調(diào)用茂浮。該方法會取消UIControl事件的監(jiān)聽双谆,并讓按鈕從高亮狀態(tài)恢復到正常狀態(tài),并發(fā)送UIControlEventTouchCancel事件励稳。如果按鈕的touch事件是被主動取消的(例如被其它手勢對象識別并取消touch事件)佃乘,該方法會調(diào)用但是不會發(fā)送UIControlEventTouchCancel事件囱井。

按鈕中的track相關(guān)方法是連續(xù)的驹尼,如果中途有中斷,那么按鈕之后的所有點擊處理都不能繼續(xù)執(zhí)行庞呕,例如在點擊按鈕后拖動過程continueTrackingWithTouch事件中返回NO新翎,那么按鈕之后的所有事件和UI處理都不會繼續(xù)進行,UIControlEventTouchUpInside住练、UIControlEventTouchUpOutside地啰、UIControlEventTouchDragInside、UIControlEventTouchDragOutside讲逛、UIControlEventTouchDragEnter亏吝、
UIControlEventTouchDragExit事件以及高亮狀態(tài)的恢復都不會執(zhí)行。重寫該方法要記得調(diào)用super方法盏混,按鈕touch事件取消時的高亮狀態(tài)恢復是在這里執(zhí)行的蔚鸥。

按鈕的UI狀態(tài)惜论、事件的處理,是通過touch事件和UIControl的track相關(guān)方法共同完成的止喷,測試中發(fā)現(xiàn)并不能對它的事件發(fā)送做過多的干涉馆类,否則會造成UI狀態(tài)和事件處理的中斷,所以需要反復調(diào)試找到合理的方案弹谁。

6乾巧,重定義按鈕事件:創(chuàng)建一個手指拖動到按鈕外就取消touch響應的按鈕

1,touch事件簡單處理

首先這個操作需要在TouchUpInside事件去處理预愤,點擊后松開手指如果手指在按鈕范圍外就不執(zhí)行這個事件沟于。但是默認按鈕的實際滑動范圍的比按鈕的尺寸大70的邊距。所以這里做的就是重寫按鈕的touchesMoved方法植康,監(jiān)聽到拖動的點出了按鈕的范圍社裆,就直接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];
}

現(xiàn)在,點擊按鈕后向图,按鈕執(zhí)行UIControlEventTouchDown事件泳秀,按鈕內(nèi)拖動,按鈕執(zhí)行UIControlEventTouchDragInside事件榄攀,按鈕拖動到按鈕尺寸范圍后嗜傅,直接調(diào)用按鈕的touchCancelled方法,并執(zhí)行按鈕的UIControlEventTouchCancel事件檩赢。
只有在按鈕點擊后吕嘀,中途沒有拖動到按鈕外,并在按鈕范圍內(nèi)松開贞瞒,按鈕才會響應UIControlEventTouchUpInside事件偶房。
這樣修改后,按鈕的UIControlEventTouchUpOutside事件不會觸發(fā)了军浆,因為滑出按鈕范圍棕洋,直接就走了UIControlEventTouchCancel事件。
所以該修改只能讓按鈕點擊拖出范圍后馬上取消事件處理并恢復高亮狀態(tài)到默認狀態(tài)乒融,并不能實現(xiàn)重新拖回按鈕內(nèi)再次響應掰盘。

2,UIControl層次的軌跡監(jiān)聽處理

- (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) { // 現(xiàn)在在外面
        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 { // 現(xiàn)在在里邊
        if (!isPreInRect) { // 之前在外邊
            // 從外邊滑動到里邊
            [self sendActionsForControlEvents:UIControlEventTouchDragEnter];
            return [super continueTrackingWithTouch:touch withEvent:event];
        } else { // 之前在里邊
            // 在按鈕內(nèi)拖動
            return [super continueTrackingWithTouch:touch withEvent:event];
        }
    }
    
    return [super continueTrackingWithTouch:touch withEvent:event];
}

該方案的效果和上面一樣赞季,但是面臨幾個問題:
1愧捕,該方法返回NO后,UIControl的事件監(jiān)聽也不會進行了申钩,按鈕外的滑動不會觸發(fā)UIControlEventTouchDragOutside事件次绘。
2,原因同上,按鈕從外部滑動會內(nèi)部邮偎,也不會重新恢復高亮狀態(tài)罗洗,UIControlEventTouchDragEnter、UIControlEventTouchDragInside钢猛、UIControlEventTouchUpInside事件也不會重新處理伙菜。

3,優(yōu)化處理第一步命迈,按鈕外滑動重新出發(fā)UIControlEventTouchDragOutside事件

既然UIControl的事件無法處理贩绕,continueTrackingWithTouch不會再調(diào)用,那么我們嘗試在touchesMoved方法中手動調(diào)用:

- (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) { // 現(xiàn)在在外面
        if (isPreInRect) { // 之前在里邊
            // 從里邊滑動到外邊
            
        } else {  // 之前在外邊
            // 在按鈕外拖動
            [self sendActionsForControlEvents:UIControlEventTouchDragOutside];
        }
    } else { // 現(xiàn)在在里邊
        if (!isPreInRect) { // 之前在外邊
            // 從外邊滑動到里邊
        
        } else { // 之前在里邊
            // 在按鈕內(nèi)拖動
        }
    }
    [super touchesMoved:touches withEvent:event];
    
}

下面發(fā)現(xiàn)壶愤,按鈕在外部滑動淑倾,也會觸發(fā)UIControlEventTouchDragOutside事件了。

4征椒,優(yōu)化處理第二部娇哆,滑動會按鈕重新進入點擊狀態(tài)并觸發(fā)相應事件。

上面分析得出勃救,UIControl的事件監(jiān)聽被截斷了碍讨,而它的開始是從beginTrackingWithTouch方法開始的,嘗試在touchesMoved方法中當滑動回按鈕范圍內(nèi)的時刻蒙秒,重新開始UIControl事件的監(jiān)聽勃黍,即手動調(diào)用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) { // 現(xiàn)在在外面
        if (isPreInRect) { // 之前在里邊
            // 從里邊滑動到外邊
            
        } else {  // 之前在外邊
            // 在按鈕外拖動
            [self sendActionsForControlEvents:UIControlEventTouchDragOutside];
        }
    } else { // 現(xiàn)在在里邊
        if (!isPreInRect) { // 之前在外邊
            // 從外邊滑動到里邊
            [self beginTrackingWithTouch:touch withEvent:event];
        } else { // 之前在里邊
            // 在按鈕內(nèi)拖動
        }
    }
    [super touchesMoved:touches withEvent:event];
}

運行測試發(fā)現(xiàn)并沒有起效果,而之前已經(jīng)分析出beginTrackingWithTouch方法是在touchesBegan方法之后調(diào)用的晕讲,可能是缺少了touchesBegan方法中的相應處理覆获,嘗試直接調(diào)用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) { // 現(xiàn)在在外面
        if (isPreInRect) { // 之前在里邊
            // 從里邊滑動到外邊
            
        } else {  // 之前在外邊
            // 在按鈕外拖動
            [self sendActionsForControlEvents:UIControlEventTouchDragOutside];
        }
    } else { // 現(xiàn)在在里邊
        if (!isPreInRect) { // 之前在外邊
            // 從外邊滑動到里邊
            //[self beginTrackingWithTouch:touch withEvent:event];
            [self touchesBegan:[NSSet setWithObject:touch] withEvent:event];
        } else { // 之前在里邊
            // 在按鈕內(nèi)拖動
        }
    }
    [super touchesMoved:touches withEvent:event];
}

運行發(fā)現(xiàn),按鈕重新滑動回來瓢省,響應的事件可以正常觸發(fā)弄息,并且按鈕可以重新恢復成高亮狀態(tài)!

5勤婚,最后的優(yōu)化摹量,按鈕范圍外松開手指的事件觸發(fā)

現(xiàn)在按鈕已經(jīng)滿足除了按鈕范圍外松開手指的事件UIControlEventTouchUpOutside的其它所有事件的完美觸發(fā)。
之所以不會觸發(fā)這個事件蛔六,是因為上面我們其實在滑動出按鈕范圍后荆永,就已經(jīng)截斷了UIControl的事件處理,UIControlEventTouchDragOutside的事件是我們在touchesMoved方法中手動觸發(fā)的国章。
現(xiàn)在我們也在touch方法中,手動觸發(fā)這個事件豆村。因為我們之前已經(jīng)分析出:UIControlEventTouchUpInside液兽、UIControlEventTouchUpOutside都是在touchesEnded方法后觸發(fā)的,所重寫這個方法,當松開手指后的點在按鈕范圍外四啰,就手動發(fā)送這個事件:

- (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) { // 現(xiàn)在在外面
        if (isPreInRect) { // 之前在里邊
            // 從里邊滑動到外邊
            
        } else {  // 之前在外邊
            // 在按鈕外拖動
            // 在按鈕范圍外拖動手動發(fā)送UIControlEventTouchDragOutside事件
            [self sendActionsForControlEvents:UIControlEventTouchDragOutside];
        }
    } else { // 現(xiàn)在在里邊
        if (!isPreInRect) { // 之前在外邊
            // 從外邊滑動到里邊
            // 從按鈕范圍外滑動回按鈕范圍內(nèi)宁玫,需要手動調(diào)用touchesBegan方法,讓按鈕進入高亮狀態(tài)柑晒,并開啟UIControl的事件監(jiān)聽
            //[self beginTrackingWithTouch:touch withEvent:event];
            [self touchesBegan:[NSSet setWithObject:touch] withEvent:event];
        } else { // 之前在里邊
            // 在按鈕內(nèi)拖動
        }
    }
    [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)) {
        // 手動觸發(fā)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) { // 現(xiàn)在在外面
        if (isPreInRect) { // 之前在里邊
            // 從里邊滑動到外邊
            // 從按鈕范圍內(nèi)滑動到按鈕范圍外手動觸發(fā)UIControlEventTouchDragExit事件并阻斷按鈕默認事件的執(zhí)行
            [self sendActionsForControlEvents:UIControlEventTouchDragExit];
            // 阻斷按鈕默認事件的事件的執(zhí)行后欧瘪,需要手動觸發(fā)touchesCancelled方法,讓按鈕從高亮狀態(tài)變成默認狀態(tài)
            [self touchesCancelled:[NSSet setWithObject:touch] withEvent:event];
            return NO;
        } else {  // 之前在外邊
            // 在按鈕外拖動
            // 在按鈕范圍外滑動時匙赞,需要手動觸發(fā)touchesCancelled方法佛掖,讓按鈕從高亮狀態(tài)變成默認狀態(tài),并阻斷按鈕默認事件的執(zhí)行
            [self touchesCancelled:[NSSet setWithObject:touch] withEvent:event];
            return NO;
        }
    } else { // 現(xiàn)在在里邊
        if (!isPreInRect) { // 之前在外邊
            // 從外邊滑動到里邊
            // 從按鈕范圍外滑動到按鈕范圍內(nèi)涌庭,需要手動觸發(fā)UIControlEventTouchDragEnter事件
            [self sendActionsForControlEvents:UIControlEventTouchDragEnter];
            return [super continueTrackingWithTouch:touch withEvent:event];
        } else { // 之前在里邊
            // 在按鈕內(nèi)拖動
            return [super continueTrackingWithTouch:touch withEvent:event];
        }
    }
    return [super continueTrackingWithTouch:touch withEvent:event];
}

運行效果:

buttonEvent.gif

6芥被,仍然存在的問題

最后唯一存在的問題就是從按鈕范圍內(nèi)拖動出按鈕范圍外的時候,因為手動調(diào)用了touchesCancelled方法坐榆,導致按鈕多余的發(fā)送了一次UIControlEventTouchCancel事件拴魄。

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

獲取授權(quán)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市席镀,隨后出現(xiàn)的幾起案子匹中,更是在濱河造成了極大的恐慌,老刑警劉巖豪诲,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件职员,死亡現(xiàn)場離奇詭異,居然都是意外死亡跛溉,警方通過查閱死者的電腦和手機焊切,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來芳室,“玉大人专肪,你說我怎么就攤上這事】昂睿” “怎么了嚎尤?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長伍宦。 經(jīng)常有香客問我芽死,道長,這世上最難降的妖魔是什么次洼? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任关贵,我火速辦了婚禮,結(jié)果婚禮上卖毁,老公的妹妹穿的比我還像新娘揖曾。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布炭剪。 她就那樣靜靜地躺著练链,像睡著了一般。 火紅的嫁衣襯著肌膚如雪奴拦。 梳的紋絲不亂的頭發(fā)上媒鼓,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音错妖,去河邊找鬼绿鸣。 笑死,一個胖子當著我的面吹牛站玄,可吹牛的內(nèi)容都是我干的枚驻。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼株旷,長吁一口氣:“原來是場噩夢啊……” “哼再登!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起晾剖,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤锉矢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后齿尽,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體沽损,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年循头,在試婚紗的時候發(fā)現(xiàn)自己被綠了绵估。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡卡骂,死狀恐怖国裳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情全跨,我是刑警寧澤缝左,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站浓若,受9級特大地震影響渺杉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜挪钓,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一是越、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧诵原,春花似錦英妓、人聲如沸挽放。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至吗蚌,卻和暖如春腿倚,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蚯妇。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工敷燎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人箩言。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓硬贯,卻偏偏與公主長得像,于是被迫代替她去往敵國和親陨收。 傳聞我的和親對象是個殘疾皇子饭豹,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容