UI篇-UIResponder之事件傳遞和響應(yīng)精析

一張UI繼承關(guān)系圖示


iOS中的幾種常見事件

這篇文章只討論觸摸事件。對于觸摸事件UIResponder內(nèi)部提供了以下方法來處理事件:

  • 事件對象在UIEvent
    UIEvent.h文件中,我們可以看到有一個UIEventType類型的屬性矮嫉,這個屬性表示了當(dāng)前的響應(yīng)事件類型癞埠。分別有多點觸控芽世、搖一搖以及遠(yuǎn)程操作(在iOS之后新增了3DTouch事件類型)。在一個用戶點擊事件處理過程中藻三,UIEvent對象是唯一的洪橘。

  • 點擊對象UITouch
    UITouch表示單個點擊,其類文件中存在枚舉類型UITouchPhase的屬性棵帽,用來表示當(dāng)前點擊的狀態(tài)熄求。這些狀態(tài)包括點擊開始、移動逗概、停止不動弟晚、結(jié)束和取消五個狀態(tài)。每次點擊發(fā)生的時候逾苫,點擊對象都放在一個集合中傳入UIResponder的回調(diào)方法中卿城,我們通過集合中對象獲取用戶點擊的位置。
    其中通過- (CGPoint)locationInView:(nullable UIView *)view獲取當(dāng)前點擊坐標(biāo)點隶垮,- (CGPoint)previousLocationInView:(nullable UIView *)view獲取上個點擊位置的坐標(biāo)點藻雪。

觸摸事件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
其中只有在程序強(qiáng)制退出或者來電時,取消點擊事件才會調(diào)用狸吞。

加速計事件 (搖一搖)
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;

遠(yuǎn)程控制事件
額外配件如耳機(jī)上的音視頻播放按鍵所觸發(fā)的事件(視頻播放勉耀、下一首)
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;

事件鏈

  • 當(dāng)我們用手指輕觸屏幕,iPhone OS會將它識別為一組觸摸對象蹋偏,并將它們封裝在UITouch和UIEvent形式的實例便斥,消息循環(huán)(runloop)會接收到觸摸事件并放入當(dāng)前應(yīng)用程序的事件隊列中。
  • 負(fù)責(zé)管理應(yīng)用程序的UIApplication單件對象將事件從隊列的頂部取出威始,找到當(dāng)前運行的程序枢纠,典型情況下,它會將事件發(fā)送給應(yīng)用程序的鍵盤焦點窗口—即擁有當(dāng)前用戶事件焦點的窗口黎棠,然后代表該窗口的UIWindow對象晋渺。
  • UIWindow對象接受到事件開始進(jìn)行最優(yōu)響應(yīng)視圖查詢的過程(逆序遍歷subviews,后加載的先遍歷)脓斩。
  • UIView對象并不一定會把事件傳遞給每一個子view木西,因為UIView是通過hitTest方法來判斷點擊事件發(fā)生在哪個子view上面的,會采用逆序查詢也就是優(yōu)先查詢后加載的子試圖随静,這樣做也是為了優(yōu)化查找速度八千,畢竟后addSubview的視圖在上易于命中吗讶。如果它第一個hitTest就命中了的話,這個事件就不會再被傳遞給其他子試圖了恋捆。

舉個例子:

就像上圖那樣照皆,點擊了紅色的View,
如果先加載藍(lán)色View沸停,后加載紅色UIView 傳遞過程是這樣的:

 UIApplication對象——>UIWindow對象——>rootVC.view對象——>redview對象

如果先加載紅色View膜毁,后加載藍(lán)色UIView 傳遞過程是這樣的:

UIApplication對象——>UIWindow對象——>rootVC.view對象——>blueview對象——>redview對象

//************華麗分割線 便于閱讀***********

事件的傳遞其實就是在事件產(chǎn)生與分發(fā)之后如何尋找最優(yōu)響應(yīng)視圖的一個過程。其中涉及到了UIView中的兩個方法(可以重寫)愤钾,當(dāng)hitTest返回YES才會調(diào)用這個View的 Touch事件爽茴,因為如果返回NO,則當(dāng)前View被排除在相應(yīng)鏈之外了绰垂。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
//判斷當(dāng)前點擊事件是否存在最優(yōu)響應(yīng)者(First Responder)

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
//判斷當(dāng)前點擊是否在控件的Bounds之內(nèi)

