事件傳遞和響應者鏈條

weixin_33912445發表於2018-09-03

事件處理

  • iOS中的事件可以分為3大型別
    • 觸控事件(MultiTouch events)
    • 加速計事件(Motion events)
    • 遠端控制事件(Remote Control events)
  • 響應者物件(UIResponder)
    • 在iOS中不是任何物件都能處理事件,只有繼承了UIResponder的物件才能接收並處理事件,稱之為響應者物件
    • UIApplication、UIViewController、UIView都繼承自UIResponder,因此它們都是響應者物件,都能夠接收並處理事件
  • UIResponder內部提供了以下方法來處理事件,重點掌握觸控事件
// 觸控事件(重點)
// 一根或者多根手指**開始**觸控view,系統會自動呼叫view的下面方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
// 一根或者多根手指在view上**移動**,系統會自動呼叫view的下面方法(隨著手指的移動,會持續呼叫該方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
// 一根或者多根手指**離開**view,系統會自動呼叫view的下面方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
// 觸控結束前,某個系統事件(例如:電話呼入)會打斷觸控過程,系統會自動呼叫view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

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

// 遠端控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;
  • UITouch
    • 當使用者用一根手指觸控螢幕時,會建立一個與手指相關聯的UITouch物件
    • 一根手指對應一個UITouch物件
    • UITouch的作用
      • 儲存著跟手指相關的資訊,比如觸控的位置、時間、階段
      • 當手指移動時,系統會更新同一個UITouch物件,使之能夠一直儲存該手指的觸控位置
      • 當手指離開螢幕時,系統會銷燬相應的UITouch物件
    • UITouch常用屬性及方法
    // *************************常用屬性***************************
    // 觸控產生時所處的視窗
    @property(nonatomic,readonly,retain) UIWindow    *window;
    // 觸控產生時所處的檢視
    @property(nonatomic,readonly,retain) UIView      *view;
    // 短時間內點按螢幕的次數,可以根據tapCount判斷單擊、雙擊或更多的點選
    @property(nonatomic,readonly) NSUInteger          tapCount;
    // 記錄了觸控事件產生或變化時的時間,單位是秒
    @property(nonatomic,readonly) NSTimeInterval      timestamp;
    // 當前觸控事件所處的狀態
    @property(nonatomic,readonly) UITouchPhase        phase;
    /*
    UITouchPhase是一個列舉型別,包含:
        UITouchPhaseBegan(觸控開始)
        UITouchPhaseMoved(接觸點移動)
        UITouchPhaseStationary(接觸點無移動)
        UITouchPhaseEnded(觸控結束)
        UITouchPhaseCancelled(觸控取消)
    */
    
    // *************************常用方法***************************
    /**
      * 返回值表示觸控在view上的位置
      * 這裡返回的位置是針對view的座標系的(以view的左上角為原點(0, 0))
      * 呼叫時傳入的view引數為nil的話,返回的是觸控點在UIWindow中的位置
     **/
    -(CGPoint)locationInView:(UIView *)view;
    
    // 該方法記錄了前一個觸控點的位置
    -(CGPoint)previousLocationInView:(UIView *)view;
    
  • UIEvent(事件物件)
    • 每產生一個事件,就會產生一個UIEvent物件
    • 作用:記錄事件產生的時刻和型別
// *************************常用屬性***************************
// 事件型別
@property(nonatomic,readonly) UIEventType     type;
// 子事件型別
@property(nonatomic,readonly) UIEventSubtype  subtype;
/*
typedef NS_ENUM(NSInteger, UIEventType) {
    UIEventTypeTouches,// 觸控事件
    UIEventTypeMotion, // 加速計事件
    UIEventTypeRemoteControl,// 遠端控制事件
};

typedef NS_ENUM(NSInteger, UIEventSubtype) {
    // available in iPhone OS 3.0
    UIEventSubtypeNone                              = 0,

    // 加速計事件的子事件型別
    // for UIEventTypeMotion, available in iPhone OS 3.0
    UIEventSubtypeMotionShake                       = 1,

    // 遠端控制事件的子事件型別
    // for UIEventTypeRemoteControl, available in iOS 4.0
    UIEventSubtypeRemoteControlPlay                 = 100,
    UIEventSubtypeRemoteControlPause                = 101,
    UIEventSubtypeRemoteControlStop                 = 102,
    UIEventSubtypeRemoteControlTogglePlayPause      = 103,
    UIEventSubtypeRemoteControlNextTrack            = 104,
    UIEventSubtypeRemoteControlPreviousTrack        = 105,
    UIEventSubtypeRemoteControlBeginSeekingBackward = 106,
    UIEventSubtypeRemoteControlEndSeekingBackward   = 107,
    UIEventSubtypeRemoteControlBeginSeekingForward  = 108,
    UIEventSubtypeRemoteControlEndSeekingForward    = 109,
};
*/

