iOS-觸摸事件

iOS的事件分為三類:觸摸事件(手勢(shì)操作),運(yùn)動(dòng)事件(搖一搖),遠(yuǎn)程控制事件(耳機(jī)線控),本文主要整理的是觸摸事件,對(duì)其它兩種就不多做介紹了,感興趣的同學(xué)可以自己查閱資料。

一. 關(guān)于UIResponder

  • 如果一個(gè)類繼承于UIResponder那么這個(gè)類就是響應(yīng)者,就能處理觸摸事件。
    UIApplication、AppDelegate、UIWindow、UIViewController、UIView都直接或間接繼承于UIResponder,所以都是響應(yīng)者,都能處理觸摸事件。
  • 為什么說(shuō)繼承了UIResponder就能夠處理事件?
    因?yàn)閁IResponder實(shí)現(xiàn)了如下方法來(lái)處理事件:
//一根或者多根手指開始觸摸view,系統(tǒng)會(huì)自動(dòng)調(diào)用
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//一根或者多根手指在view上移動(dòng)時(shí),系統(tǒng)會(huì)自動(dòng)調(diào)用(可能多次調(diào)用)
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//一根或者多根手指離開view,系統(tǒng)會(huì)自動(dòng)調(diào)用
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//觸摸結(jié)束前,某個(gè)系統(tǒng)事件( 如電話呼)會(huì)打斷觸摸過(guò)程,系統(tǒng)會(huì)自動(dòng)調(diào)用
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

注意:

  1. 一次完整的觸摸過(guò)程中,只會(huì)產(chǎn)生一個(gè)事件對(duì)象,4個(gè)觸摸方法都是同一個(gè)event參數(shù)。
  2. 如果兩根手指同時(shí)觸摸一個(gè)view,那么view只會(huì)調(diào)用一次touchesBegan:withEvent:方法,touches參數(shù)中裝著2個(gè)UITouch對(duì)象。
  3. 如果這兩根手指一前一后分開觸摸同一個(gè)view,那么view會(huì)分別調(diào)用兩次touchesBegan:withEvent:方法,并且每次調(diào)用時(shí)的touches參數(shù)中只包含一個(gè)UITouch對(duì)象。

二. UITouch和UIEvent介紹

1. UITouch

① UITouch的屬性

  1. (NSSet<UITouch *> *)touches中存放的都是UITouch對(duì)象,它是一個(gè)NSSet集合。
  2. UITouch對(duì)象它就是用來(lái)保存手指相關(guān)聯(lián)的信息,包括位置,時(shí)間觸摸產(chǎn)生時(shí)所處的窗口,觸摸的View,點(diǎn)擊的次數(shù)觸摸,階段等信息。
  3. 每一個(gè)手指對(duì)應(yīng)著一個(gè)UITouch對(duì)象。
  4. 這個(gè)UITouch是系統(tǒng)自動(dòng)幫我們創(chuàng)建的,當(dāng)手指移動(dòng)時(shí),系統(tǒng)會(huì)更新同一個(gè)UITouch對(duì)象,使它能夠一直保存該手指在的觸摸位置,當(dāng)手指離開屏幕時(shí),系統(tǒng)會(huì)銷毀相應(yīng)的UITouch對(duì)象。
//觸摸產(chǎn)生時(shí)所處的窗口
@property(nonatomic,readonly,retain)UIWindow *window
//觸摸產(chǎn)生時(shí)所處的視圖
@property(nonatomic,readonly,retain)UIView *view
//短時(shí)間內(nèi)點(diǎn)按屏幕的次數(shù),可以根據(jù)tapCount判斷單擊、雙擊或更多的點(diǎn)擊
@property(nonatomic,readonly)NSUInteger tapCount;
//記錄觸摸事件產(chǎn)生或變化時(shí)的時(shí)間,單位是秒
@property(nonatomic,readonly)NSTimeInterval timestamp;
//當(dāng)前觸摸事件所處的狀態(tài)
@property(nonatomic,readonly)UITouchPhase  phase;