視圖命中查找流程

1.調(diào)用hitTest方法進(jìn)行最優(yōu)響應(yīng)視圖查詢

hidden = YES
userInteractionEnabled = NO
alpha < 0.01

以上三種情況會使該方法返回nil,即當(dāng)前視圖下無最優(yōu)響應(yīng)視圖

2.hitTest方法內(nèi)部會調(diào)用pointInside方法對點擊點進(jìn)行是否在當(dāng)前視圖bounds內(nèi)進(jìn)行判斷火焰,如果超出bounds劲装,hitTest則返回nil。 未超出范圍則進(jìn)行步驟3

3.對當(dāng)前視圖下的subviews采取逆序上述1 2步驟查詢最優(yōu)響應(yīng)視圖昌简。如果hitTest返回了對應(yīng)視圖則說明在當(dāng)前視圖層級下有最優(yōu)響應(yīng)視圖占业,可能為self或者其subview,這個要看具體返回纯赎。

如何看到這一切呢谦疾?
我們可以重寫view的-(UIView )hitTest:(CGPoint)point withEvent:(UIEvent)event方法來測試

#import "UIView+MYtes.h"
#import <objc/runtime.h>

@implementation UIView (MYtes)
+ (void)load {

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        Class class = [self class];

        SEL oriSEL = @selector(hitTest:withEvent:);
        SEL swiSEL = @selector(wcq_hitTest:withEvent:);

        Method oriMethod = class_getInstanceMethod(class, oriSEL);
        Method swiMethod = class_getInstanceMethod(class, swiSEL);

        BOOL didAddMethod = class_addMethod(class, oriSEL,
                                            method_getImplementation(swiMethod),
                                            method_getTypeEncoding(swiMethod));

        if (didAddMethod) {

            class_replaceMethod(class,
                                swiSEL,
                                method_getImplementation(oriMethod),
                                method_getTypeEncoding(oriMethod));
        }else {

            method_exchangeImplementations(oriMethod, swiMethod);
        }
    });
}
- (UIView *)wcq_hitTest:(CGPoint)point withEvent:(UIEvent *)event {

    NSLog(@"%@     %s",[self class], __PRETTY_FUNCTION__);
    return [self wcq_hitTest:point withEvent:event];
}

然后我們分別新建三個UIView的子類: AView、BView犬金、CView并依次按順序添加到ViewController上


然后我們依次點擊A念恍、B視圖看下hitTes調(diào)用順序是否和預(yù)期一致

點擊AView

點擊BView

響應(yīng)者鏈

介紹響應(yīng)者鏈之前先介紹下響應(yīng)者對象

響應(yīng)者對象:是可以響應(yīng)事件并對其進(jìn)行處理的對象。UIResponder是所有響應(yīng)者對象的基類晚顷,它不僅為事件處理峰伙,而且也為常見的響應(yīng)者行為定義編程接口。UIApplication该默、UIView瞳氓、和所有從UIView派生出來的UIKit類(包括UIWindow)都直接或間接地繼承自UIResponder類。
第一響應(yīng)者是應(yīng)用程序中當(dāng)前負(fù)責(zé)接收觸摸事件的響應(yīng)者對象(通常是一個UIView對象)栓袖。

響應(yīng)者鏈:由一系列“下一個響應(yīng)者”組成

其順序如下:

  • 1.iOS系統(tǒng)在處理事件時匣摘,通過UIApplication對象和每個UIWindow對象的sendEvent:方法將事件以消息的形式分發(fā)給具體處理此事件的第一響應(yīng)者,使其有機(jī)會首先處理事件裹刮。如果第一響應(yīng)者沒有進(jìn)行處理音榜,第一響應(yīng)者將事件將處理事件的責(zé)任傳遞給下一個,更高級的對象,即當(dāng)前responder對象的nextResponder必指。
  • 2.UIView的nextResponder屬性囊咏,如果有管理此view的UIViewController對象,則為此UIViewController對象;否則nextResponder即為其superview
    UIViewController的nextResponder屬性為其管理view的superview.
    UIWindow的nextResponder屬性為UIApplication對象梅割。
    UIApplication的nextResponder屬性為nil霜第。
  • 3.類似地,視圖層次中的每個后續(xù)視圖如果不處理事件都首先傳遞給它的視圖控制器(如果有的話)户辞,然后是它的父視圖泌类。
  • 4.最上層的容器視圖將事件傳遞給UIWindow對象。
  • 5.UIWindow對象將事件傳遞給UIApplication單件對象底燎。
  • 6.如果應(yīng)用程序找不到能夠處理事件的響應(yīng)者對象刃榨,則丟棄該事件。