// 事件產生的時間
@property(nonatomic,readonly) NSTimeInterval  timestamp;

// UIEvent還提供了相應的方法可以獲得在某個view上面的觸控物件(UITouch)
- (NSSet *)allTouches;
- (NSSet *)touchesForWindow:(UIWindow *)window;
- (NSSet *)touchesForView:(UIView *)view;
- (NSSet *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture
  • touches和event引數
    • 一次完整的觸控過程,會經歷3個狀態
      • 觸控開始
        • (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
    • 4個觸控事件處理方法中,都有NSSet *touchesUIEvent *event兩個引數
      一次完整的觸控過程中,只會產生一個事件物件,4個觸控方法都是同一個event引數
    • 如果兩根手指同時觸控一個view,那麼view只會呼叫一次touchesBegan:withEvent:方法,touches引數中裝著2個UITouch物件
    • 如果這兩根手指一前一後分開觸控同一個view,那麼view會分別呼叫2次touchesBegan:withEvent:方法,並且每次呼叫時的touches引數中只包含一個UITouch物件
    • 根據touches中UITouch的個數可以判斷出是單點觸控還是多點觸控
  • 注意點:
    • 提示:iPhone開發中,要避免使用雙擊事件!
    • 預設情況不支援多點觸控,需要勾選Multiple Touch選項

觸控事件處理

  • 觸控事件處理的完整過程:
    • 發生觸控事件後,系統會將該事件加入到一個由UIApplication管理的事件佇列
    • UIApplication會從事件佇列中取出最前面的事件,並將事件分發下去以便處理,先傳送事件給應用程式的主視窗
    • 主視窗會在檢視層次結構中找到一個最合適處理事件的控制元件,這也是整個事件處理過程的第一步
    • 找到最合適處理事件的控制元件後,呼叫控制元件的touches...方法,touches...方法的預設做法是將事件順著響應者鏈條向上傳遞,將事件交給上一個響應者進行處理,直至事件處理完畢
  • 觸控事件傳遞
    • 觸控事件的傳遞是從父控制元件傳遞到子控制元件
    • 如果父控制元件不能接收觸控事件,那麼子控制元件就不可能接收到觸控事件
    • 不能接受觸控事件的四種情況
      • 不接收使用者互動,即:userInteractionEnabled = NO
      • 隱藏,即:hidden = YES
      • 透明,即:alpha <= 0.01
      • 未啟用,即:enabled = NO
    • 提示:UIImageView的userInteractionEnabled預設就是NO,因此UIImageView以及它的子控制元件預設是不能接收觸控事件的
    • 如何找到最合適處理事件的控制元件:
      • 首先,判斷自己能否接收觸控事件
        • 可以通過重寫hitTest:withEvent:方法驗證
      • 其次,判斷觸控點是否在自己身上
        • 對應方法pointInside:withEvent:
      • 尋找檢視是從後往前遍歷子控制元件(後新增的子控制元件先遍歷),重複前面的兩個步驟
      • 如果沒有符合條件的子控制元件,那麼就自己處理
  • 響應者鏈條
    • 響應者鏈條:是由多個響應者物件連線起來的鏈條
    • 作用:能很清楚的看見每個響應者之間的聯絡,並且可以讓一個事件可以由多個物件處理
    • 響應者物件:能處理事件的物件
    • 如何判斷上一個響應者
      • 如果當前這個view是控制器的view,那麼控制器就是上一個響應者
      • 如果當前這個view不是控制器的view,那麼父控制元件就是上一個響應者
        611015-bf9a7b41c67941ab.png
        響應者鏈條
      • 注意:繼承於UIControl的控制元件有些特殊,下面用程式碼解釋
      -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
      {
          // 這個事件是不會傳遞給UIButton父控制元件的,因為UIButton本身有處理事件的能力
          // 切記重寫此方法【必須】呼叫父類方法,這樣addTarget設定的監聽的方法【才會被執行】
          [super touchesBegan:touches withEvent:event];
      
          // 如果真想把事件傳遞給父控制元件,可以這樣做
      //    [self.nextResponder touchesBegan:touches withEvent:event];
      }
      
    • 響應者鏈的事件傳遞過程
      • 如果view的控制器存在,就傳遞給控制器;
      • 如果控制器不存在,則將其傳遞給它的父控制元件
      • 在檢視層次結構的最頂層檢視,如果也不能處理收到的事件或訊息,則將事件或訊息傳遞給window物件進行處理
      • 如果window物件也不處理,則將事件或訊息傳遞給UIApplication物件
      • 如果UIApplication也不能處理該事件或訊息,則將其丟棄
  • 驗證事件傳遞和響應者鏈條的預設做法


    611015-72593c3e8fc030b0.png
    事件傳遞過程
  • 模仿系統做法找到最合適的控制元件的做法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    NSLog(@"%@---%s",NSStringFromClass([self class]),__func__);

    // 系統預設做法
//    return [super hitTest:point withEvent:event];

    // 模仿系統做法找到最合適的控制元件的做法
    return [self findTheRightView:point withEvent:event];
}

/**
 *  模仿系統做法找到最合適的控制元件的做法
 */
- (UIView *)findTheRightView:(CGPoint)point withEvent:(UIEvent *)event
{
    // 1.判斷當前控制元件能否接收事件
    if (!self.userInteractionEnabled
        || self.hidden
        || self.alpha <= 0.01) return nil;

    // 2. 判斷點在不在當前控制元件
    if (![self pointInside:point withEvent:event]) return nil;

    // 3.從後往前遍歷自己的子控制元件
    NSInteger count = self.subviews.count;
    for (NSInteger i = count - 1; i >= 0; i--)
    {
        UIView *childView = self.subviews[i];

        // 把當前控制元件上的座標系轉換成子控制元件上的座標系
        CGPoint childP = [self convertPoint:point toView:childView];
        // 遞迴找到最合適處理事件的控制元件
        UIView *rightView = [childView findTheRightView:childP withEvent:event];

        if (rightView) return rightView;
    }

    // 迴圈結束,表示沒有比自己更合適的處理事件的view
    return self;
}
  • hitTest:withEvent:方法
    • 可以重寫這個方法來改變系統預設尋找最合適處理事件的view的處理邏輯(比如可以控制一個按鈕始終是最合適處理事件的控制元件,不管他上面是否有其他覆蓋控制元件)
  • pointInside:withEvent:方法
    • 可以重寫這個方法來改變系統預設判斷點是否在自己身上的邏輯(比如可以通過這個方法來控制手勢解鎖的有效範圍)
  • pointInside:withEvent:方法與CGRectContainsPoint的區別:
    • CGRectContainsPoint用於判斷一個點是否在指定區域,左上角參考點(0,0)必須一致才有可比性
    • pointInside:withEvent:方法用於判斷一個點(相對指定控制元件左上角的(0,0)點確定的座標)是否在指定控制元件內
/**
 *  控制返回最合適處理事件的控制元件
 */
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 將當前座標點轉化為相對按鈕左上角(0,0)座標點
    CGPoint btnPoint = [self convertPoint:point toView:self.btn];

    // CGRectContainsPoint用於判斷一個點是否在指定區域,左上角參考點(0,0)必須一致才有可比性
    // 由於btnPoint已經被轉化為相對按鈕的左上角的座標點了,所以不能用此函式判斷點是否在指定區域內
//    BOOL isContain = CGRectContainsPoint(self.btn.frame, btnPoint);
//    NSLog(@"self.btn.frame-->%@",NSStringFromCGRect(self.btn.frame));
//    NSLog(@"btnPoint-->%@",NSStringFromCGPoint(btnPoint));

    // 只要點在按鈕上就交由按鈕處理對應的事件
    if([self pointInside:btnPoint withEvent:event])
    {
        return self.btn;
    }

    return [super hitTest:point withEvent:event];
}

