iOS事件的傳遞和響應(yīng)

問(wèn)題

  • 事件的產(chǎn)生和傳遞(事件如何從父控件傳遞到子控件并尋找到最合適的view、尋找最合適的view的底層實(shí)現(xiàn)、攔截事件的處理)

  • 找到最合適的view后事件的處理(touches方法的重寫(xiě),也就是事件的響應(yīng))

  • 其中重點(diǎn)和難點(diǎn)是:

    • 1.如何尋找最合適的view
    • 2.尋找最合適的view的底層實(shí)現(xiàn)(hitTest:withEvent:底層實(shí)現(xiàn))

iOS中的事件

  • 觸摸事件 【本文只討論觸摸事件】
  • 加速計(jì)事件
  • 遠(yuǎn)程控制事件
  • 響應(yīng)者對(duì)象(UIResponder)
    在iOS中不是任何對(duì)象都能處理事件,只有繼承了UIResponder的對(duì)象才能接受并處理事件,我們稱之為“響應(yīng)者對(duì)象”。以下都是繼承自UIResponder的,所以都能接收并處理事件。

  • UIViewController

@interface UIViewController : UIResponder <NSCoding, UIAppearanceContainer, UITraitEnvironment, UIContentContainer, UIFocusEnvironment>
  • UIView
@interface UIView : UIResponder <NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, UIFocusItemContainer, CALayerDelegate>

事件的處理往往需要 UITouch 對(duì)象