② UITouch的方法

//返回值表示觸摸在view上的位置
//這里返回的位置是針對(duì)view的坐標(biāo)系的(以view的左上角為原點(diǎn)(0, 0)
//若調(diào)用時(shí)傳的view參數(shù)為nil的話,返回的是觸摸點(diǎn)是在UIWindow的位置
-(CGPoint)locationInView:(UIView*)view;
//該方法記錄了前一個(gè)觸摸點(diǎn)的位置 
- (CGPoint)previousLocationInView:(UIView*)view;

2. UIEvent

每產(chǎn)生一個(gè)事件,就會(huì)產(chǎn)生一個(gè)UIEvent對(duì)象,UIEvent稱為事件對(duì)象,記錄事件產(chǎn)生的時(shí)刻和類型。

常見(jiàn)屬性

//事件類型
@property(nonatomic,readonly) UIEventType     type;
@property(nonatomic,readonly) UIEventSubtype  subtype;
//事件產(chǎn)生的時(shí)間
@property(nonatomic,readonly)NSTimeIntervaltimestamp;

三. 尋找最佳響應(yīng)者,響應(yīng)者鏈條

觸摸事件從產(chǎn)生到處理完成一共經(jīng)過(guò)兩步:

  1. 尋找事件的最佳響應(yīng)者
  2. 通過(guò)響應(yīng)者鏈條尋找最終處理者,直到有處理者處理這個(gè)事件

1. 尋找事件的最佳響應(yīng)者

繼承于UIResponder的類就是響應(yīng)者,一個(gè)頁(yè)面上通常會(huì)有許許多多個(gè)這種類型的對(duì)象,都可以對(duì)點(diǎn)擊事件作出響應(yīng)。為了避免沖突,這就需要有一個(gè)先后順序,也就是響應(yīng)的優(yōu)先級(jí),hitTest方法的目的就是找到具有最高優(yōu)先級(jí)的響應(yīng)對(duì)象,就是最佳響應(yīng)者。

下面先用MJ老師的一張圖來(lái)解釋如何尋找事件的最佳響應(yīng)者:

尋找最佳響應(yīng)者

尋找最佳響應(yīng)者的順序?yàn)?
UIApplication -> UIWindow -> UIView

發(fā)生觸摸事件后,系統(tǒng)會(huì)將該事件加入到一個(gè)由UIApplication管理的事件隊(duì)列中。UIApplication會(huì)從事件隊(duì)列中取出最前面的事件并將事件分發(fā)給應(yīng)用程序的keyWindow,keyWindow判斷自己可以接收觸摸事件并且觸摸點(diǎn)在自己身上,就會(huì)從后往前遍歷子控件,然后重復(fù)以上步驟,直到找到最佳響應(yīng)者。

那么系統(tǒng)內(nèi)部究竟是如何找到最合適的視圖呢?

我們發(fā)現(xiàn)UIView中包括以下兩個(gè)方法:
這兩個(gè)方法用于尋找最佳響應(yīng)者,所以說(shuō)凡是繼承于UIView的類都有可能成為最佳響應(yīng)者。

//這個(gè)方法返回的就是最佳響應(yīng)者
//只要一個(gè)事件,傳遞給一個(gè)控件時(shí), 就會(huì)調(diào)用這個(gè)控件的hitTest方法
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system

//用于判斷觸摸的點(diǎn)是否在自己身上
//point:必須是方法調(diào)用者的坐標(biāo)系
//hitTest方法底層會(huì)調(diào)用這個(gè)方法,判斷點(diǎn)在不在控件上
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   // default returns YES if point is in bounds