程序?qū)ふ夷軌蛱幚硎录膶ο笏裕录驮陧憫?yīng)者鏈中向上傳遞枢希。

//******************* 華麗的分割線 ****************

系統(tǒng)先調(diào)用pointInSide: WithEvent:判斷當(dāng)前視圖以及這些視圖的子視圖是否能接收這次點擊事件,然后在調(diào)用hitTest: withEvent:依次獲取處理這個事件的所有視圖對象朱沃,在獲取所有的可處理事件對象后苞轿,開始調(diào)用這些對象的touches回調(diào)方法

在自定義View中重寫 touchesBegan方法

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UIResponder * next = [self nextResponder];
    NSMutableString * prefix = @"".mutableCopy;
    
    while (next != nil) {
        NSLog(@"%@%@", prefix, [next class]);
        [prefix appendString: @"--"];
        next = [next nextResponder];
    }
}

通過這張圖我們可以看到響應(yīng)者鏈的組成。

需要注意的是:viewController.m文件中重寫touchBegan:withEvent:方法逗物,相當(dāng)于處理的是viewController的觸摸事件搬卒,想處理自定義View的觸摸事件,必須在自定義UIView中重寫touchBegan:withEvent:方法翎卓,兩者不是一回事契邀,但是都是繼承自UIResponder 。

跟UIResponder相關(guān)其他值得注意的地方

  • UIApplication對象和每個UIWindow對象都在sendEvent:方法(兩個類都聲明了這個方法)中派發(fā)事件失暴。由于這些方法是事件進(jìn)入應(yīng)用程序的通道坯门,所以,您可以從UIApplication或UIWindow派生出子類锐帜,重載其sendEvent:方法田盈,實現(xiàn)對事件的監(jiān)控或執(zhí)行特殊的事件處理。但是缴阎,大多數(shù)應(yīng)用程序都不需要這樣做允瞧。

  • 在一定的時間內(nèi)關(guān)閉事件的傳遞。應(yīng)用程序可以調(diào)用UIApplication的beginIgnoringInteractionEvents方法蛮拔,并在隨后調(diào)用endIgnoringInteractionEvents方法來實現(xiàn)這個目的述暂。前一個方法使應(yīng)用程序完全停止接收觸摸事件消息,第二個方法則重啟消息的接收建炫。某些時候畦韭,當(dāng)您的代碼正在執(zhí)行動畫時,可能希望關(guān)閉事件的傳遞肛跌。

  • 在view添加單擊手勢之后艺配,原來的touchesEnded方法就無效了察郁。touchesBegin 還是生效的。

  • 巧妙利用nextResponder獲得當(dāng)前頁面的控制容器

    @implementation UIView (ParentController)
    -(UIViewController*)parentController{
            UIResponder *responder = [self nextResponder];
            while (responder) {
            if ([responder isKindOfClass:[UIViewController class]]) {
              return (UIViewController*)responder;
            }
            responder = [responder nextResponder];
         }
            return nil;
      }
    @end
    
  • inputAccessoryView

我們在使用UITextView和UITextField的時候转唉,可以通過它們的inputAccessoryView屬性給輸入時呼出的鍵盤加一個附屬視圖皮钠,通常是UIToolBar,用于回收鍵盤赠法。 但是當(dāng)我們要操作的視圖不是UITextView或UITextField的時候麦轰,inputAccessoryView就變成了readonly的。 這時我們?nèi)绻€想再加inputAccessoryView砖织,按API中的說法款侵,就需要新建一個該視圖的子類,并重新聲明inputAccessoryView屬性為readwrite的侧纯。比如我們要實現(xiàn)點擊一個tableView的一行時新锈,呼出一個UIPickerView,并且附加一個用于回收PickerView的toolbar眶熬。因此我們自建一個UITableViewCell類壕鹉,并聲明inputAccessoryView和inputView為readwrite的,并且重寫它們的get方法聋涨,這樣在某個tableviewcell變成第一響應(yīng)者時,它就會自動呼出inputView和inputAccessoryView负乡;

  @interface MyTableViewCell : UITableViewCell<UIPickerViewDelegate,UIPickerViewDataSource>
{
    UIToolbar *_inputAccessoryView;
    UIPickerView *_inputView;
}
@property(strong,nonatomic,readwrite) UIToolbar *inputAccessoryView;
@property(strong,nonatomic,readwrite) UIPickerView *inputView;
@end