手勢識別

  • 監聽觸控事件的做法
    • 如果想監聽一個view上面的觸控事件,之前的做法是
      • 自定義一個view
      • 實現view的touches方法,在方法內部實現具體處理程式碼
    • 通過touches方法監聽view觸控事件,有很明顯的幾個缺點
      • 必須得自定義view
      • 由於是在view內部的touches方法中監聽觸控事件,因此預設情況下,無法讓其他外界物件監聽view的觸控事件
      • 不容易區分使用者的具體手勢行為
    • iOS 3.2之後,蘋果推出了手勢識別功能(Gesture Recognizer),在觸控事件處理方面,大大簡化了開發者的開發難度
  • UIGestureRecognizer是一個抽象類,定義了所有手勢的基本行為,使用它的子類才能處理具體的手勢
  • 常見手勢
    • UITapGestureRecognizer(敲擊)
    • UIPinchGestureRecognizer(捏合,用於縮放)
    • UIPanGestureRecognizer(拖拽)
    • UISwipeGestureRecognizer(輕掃)
    • UIRotationGestureRecognizer(旋轉)
    • UILongPressGestureRecognizer(長按)
  • 手勢使用演示
// 建立手勢識別器物件
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] init];
// 連續敲擊2次
tap.numberOfTapsRequired = 2;
// 需要2根手指一起敲擊
tap.numberOfTouchesRequired = 2;
// 新增手勢識別器到對應的view上,那麼就可以在此view應用對應的手勢
[self.iconView addGestureRecognizer:tap];
// 監聽手勢
[tap addTarget:self action:@selector(tapIconView:)];
  • 手勢識別的狀態
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
    // 沒有觸控事件發生,所有手勢識別的預設狀態
    UIGestureRecognizerStatePossible,
    // 一個手勢已經開始但尚未改變或者完成時
    UIGestureRecognizerStateBegan,
    // 手勢狀態改變
    UIGestureRecognizerStateChanged,
    // 手勢完成
    UIGestureRecognizerStateEnded,
    // 手勢取消,恢復至Possible狀態
    UIGestureRecognizerStateCancelled,
    // 手勢失敗,恢復至Possible狀態
    UIGestureRecognizerStateFailed,

    UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
};
  • 同時支援多個手勢是通過代理完成
    • 設定每個手勢的代理
    • 實現UIGestureRecognizerDelegate協議