hitTest方法內(nèi)部干的事情就是下面??

  1. 判斷自己是否能接收觸摸事件, 如果是
  2. 判斷觸摸點(diǎn)是否在自己身上, 如果是
  3. 從后往前遍歷子控件,重復(fù)前面的兩個(gè)步驟
  4. 如果沒(méi)有符合條件的子控件,那么hitTest就返回自己最適合處理
  • UIView 不接收觸摸事件的三種情況:
    不接收用戶交互 userInteractionEnabled=NO
    隱藏 hidden=YES.
    透明 alpha=0.0 ~ 0.01

  • UIImageView的userInteractionEnabled默認(rèn)就是NO,因此UIImageView以及它的子控件默認(rèn)是不能接收觸摸事件的

既然我們都知道hitTest內(nèi)部做了什么,那么我們可以嘗試實(shí)現(xiàn)hitTest方法,代碼如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 先判斷視圖是否處于不能響應(yīng)事件的3種狀態(tài)
    if (self.userInteractionEnabled == NO || self.hidden || self.alpha < 0.01) {
        return nil;
    }
    // 判斷觸摸點(diǎn)是否在視圖的坐標(biāo)范圍內(nèi)
    if ([self pointInside:point withEvent:event] == NO) {
        return nil;
    }
    // 從后向前遍歷視圖的子視圖
    for (int i = (int)self.subviews.count - 1; i >= 0; i--) {
        UIView *subView = self.subviews[I];
        // 坐標(biāo)轉(zhuǎn)換,把觸摸點(diǎn)的位置轉(zhuǎn)換為子視圖坐標(biāo)系下的坐標(biāo)
        CGPoint subPoint = [self convertPoint:point toView:subView];
        // 對(duì)子視圖進(jìn)行Hit-Testing
        UIView *subHTView = [subView hitTest:subPoint withEvent:event];
        // 如果子視圖有最佳響應(yīng)者,返回該最佳響應(yīng)者視圖,結(jié)束循環(huán)
        if (subHTView) {
            return subHTView;
        }
    }
    // 如果子視圖中沒(méi)有最佳響應(yīng)者,返回自己
    return self;
}

我們通過(guò)這段代碼還可以解釋另外一種現(xiàn)象,子視圖超出了父視圖的范圍,點(diǎn)擊子視圖在父視圖之外的部分沒(méi)有反應(yīng)。這是因?yàn)樵谶M(jìn)行hitTest的時(shí)候,父視圖雖然能響應(yīng)事件,但是點(diǎn)并不不父視圖內(nèi),直接就返回nil了,自然不會(huì)再去詢問(wèn)子視圖是否能夠響應(yīng)事件。

事件的最佳響應(yīng)者找到之后,下面就是通過(guò)響應(yīng)者鏈條尋找事件的最終處理者。

2. 通過(guò)響應(yīng)者鏈條尋找事件的最終處理者

① 什么是響應(yīng)者鏈條

最佳響應(yīng)者的nextResponder是下一個(gè)響應(yīng)者,下一個(gè)響應(yīng)者的nextResponder是下下一個(gè)響應(yīng)者,一直到AppDelegate,他們就組成了響應(yīng)者鏈條。

對(duì)于響應(yīng)者對(duì)象,默認(rèn)的nextResponder實(shí)現(xiàn)如下??
UIView:若視圖是控制器的根視圖,則其nextResponder為控制器對(duì)象;否則,其nextResponder為父視圖。
UIViewController:若控制器的視圖是window的根視圖,則其nextResponder為窗口對(duì)象;若控制器是從別的控制器present出來(lái)的,則其nextResponder為presenting view controller。
UIWindow:nextResponder為UIApplication對(duì)象
UIApplication:若當(dāng)前應(yīng)用的app delegate是一個(gè)UIResponder對(duì)象,且不是UIView、UIViewController或app本身,則UIApplication的nextResponder為app delegate
AppDelegate:nextResponder為nil

他們就組成了響應(yīng)鏈條:
View -> UIViewController -> UIWindow -> UIApplication -> AppDelegate