事件的產(chǎn)生和傳遞

    1. 事件的產(chǎn)生
      點(diǎn)擊一個(gè)UIView或產(chǎn)生一個(gè)觸摸事件A,那么這個(gè)事件就產(chǎn)生了,這個(gè)觸摸事件A會(huì)被添加到由UIApplication管理的事件隊(duì)列中(即,首先接收到事件的是UIApplication,為什么是隊(duì)列而不是棧?因?yàn)殛?duì)列的特點(diǎn)是FIFO,即先進(jìn)先出,先產(chǎn)生的事件先處理才符合常理,所以把事件添加到隊(duì)列)
    1. 事件的傳遞
    • 2.1 觸摸事件的傳遞就是把事件傳遞到處理該事件最合適的view上
      注 意: 如果父控件不能接受觸摸事件,那么子控件就不可能接收到觸摸事件

    • 2.2 如何找到最合適的控件來(lái)處理事件
      1)UIApplication會(huì)從事件隊(duì)列中取出最前面的事件,并將事件分發(fā)下去以便處理,通常,先發(fā)送事件給應(yīng)用程序的主窗口(keyWindow)

      2)判斷主窗口(keyWindow)自己是否能接受觸摸事件,如果能,則判斷觸摸點(diǎn)是否在keyWindow身上,如果在,則UIWindow將事件向下分發(fā),即UIView

      3)判斷觸摸點(diǎn)是否在自己身上,若果在,怎執(zhí)行第4步

      4)子控件數(shù)組中從后往前遍歷子控件,重復(fù)前面的兩個(gè)步驟(所謂從后往前遍歷子控件,就是首先查找子控件數(shù)組中最后一個(gè)元素,然后執(zhí)行3步驟)

      4)view,比如叫做fitView,那么會(huì)把這個(gè)事件交給這個(gè)fitView,再遍歷這個(gè)fitView的子控件,直至沒(méi)有更合適的view為止。

      6)如果沒(méi)有符合條件的子控件,那么就認(rèn)為自己最合適處理這個(gè)事件,也就是自己是最合適的view。

  • 2.3 尋找最合適的view底層剖析

    • 兩個(gè)最重要的方法
      hitTest:withEvent:方法, pointInside方法

      - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
      
      - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
      

    hitTest:withEvent 的說(shuō)明

    • 調(diào)用的條件 :只要事件一傳遞給一個(gè)控件,那么這個(gè)控件就會(huì)調(diào)用自己的這個(gè)方法
    • 作用 :尋找并返回最合適的view 能夠響應(yīng)事件的那個(gè)最合適的view

    注 意:不管這個(gè)控件能不能處理事件,也不管觸摸點(diǎn)在不在這個(gè)控件上,事件都會(huì)先傳遞給這個(gè)控件,隨后再調(diào)用hitTest:withEvent:方法

    • 攔截事件的處理
      正因?yàn)?code>hitTest:withEvent:方法 的返回值為:響應(yīng)事件的那個(gè)最合適的view,所以,通過(guò) 復(fù)寫(xiě) 該方法可以 攔截事件

    新建一個(gè)MView,在.m 文件實(shí)現(xiàn)如下:

      // 開(kāi)始觸摸時(shí)就會(huì)調(diào)用一次這個(gè)方法
      - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
          NSLog(@"摸我干啥!");
      }
      // 手指移動(dòng)就會(huì)調(diào)用這個(gè)方法
      // 這個(gè)方法調(diào)用非常頻繁
      - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
          NSLog(@"哎呀,不要拽人家!");
      }
      // 手指離開(kāi)屏幕時(shí)就會(huì)調(diào)用一次這個(gè)方法
      - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
          NSLog(@"手放開(kāi)還能繼續(xù)玩耍!");
      }
    
      
      - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
      {
          NSLog(@"point.x = %f",point.x);
          NSLog(@"point.y = %f",point.y);
          NSLog(@"%@",event);
          return nil;
      }
    
    

    上述創(chuàng)建的view 對(duì)象點(diǎn)擊 輸出如下

     point.x = 70.000000
     point.y = 48.666672
     <UITouchesEvent: 0x600001df1dd0> timestamp: 1620.54 touches: {()}
    
    point.x = 70.000000
    point.y = 48.666672
    <UITouchesEvent: 0x600001df1dd0> timestamp: 1620.54 touches: {()}
    

    因?yàn)椋?code>hitTest:withEvent:方法中返回nil,那么調(diào)用該方法的控件本身和其子控件都不是最合適的view,也就是在自己身上沒(méi)有找到更合適的view。那么最合適的view就是該控件的父控件。

    • 所以事件的傳遞順序是這樣的
      產(chǎn)生觸摸事件->UIApplication事件隊(duì)列->[UIWindow hitTest:withEvent:]->返回更合適的view->[子控件 hitTest:withEvent:]->返回最合適的view

    • 變形 - 返回 self.superview

      - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
      {
          NSLog(@"point.x = %f",point.x);
          NSLog(@"point.y = %f",point.y);
          NSLog(@"%@",event);
          return self.superview;
      }
    

    輸出與上面的一樣
    總結(jié): 即便父控件是最合適的view了,子控件的hitTest:withEvent:方法還是會(huì)調(diào)用,不然怎么知道有沒(méi)有更合適的!即,如果確定最終父控件是最合適的view,那么該父控件的子控件的hitTest:withEvent:方法也是會(huì)被調(diào)用的。

    • hitTest:withEvent:方法底層實(shí)現(xiàn)
    
      - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
          // 1.判斷下窗口能否接收事件
          if (self.userInteractionEnabled == NO || self.hidden == YES ||  self.alpha <= 0.01) return nil;
          // 2.判斷下點(diǎn)在不在窗口上
          // 不在窗口上
          if ([self pointInside:point withEvent:event] == NO) return nil;
          // 3.從后往前遍歷子控件數(shù)組
          int count = (int)self.subviews.count;
          for (int i = count - 1; i >= 0; i--)     {
              // 獲取子控件
              UIView *childView = self.subviews[i];
              // 坐標(biāo)系的轉(zhuǎn)換,把窗口上的點(diǎn)轉(zhuǎn)換為子控件上的點(diǎn)
              // 把自己控件上的點(diǎn)轉(zhuǎn)換成子控件上的點(diǎn)
              CGPoint childP = [self convertPoint:point toView:childView];
              UIView *fitView = [childView hitTest:childP withEvent:event];
              if (fitView) {
                  // 如果能找到最合適的view
                  return fitView;
              }
          }
          // 4.沒(méi)有找到更合適的view,也就是沒(méi)有比自己更合適的view
          return self;
      }
    
      // 作用:判斷下傳入過(guò)來(lái)的點(diǎn)在不在方法調(diào)用者的坐標(biāo)系上
      // point:是方法調(diào)用者坐標(biāo)系上的點(diǎn)
      //- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
      //{
      // return NO;
      //}
    
    • pointInside:withEvent:方法
      pointInside:withEvent:方法判斷點(diǎn)在不在當(dāng)前view上(方法調(diào)用者的坐標(biāo)系上)
      如果返回YES,代表點(diǎn)在方法調(diào)用者的坐標(biāo)系上;
      返回NO代表點(diǎn)不在方法調(diào)用者的坐標(biāo)系上,那么方法調(diào)用者也就不能處理事件
  • UIView不能接收觸摸事件的三種情況:

    • 不允許交互:userInteractionEnabled = NO
    • 隱藏:如果把父控件隱藏,那么子控件也會(huì)隱藏,隱藏的控件不能接受事件
    • 透明度:如果設(shè)置一個(gè)控件的透明度<0.01,會(huì)直接影響子控件的透明度。alpha:0.0~0.01為透明。
  • 3.事件的響應(yīng)

    • 觸摸事件處理的整體過(guò)程

      • 用戶點(diǎn)擊屏幕后產(chǎn)生的一個(gè)觸摸事件,經(jīng)過(guò)一系列的傳遞過(guò)程后,會(huì)找到最合適的視圖控件來(lái)處理這個(gè)事件
      • 找到最合適的視圖控件后,就會(huì)調(diào)用控件的touches方法來(lái)作具體的事件處理touchesBegan…touchesMoved…touchedEnded…
      • 這些touches方法的默認(rèn)做法是將事件順著響應(yīng)者鏈條向上傳遞(也就是touch方法默認(rèn)不處理事件,只傳遞事件),將事件交給上一個(gè)響應(yīng)者進(jìn)行處理
    • 如何判斷上一個(gè)響應(yīng)者
      如果當(dāng)前這個(gè)view是控制器的view,那么控制器就是上一個(gè)響應(yīng)者
      如果當(dāng)前這個(gè)view不是控制器的view,那么父控件就是上一個(gè)響應(yīng)者

    • 響應(yīng)者鏈的事件傳遞過(guò)程:

      • 如果當(dāng)前view是控制器的view,那么控制器就是上一個(gè)響應(yīng)者,事件就傳遞給控制器;如果當(dāng)前view不是控制器的view,那么父視圖就是當(dāng)前view的上一個(gè)響應(yīng)者,事件就傳遞給它的父視圖
      • 在視圖層次結(jié)構(gòu)的最頂級(jí)視圖,如果也不能處理收到的事件或消息,則其將事件或消息傳遞給window對(duì)象進(jìn)行處理
      • 如果window對(duì)象也不處理,則其將事件或消息傳遞給UIApplication對(duì)象
      • 如果UIApplication也不能處理該事件或消息,則將其丟棄
      //只要點(diǎn)擊控件,就會(huì)調(diào)用touchBegin,如果沒(méi)有重寫(xiě)這個(gè)方法,自己處理不了觸摸事件
      // 上一個(gè)響應(yīng)者可能是父控件
      - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
          // 默認(rèn)會(huì)把事件傳遞給上一個(gè)響應(yīng)者,上一個(gè)響應(yīng)者是父控件,交給父控件處理
          [super touchesBegan:touches withEvent:event];
          // 注意不是調(diào)用父控件的touches方法,而是調(diào)用父類的touches方法
          // super是父類 superview是父控件
      }
    