-(UIToolbar *)inputAccessoryView
{
    if(!_inputAccessoryView)
    {
        UIToolbar *toolBar = [[UIToolbar alloc]initWithFrame:CGRectMake(0, 0, 320, 44)];
//        UIBarButtonItem *right = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonItem target:self action:@selector(dodo)];
    UIBarButtonItem *right = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(dodo)];
    toolBar.items = [NSArray arrayWithObject:right];
    return toolBar;
   }
  return _inputAccessoryView;
}
-(UIPickerView *)inputView
{
    if(!_inputView)
    {
     UIPickerView *  pickView = [[UIPickerView alloc]initWithFrame:CGRectMake(0, 200, 320, 200)];
        pickView.delegate =self;
        pickView.dataSource = self;
        pickView.showsSelectionIndicator = YES;
        return pickView;
    }
    return _inputView;
}

 手動把它變成第一響應(yīng)者牍白。(難道cell被選中時不是第一響應(yīng)者?)
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    [cell becomeFirstResponder];
}

運行結(jié)果:


  • 巧妙重寫pointInside實現(xiàn)點擊圓形區(qū)域判斷
效果圖

實現(xiàn)過程解析:

  • 1.自定義一個View設(shè)置其顏色為橙色抖棘,高度為200茂腥,并設(shè)置

     self.layer.cornerRadius = 100;
     self.clipsToBounds = YES;
    
  • 2.在pointInside中創(chuàng)建一個 UIBezierPath,通過 [path containsPoint: point]來判斷當(dāng)前的點是否在圓內(nèi)切省,pointInside的返回值直接影響到touchesEnded的調(diào)用最岗,如果返回NO是不會調(diào)用touchesEnded事件的。

    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
    {
        UIBezierPath * path = [UIBezierPath bezierPathWithOvalInRect: CGRectMake(0, 0, 200, 200)];
        return [path containsPoint: point];
    }
    
  • 3.在touchesEnded事件中顯示 UIAlertView即可朝捆。

     - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
      {
         UIAlertView * alert = [[UIAlertView alloc] initWithTitle: nil message: @"點擊圓形視圖" delegate: nil cancelButtonTitle: @"確認(rèn)" otherButtonTitles: nil];
          [alert show];
      }
    

參考文章:
iOS開發(fā) - 事件傳遞響應(yīng)鏈
iOS編程中的快遞小哥-Responder Chain(響應(yīng)鏈)
IOS 應(yīng)用事件的傳遞分析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末般渡,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子芙盘,更是在濱河造成了極大的恐慌驯用,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件儒老,死亡現(xiàn)場離奇詭異蝴乔,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)驮樊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門薇正,熙熙樓的掌柜王于貴愁眉苦臉地迎上來片酝,“玉大人,你說我怎么就攤上這事挖腰〉裱兀” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵曙聂,是天一觀的道長晦炊。 經(jīng)常有香客問我,道長宁脊,這世上最難降的妖魔是什么断国? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮榆苞,結(jié)果婚禮上稳衬,老公的妹妹穿的比我還像新娘。我一直安慰自己坐漏,他們只是感情好薄疚,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著赊琳,像睡著了一般街夭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上躏筏,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天板丽,我揣著相機(jī)與錄音,去河邊找鬼趁尼。 笑死埃碱,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的酥泞。 我是一名探鬼主播砚殿,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼芝囤!你這毒婦竟也來了似炎?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤悯姊,失蹤者是張志新(化名)和其女友劉穎名党,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體挠轴,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡传睹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了岸晦。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片欧啤。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡睛藻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出邢隧,到底是詐尸還是另有隱情店印,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布倒慧,位于F島的核電站按摘,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏纫谅。R本人自食惡果不足惜炫贤,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望付秕。 院中可真熱鬧兰珍,春花似錦、人聲如沸询吴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽猛计。三九已至唠摹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間奉瘤,已是汗流浹背跃闹。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留毛好,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓苛秕,卻偏偏與公主長得像肌访,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子艇劫,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

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