官網(wǎng)響應(yīng)者鏈條示意圖如下:
響應(yīng)者鏈條示意圖

上圖是官網(wǎng)對(duì)于響應(yīng)鏈的示例展示,若觸摸發(fā)生在UITextField上,則事件的傳遞順序是:
UITextField->UIView->UIView->UIViewController->UIWindow->UIApplication->UIApplicationDelegate。

圖中虛線箭頭是指若該UIView是作為UIViewController根視圖存在的,則其nextResponder為UIViewController對(duì)象;若是直接add在UIWindow上的,則其nextResponder為UIWindow對(duì)象。

可以用以下方式打印一個(gè)響應(yīng)鏈中的每一個(gè)響應(yīng)對(duì)象,在最佳響應(yīng)者的 touchBegin:withEvent: 方法中調(diào)用即可(別忘了調(diào)用父類的方法)。

- (void)printResponderChain
{
    UIResponder *responder = self;
    printf("%s",[NSStringFromClass([responder class]) UTF8String]);
    while (responder.nextResponder) {
        responder = responder.nextResponder;
        printf(" --> %s",[NSStringFromClass([responder class]) UTF8String]);
    }
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self printResponderChain];
    [super touchesBegan:touches withEvent:event];
}

以上一節(jié)tabBar上凸起的圓形按鈕的案例為例,重寫CircleButton的 touchBegin:withEvent:方法,點(diǎn)擊原型按鈕的任意區(qū)域,打印出的完整響應(yīng)鏈如下:
CircleButton --> CustomeTabBar --> UIView --> UIViewController --> UIViewControllerWrapperView --> UINavigationTransitionView --> UILayoutContainerView --> UINavigationController --> UIWindow --> UIApplication --> AppDelegate
另外如果有需要,完全可以重寫響應(yīng)者的 nextResponder 方法來(lái)自定義響應(yīng)鏈。但是這個(gè)屬性值只讀的,不允許重寫。

② 如何找到最終處理者

經(jīng)歷hitTest后,UIApplication已經(jīng)知道事件的最佳響應(yīng)者是誰(shuí)了,接下來(lái)要做的兩件事情就是:

  1. 將事件傳遞給最佳響應(yīng)者響應(yīng)處理。如果最佳響應(yīng)者不處理的話:??
  2. 通過(guò)最佳響應(yīng)者的nestResponder將事件沿著響應(yīng)鏈傳遞,直到有UIResponder對(duì)象對(duì)此事件負(fù)責(zé)。

首先:UIApplication如何把事件傳遞給最佳響應(yīng)者?

因?yàn)樽罴秧憫?yīng)者具有最高的事件響應(yīng)優(yōu)先級(jí),因此UIApplication會(huì)先將事件傳遞給它供其響應(yīng)。首先,UIApplication將事件通過(guò) sendEvent: 傳遞給事件所屬的window,window同樣通過(guò) sendEvent: 再將事件傳遞給hit-tested view,即最佳響應(yīng)者。過(guò)程如下:

UIApplication ——> UIWindow ——> hit-tested view

如下,可以發(fā)現(xiàn)UIApplication和UIWindow里面的確有sendEvent方法:

//UIApplication里面的sendEvent方法
- (void)sendEvent:(UIEvent *)event;

//UIWindow里面的sendEvent方法
- (void)sendEvent:(UIEvent *)event; // called by UIApplication to dispatch events to views inside the window

以尋找事件的最佳響應(yīng)者一節(jié)中點(diǎn)擊視圖E為例,在EView的 touchesBegan:withEvent: 上斷點(diǎn)查看調(diào)用棧就能看清這一過(guò)程:

image

那么問(wèn)題又來(lái)了。這個(gè)過(guò)程中,假如應(yīng)用中存在多個(gè)window對(duì)象,UIApplication是怎么知道要把事件傳給哪個(gè)window的?window又是怎么知道哪個(gè)視圖才是最佳響應(yīng)者的呢?