// 每個手勢設定代理
// 是否允許同時支援多個手勢,預設是不支援多個手勢
// 返回yes表示支援多個手勢
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}
  • 注意點:
    • 如果以後想要一個控制元件支援多個方向的輕掃,必須建立多個輕掃手勢,一個輕掃手勢只支援一個方向(但是同時設定左右方向是能同時支援,同時設定上下方向也是支援的,但是最好是建立多個清掃手勢來支援)
    • 旋轉手勢,記得復位
    • 縮放手勢,記得復位
    • 拖拽手勢,記得復位
#pragma mark - 旋轉手勢
- (void)setUpRotation
{
    UIRotationGestureRecognizer *rotation = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(rotation:)];
    rotation.delegate = self;
    [self.imageView addGestureRecognizer:rotation];
}

// 預設傳遞的旋轉的角度都是相對於最開始的位置
- (void)rotation:(UIRotationGestureRecognizer *)rotation
{
    self.imageView.transform = CGAffineTransformRotate(self.imageView.transform, rotation.rotation);

    // 復位,這個很重要!!!
    rotation.rotation = 0;
}

#pragma mark - 捏合
- (void)setUpPinch
{
    UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinch:)];
    pinch.delegate = self;
    [self.imageView addGestureRecognizer:pinch];
}

- (void)pinch:(UIPinchGestureRecognizer *)pinch
{
    self.imageView.transform = CGAffineTransformScale(self.imageView.transform, pinch.scale, pinch.scale);

    // 復位,這個很重要!!!
    pinch.scale = 1;
}

#pragma mark - 拖拽
- (void)setUpPan
{
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];

    pan.delegate = self;

    [self.imageView addGestureRecognizer:pan];
}

- (void)pan:(UIPanGestureRecognizer *)pan
{
    // 移動檢視
    // 獲取手勢的移動,也是相對於最開始的位置
    CGPoint transP = [pan translationInView:self.imageView];

    self.imageView.transform = CGAffineTransformTranslate(self.imageView.transform, transP.x, transP.y);

    // 復位,這個很重要!!!
    [pan setTranslation:CGPointZero inView:self.imageView];
}

相關文章