總結(jié)

  • 事件的傳遞與響應(yīng):
    • 事件的傳遞 當(dāng)一個(gè)事件發(fā)生后,事件會(huì)從父控件傳給子控件,也就是說(shuō)由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的傳遞,也就是尋找最合適的view的過(guò)程。

    • 事件的響應(yīng) 首先看initial view能否處理這個(gè)事件,如果不能則會(huì)將事件傳遞給其上級(jí)視圖(inital view的superView);如果上級(jí)視圖仍然無(wú)法處理則會(huì)繼續(xù)往上傳遞;一直傳遞到視圖控制器view controller,首先判斷視圖控制器的根視圖view是否能處理此事件;如果不能則接著判斷該視圖控制器能否處理此事件,如果還是不能則繼續(xù)向上傳 遞;(對(duì)于第二個(gè)圖視圖控制器本身還在另一個(gè)視圖控制器中,則繼續(xù)交給父視圖控制器的根視圖,如果根視圖不能處理則交給父視圖控制器處理);一直到 window,如果window還是不能處理此事件則繼續(xù)交給application處理,如果最后application還是不能處理此事件則將其丟棄

    • 在事件的響應(yīng)中,如果某個(gè)控件實(shí)現(xiàn)了touches...方法,則這個(gè)事件將由該控件來(lái)接受,如果調(diào)用了[supertouches….];就會(huì)將事件順著響應(yīng)者鏈條往上傳遞,傳遞給上一個(gè)響應(yīng)者;接著就會(huì)調(diào)用上一個(gè)響應(yīng)者的touches….方法

    • 如何做到一個(gè)事件多個(gè)對(duì)象處理:
      因?yàn)橄到y(tǒng)默認(rèn)做法是把事件上拋給父控件,所以可以通過(guò)重寫(xiě)自己的touches方法和父控件的touches方法來(lái)達(dá)到一個(gè)事件多個(gè)對(duì)象處理的目的。

      - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
          // 1.自己先處理事件...
          NSLog(@"do somthing...");
          // 2.再調(diào)用系統(tǒng)的默認(rèn)做法,再把事件交給上一個(gè)響應(yīng)者處理
          [super touchesBegan:touches withEvent:event]; 
      }
      
    • 事件的傳遞和響應(yīng)的區(qū)別:
      事件的傳遞是 從上到下(父控件到子控件)
      事件的響應(yīng)是 從下到上(順著響應(yīng)者鏈條向上傳遞:子控件到父控件。

參考博客

最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • APP通過(guò)響應(yīng)者對(duì)象來(lái)接收和處理事件。響應(yīng)者對(duì)象:只有繼承了UIResponder或者UIView, UIView...
    明若晴空閱讀 489評(píng)論 0 1
  • 響應(yīng)者對(duì)象 在iOS中不是所有對(duì)象都能處理事件,只有繼承UIResponder的對(duì)象才能接收并處理事件,我們稱之為...
    小寶二代閱讀 391評(píng)論 0 1
  • 事件傳遞與響應(yīng)的完整過(guò)程 事件產(chǎn)生>UIUIApplication事件隊(duì)列>主窗口通過(guò)hitTest方法找到最適合...
    CDLOG閱讀 215評(píng)論 0 0
  • 響應(yīng)者對(duì)象:能處理事件的對(duì)象,也就是繼承自UIResponder的對(duì)象。響應(yīng)者鏈?zhǔn)怯啥鄠€(gè)響應(yīng)者對(duì)象連接起來(lái)的鏈條。...
    wg剛閱讀 513評(píng)論 0 2
  • 簡(jiǎn)單分享一下iOS開(kāi)發(fā)中事件在view之間如何傳遞,如何響應(yīng)事件,如有錯(cuò)誤,請(qǐng)大家指正。 一、事件的產(chǎn)生 應(yīng)用外層...
    leesum閱讀 1,296評(píng)論 0 1

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