其實(shí)簡(jiǎn)單思考一下,這兩個(gè)過(guò)程都是傳遞事件的過(guò)程,涉及的方法都是 sendEvent: ,而該方法的參數(shù)(UIEvent對(duì)象)是唯一貫穿整個(gè)經(jīng)過(guò)的線索,那么就可以大膽猜測(cè)必然是該觸摸事件對(duì)象上綁定了這些信息。事實(shí)上之前在介紹UITouch的時(shí)候就說(shuō)過(guò)touch對(duì)象保存了觸摸所屬的window及view,而event對(duì)象又綁定了touch對(duì)象,如此一來(lái),是不是就知道是哪個(gè)window了。要是不信的話,那就自定義一個(gè)Window類,重寫 sendEvent: 方法,捕捉該方法調(diào)用時(shí)參數(shù)event的狀態(tài),答案就顯而易見(jiàn)了。

image

至于這兩個(gè)屬性是什么時(shí)候綁定到touch對(duì)象上的,必然是在hit-testing的過(guò)程中唄,仔細(xì)想想hit-testing干的不就是這個(gè)事兒?jiǎn)帷?/p>

其次:如果最佳響應(yīng)者不處理事件怎么辦?

如果最佳響應(yīng)者能處理事件,那肯定把事件交給最佳響應(yīng)者處理,如果它不處理,再順著響應(yīng)者鏈條往上尋找其他處理者(其他博客都說(shuō)響應(yīng)者鏈?zhǔn)峭聦ふ?,其?shí)順序都是一個(gè)意思)。

  1. 找到最佳響應(yīng)者之后,如果這個(gè)view可以處理就會(huì)給這個(gè)view處理,如果這個(gè)view沒(méi)處理。
  2. 如果view的控制器存在就傳遞給view的控制器處理;如果控制器不存在,則將其傳遞給它的父視圖。
  3. 在視圖層次結(jié)構(gòu)的最頂級(jí)視圖,如果也不能處理收到的事件,則其將事件傳遞給window對(duì)象進(jìn)行處理。
  4. 如果window對(duì)象也不能處理 ,則其將事件傳遞給UIApplication對(duì)象處理。
  5. 如果UIApplication也不能處理該事件,則其將事件傳遞給AppDelegate,如果AppDelegate也不能處理,則將事件丟棄。

就是將事件順著響應(yīng)者鏈條向上傳遞,將事件交給上一個(gè)響應(yīng)者進(jìn)行處理,找到處理事件的最終處理者之后他就會(huì)調(diào)用touchesBegan等方法來(lái)進(jìn)行事件處理。

③ 如何處理事件

響應(yīng)者對(duì)于事件的攔截以及傳遞都是通過(guò)touchesBegan:withEvent:方法控制的,該方法的默認(rèn)實(shí)現(xiàn)是將事件沿著默認(rèn)的響應(yīng)鏈往上傳遞。如果你在不同的UIResponder對(duì)象上面都聲明了touchMoved方法,那么這些對(duì)象都可以執(zhí)行該方法,因?yàn)閠ouchesBegan方法默認(rèn)是把事件沿著響應(yīng)鏈傳遞的。如果只想讓一個(gè)對(duì)象響應(yīng)touchesMoved方法,需要重寫touchesBegan方法以攔截事件。

響應(yīng)者對(duì)于接收到的事件有3種操作:

  1. 不攔截,默認(rèn)操作
    事件會(huì)自動(dòng)沿著默認(rèn)的響應(yīng)鏈向上傳遞。
  2. 攔截,不再往上分發(fā)事件
    重寫touchesBegan:withEvent:方法進(jìn)行事件處理,不調(diào)用父類的touchesBegan:withEvent:方法。
  3. 攔截,繼續(xù)往上分發(fā)事件
    重寫touchesBegan:withEvent:進(jìn)行事件處理,同時(shí)調(diào)用父類的touchesBegan:withEvent:將事件往上傳遞。

四. hitTest驗(yàn)證和應(yīng)用

1. 驗(yàn)證hitTest方法調(diào)用順序

新建HTView繼承于UIView,重寫hitTest:withEvent:方法。

HTView代碼如下:

@interface HTView : UIView
@property (nonatomic, strong) NSString *name; //視圖的名字
@end

@implementation HTView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    NSLog(@"進(jìn)入%@視圖-%s", self.name, __func__);
    UIView *view = [super hitTest:point withEvent:event];
    NSLog(@"離開%@視圖-%s", self.name, __func__);
    return view;
}
@end

ViewController代碼如下:

#import "ViewController.h"
#import "HTView.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet HTView *aView;
@property (weak, nonatomic) IBOutlet HTView *bView;
@property (weak, nonatomic) IBOutlet HTView *cView;
@property (weak, nonatomic) IBOutlet HTView *dView;
@property (weak, nonatomic) IBOutlet HTView *eView;
@property (weak, nonatomic) IBOutlet HTView *fView;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.aView.name = @"A";
    self.bView.name = @"B";
    self.cView.name = @"C";
    self.dView.name = @"D";
    self.eView.name = @"E";
    self.fView.name = @"F";
}
@end

如圖:
hitTest方法順序.png

如圖所示,A視圖上面添加了子視圖B和C,B上面添加了子視圖D,C上面添加了子視圖E和F。

點(diǎn)擊E后打印如下:

點(diǎn)擊E

由打印結(jié)果可以看出

  1. 事件首先傳遞給視圖A,A判斷自身能響應(yīng)事件并且點(diǎn)在自己內(nèi)
  2. 繼續(xù)從后向前遍歷A的子視圖,因?yàn)镃比B后添加,因此首先傳遞給C
  3. C判斷自身能響應(yīng)事件,并且點(diǎn)在自己內(nèi)。繼續(xù)從后向前遍歷C的子視圖,因?yàn)镕比E后添加,因此首先傳遞給F
  4. F判斷自身不能響應(yīng)事件(因?yàn)辄c(diǎn)不在F內(nèi)),C又將事件傳遞給E
  5. E判斷自身能響應(yīng)事件,同時(shí)E已經(jīng)沒(méi)有子視圖,因此最終E就是最佳響應(yīng)者

這里有兩個(gè)問(wèn)題:

  1. 為什么要從后往前遍歷? 因?yàn)楹竺娴囊晥D一般都是后添加的,一般在后面,這樣遍歷節(jié)省時(shí)間。
  2. 為什么一直打印兩次? 這個(gè)我也不知道為什么一直都是打印兩次,就這樣記吧。

2. hitTest應(yīng)用

應(yīng)用①:

view上添加一個(gè)button和一個(gè)藍(lán)色view, 藍(lán)色view遮擋在按鈕的上面
點(diǎn)擊View時(shí), View接收事件,當(dāng)發(fā)現(xiàn)點(diǎn)擊的點(diǎn)在按鈕的位置時(shí), 讓底部的按鈕處理事件

如圖:
示例

實(shí)現(xiàn)藍(lán)色View的touchBegain方法,先監(jiān)聽UIView的點(diǎn)擊. 并去實(shí)現(xiàn)UIView的hitTest方法, 在hitTest方法當(dāng)中通過(guò)把當(dāng)前點(diǎn)轉(zhuǎn)換成按鈕所在的坐標(biāo)系
CGPoint btnP = [self convertPoint:point toView:self.btn];
轉(zhuǎn)換過(guò)后查看當(dāng)前點(diǎn)在不在按鈕上,如果在按鈕上,就直接返回按鈕, 如果不在按鈕上,保持系統(tǒng)默認(rèn)做法

藍(lán)色view里面實(shí)現(xiàn)代碼:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    //判斷當(dāng)前點(diǎn)在不在按鈕上.
    //把當(dāng)前點(diǎn)轉(zhuǎn)換成按鈕所在的坐標(biāo)系
    CGPoint btnP = [self convertPoint:point toView:self.btn];
    if ([self.btn pointInside:btnP withEvent:event]) {
        return self.btn;
    }else{
      return [super hitTest:point withEvent:event];
    }
 }

應(yīng)用②:

對(duì)話框按鈕內(nèi)部添加一個(gè)云朵按鈕, 添加的云朵按鈕在父控件的外面,如圖:
對(duì)話框

要求:
1.按鈕可以隨著手指拖動(dòng)而拖動(dòng)
2.讓超過(guò)按鈕的子控件也能夠響應(yīng)事件,一般情況下,當(dāng)一個(gè)控件超過(guò)他的父控件的時(shí)候,是不能夠接收事件的.

實(shí)現(xiàn)思路:
第一步:先辦到讓對(duì)話框按鈕能夠跟隨著手指移動(dòng)而移動(dòng)
實(shí)現(xiàn)對(duì)話框按鈕的touchesMoved方法,在touchesMoved方法當(dāng)中,獲得當(dāng)前手指所在的點(diǎn).以前上一個(gè)點(diǎn)
分別計(jì)算X軸的偏移量以及Y軸的偏移量
然后修改當(dāng)前按鈕的transform讓按鈕辦到能夠跟隨著手指移動(dòng)而移動(dòng)
第二步, 實(shí)現(xiàn)對(duì)話框按鈕的hitTest方法
在該方法當(dāng)中去判斷當(dāng)前的點(diǎn)在不在按鈕的子控件上
如果在按鈕的子控件上.就返回按鈕的子控件如果不在的話, 就保持系統(tǒng)的默認(rèn)做法

對(duì)話框按鈕.m 文件

#import "chatBtn.h"
@implementation chatBtn
 
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    //判斷當(dāng)前點(diǎn)在不在popBtn身上
    //把當(dāng)前點(diǎn)轉(zhuǎn)換popBtn身上的點(diǎn)
    CGPoint popBtnP = [self convertPoint:point toView:self.popBtn];
   if ( [self.popBtn pointInside:popBtnP withEvent:event]) {
       return self.popBtn;
   }else{
       return [super hitTest:point withEvent:event];
   }
}
 
-(void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    //1.獲取UITouch
    UITouch *touch = [touches anyObject];
    //2.獲取當(dāng)前手指的點(diǎn),上一個(gè)手指的點(diǎn)
    CGPoint curP = [touch locationInView:self];
    CGPoint preP = [touch previousLocationInView:self];
    //3.計(jì)算偏移量
    CGFloat offsetX = curP.x - preP.x;
    CGFloat offsetY = curP.y - preP.y;
    //4.平移
    self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}
@end

應(yīng)用③:

如果碰到這種需求怎么辦?比如說(shuō)tabBar中間的按鈕凸起
截圖

點(diǎn)擊凸起區(qū)域后,生成的觸摸事件首先傳到UIWindow,然后傳到控制器的根視圖即RootView。RootView經(jīng)判斷可以響應(yīng)觸摸事件,而后將事件傳給了子控件TabBar。問(wèn)題就出在這里,因?yàn)橛|摸點(diǎn)不在TabBar的坐標(biāo)范圍內(nèi),因此TabBar無(wú)法響應(yīng)該觸摸事件,hitTest:withEvent: 直接返回了nil。而后RootView就會(huì)詢問(wèn)TableView是否能夠響應(yīng),事實(shí)上是可以的,因此事件最終被TableView消耗。整個(gè)過(guò)程,事件根本沒(méi)有傳遞到圓形按鈕。

有問(wèn)題就會(huì)有解決策略。經(jīng)過(guò)分析,發(fā)現(xiàn)原因是hit-Testing的過(guò)程中,事件在傳遞到TabBar的時(shí)候沒(méi)能繼續(xù)往CircleButton傳,因?yàn)辄c(diǎn)擊區(qū)域坐標(biāo)不在Tabbar的坐標(biāo)范圍內(nèi),因此Tabbar被識(shí)別成了無(wú)法響應(yīng)事件。既然如此,我們可以修改事件hit-Testing的過(guò)程,當(dāng)點(diǎn)擊紅色方框區(qū)域時(shí)讓事件流向原型按鈕。事件傳遞到TabBar時(shí),TabBar的 hitTest:withEvent: 被調(diào)用,但是 pointInside:withEvent: 會(huì)返回NO,如此一來(lái) hitTest:withEvent: 返回了nil。既然如此,可以重寫TabBard的 pointInside:withEvent: ,判斷當(dāng)前觸摸坐標(biāo)是否在子視圖CircleButton的坐標(biāo)范圍內(nèi),若在,則返回YES,反之返回NO。這樣一來(lái)點(diǎn)擊紅色區(qū)域,事件最終會(huì)傳遞到CircleButton,CircleButton能夠響應(yīng)事件,最終事件就由CircleButton響應(yīng)了。同時(shí)點(diǎn)擊紅色方框以外的非TabBar區(qū)域的情況下,因?yàn)門abBar無(wú)法響應(yīng)事件,會(huì)按照預(yù)期由TableView響應(yīng)。代碼如下:

//tabBar
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    //將觸摸點(diǎn)坐標(biāo)轉(zhuǎn)換到在circleButton上的坐標(biāo)
    CGPoint subPoint = [self convertPoint:point toView:self.circleButton];
    //若觸摸點(diǎn)circleButton上則返回YES
    if ([self.circleButton pointInside:subPoint withEvent:event])       {
        return YES;
    }
    //否則返回默認(rèn)的操作
    return [super pointInside:point withEvent:event];
}

五. 總結(jié)

  1. hitTest:withEvent:和pointInside:withEvent:是UIView的方法,用來(lái)尋找最佳響應(yīng)者的。
  2. sendEvent:是UIApplication和UIWindow的方法,用來(lái)把事件傳遞給最佳響應(yīng)者。
  3. touchesBegan:withEvent:等方法是UIResponder的方法,UIApplication、AppDelegate、UIWindow、UIViewController、UIView都直接或間接繼承于UIResponder,所以都是響應(yīng)者,都能處理觸摸事件。

Demo地址:https://github.com/iamkata/touch

本文部分參考以下鏈接,如有侵權(quán)請(qǐng)聯(lián)系刪除。
iOS觸摸事件處理
iOS觸摸事件全家桶

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 在iOS開發(fā)中經(jīng)常會(huì)涉及到觸摸事件。本想自己總結(jié)一下,但是遇到了這篇文章,感覺(jué)總結(jié)的已經(jīng)很到位,特此轉(zhuǎn)載。作者:L...
    WQ_UESTC閱讀 6,250評(píng)論 4 26
  • 本文主要講解iOS觸摸事件的一系列機(jī)制,涉及的問(wèn)題大致包括: 觸摸事件由觸屏生成后如何傳遞到當(dāng)前應(yīng)用? 應(yīng)用接收觸...
    baihualinxin閱讀 1,280評(píng)論 0 9
  • 掏出手機(jī)->解鎖手機(jī)->點(diǎn)開APP->低頭刷手機(jī),這一套流程是現(xiàn)在每個(gè)手機(jī)黨每天重復(fù)最多的一套連擊操作,那么作為開...
    LoveY34閱讀 758評(píng)論 0 1
  • 轉(zhuǎn)載: https://blog.csdn.net/qq871531334/article/details/822...
    NicooYang閱讀 1,689評(píng)論 0 9
  • 目錄一、基本概念1.1、UITouch1.2、UIEvent1.3、UIResponder二、查找第一響應(yīng)者三、響...
    barry閱讀 1,822評(píng)論 0 5

友情鏈接更多精彩內(nèi)容