版權聲明

所有的部落格文章都可以在右邊[blog文章原始檔案]下載最原始的文字檔案,並依你高興使用 docutil 工具轉換成任何對應的格式方便離線閱覽,除了集結成書販賣歡迎任意取用,引用

iPhone Multi-touch Event Handling

iPhone Multi-touch Event Handling

Type & SubType

Type Description
UIEventTypeTouches 觸碰事件
UIEventTypeMotion 位移感測事件
UIEventTypeRemoteControl Remote Control event
SubType Description
UIEventSubtypeNone Touch event always has this subtype
UIEventSubtypeMotionShake  
UIEventSubtypeRemoteControlPlay  
UIEventSubtypeRemoteControlPause  
UIEventSubtypeRemoteControlStop  
UIEventSubtypeRemoteControlTooglePlayPause  
UIEventSubtypeRemoteControlNextTrack  
UIEventSubtypeRemoteControlPreviousTrack  
UIEventSubtypeRemoteControlBeginSeekingBackward  
UIEventSubtypeRemoteControlEndSeekingBackward  
UIEventSubtypeRemoteControlBeginSeekingForward  
UIEventSubtypeRemoteControlEndSeekingForward  

* Never retain UIEvent

Responder object : An object that can respond to events and handle them。 UIResponder is the base class for responder objects。

First responder: The responder object will first receive events

First responder 會收到下面4種events

  1. Motion events
  2. Remote-control events
  3. Action message (which no target is specified)
  4. Editing-menu message (copy,cut,paste)

UIKit 會自動指定 text view or text field 在使用者觸碰後成為 first responder 。

Touch events 的流程

http://s3.amazonaws.com/ember/GiKDcx86KiRboA2PPzaTXEcrOprQvlLt_m.png

application 收到 touch event 後開始對 views 進行 hit test (-hitTest:withEvent:), - hitTest:withEvent: 做的事情就是針對整個 subviews 架構遞迴送出 -pointInside:wihtEvent: 最末端的 subview 即為 first responder, - hitTest:withEvent: 傳回 first responder。 user interaction off 和 alpha < 0.01 的 views 會忽略不測試 (內容透明不在此限)。

application 將 event 送至 hitTest:withEvent: 傳回的 view。

  1. 假如有 view controller 就把event送至view controller,如果沒有送往 super view
  2. 如過 view controller 不處理收到的 event 也會送往 super view
  3. 循環步驟1和2直到最頂層 window ,如果 window 也不處理就送往 application

Multitouch Events

Cocoa touch 將觸碰事件分成幾個不同的階段(Phase) touch begin, touch move, touch end 和 touch cancel。

可以利用 subclass override 下面 methods 來處理 multitouch event。

如果 responder 想要處理 multitouch event , 請確認 multipleTouchEnable property is YES。

- (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;

可以透過參數傳進的 UIEvent 物件包含的 UITouch 物件中的 phase property 了解 touch event 屬於 哪個 phase。

typedef enum {
    UITouchPhaseBegan, // one or more fingers touch down
    UITouchPhaseMoved,
    UITouchPhaseStationary,
    UITouchPhaseEnded, // one or more fingers lift up
    UITouchPhaseCancelled,
} UITouchPhase;

管理 Multitouch events

Enable/Disable multitouch Event:

UIView
userInteractionEnabled property multipleTouchEnabled property exclusiveTouch
UIApplication
- beginIgnoringInteractionEvents - endIgnoringInteractionEvents

UITouch 的常用資訊

[touch locationInView:view];
[touch previousLocationInView:view];
// @property(nonatomic,readonly) NSUInteger tapCount
touch.tapCount;
// @property(nonatomic, readonly) UITouchPhase phase
touch.phase;
// @property(nonatomic, readonly) NSTimeInterval timestamp
touch.timestamp;

Example process tab

code example 處理 tab 和 double tab,示範如何在 subclass override touches handler method 來處理 tab 和 double tab。

由於 UITouch 沒有實作 NSCopying protocol 所以無法透過 copy 的手段將內容保存 以供後續使用,也不能用 retain 的方式保留 reference 因為其內容隨時可能因為 touch event 的狀態改變而改變,我們只能針對有興趣的部分加以保存,本例中使用 NSValue 包裝 locationInView 後將值加入 dictionary 中。

請注意範例對於 dictionary 物件並沒有妥善的記憶體管理,我有看 retainCount 的內容 為 1 說明應該必須要正確的處理記憶體釋放,不過 Build & Analyze 並沒有檢查出來?

這個範例利用 performSelector:withObject:afterDelay: 並設定0.3來判斷是否 double tab,如果使用者0.3秒內沒有再次tab,則送出設定的selector,反之在 touchesBegan:withEvent: 利用 tapCount property 判斷是否按下第二次,如果 按下第二次就取消待命中的 handleSingleTap:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *aTouch = [touches anyObject];
    if (aTouch.tapCount == 2) {
        // 取消待命中的 perform request
        [NSObject cancelPreviousPerformRequestsWithTarget:self];
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    /* no state to clean up, so null implementation */
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *theTouch = [touches anyObject];
    if (theTouch.tapCount == 1) {
        NSDictionary *touchLoc = [NSDictionary dictionaryWithObject:
            [NSValue valueWithCGPoint:[theTouch locationInView:self]] forKey:@"location"];
        [self performSelector:@selector(handleSingleTap:) withObject:touchLoc afterDelay:0.3];
    } else if (theTouch.tapCount == 2) {
        // Double-tap: increase image size by 10%"
        [self handleDoubleTap];
    }
}

- (void) handleSingleTap:(id)object
{
    // 單擊處理
}

- (void) handleDoubleTap
{
    // 雙擊處理
}

Super View 攔截 sub View 的 touch event

Multitouch Event 傳遞路徑事從最底層的sub view往上(Super View)傳遞,當 event 沒有處理時會順的 responder chain 往上層送,有時候我們想要處理某個 view 特定的 touch event,但是無法或是不想去修改View卻想處理特定的 touch event 時該怎麼辦?

最簡單的方法是 Override hitTest:

下面範例中 InterceptTouchView 會改寫 hitTest: withEvent: 使自己成為 first responder, 確保可以接收所有的 touch events。

@protocol InterceptDelegate
- (void)interceptView:(UIView *)view withTapPoint:(CGPoint)point;
@end

@interface InterceptTouchView : UIView {
    UIView *viewTouched;
    id <InterceptDelegate> delegate;

}

// Override hitTest:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    viewTouched = [super hitTest:point withEvent:event];
    if ([viewTouched isDescendantOfView:self] ) {
        return self;
    }
    return viewTouched;
}



- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"InterceptTouchview touches began!");
    if (viewTouched) {
        [viewTouched touchesBegan:touches withEvent:event];
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"InterceptTouchview touches move");
    if (viewTouched) {
        [viewTouched touchesMoved:touches withEvent:event];
    }
}

// 透過 delegate 觸發事件
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
    CGPoint point;
    point = [[touches anyObject] locationInView:self];
    NSLog(@"point = %f %f",point.x,point.y);
    [delegate interceptView:self withTapPoint:point];

}

Forwarding Touch Events

自組 touch event 送給特定的 view 也是一個十分有用的技巧, cocoa 由於 UITouch 有一個 view property 記錄 touch event 的責任 view object, 所以如果要轉發或是發送自製的 touch event ,必須要處理 UITouch.view。

不論是改變 touch event 或是增加額外的處理,大概不脫三種方式

  1. Override overlay view's hitTest: , 上衣個章節就是採用這種作法。
  2. Override sendEvent: in a custom subclass of UIWinodw。
  3. Custom subclass of view

下面例子討論 override sendEvent:

我們都知道 UIWindow 有兩個主要的任務:

  1. 安排組織他的 subviews 並呈現到螢幕上面。
  2. 分送 event 給 views。
// Overrid sendEvent
- (void)sendEvent:(UIEvent *)event
{
    for (TransformGesture *gesture in transformGestures) {
        // collect all the touches we care about from the event
        NSSet *touches = [gesture observedTouchesForEvent:event];
        NSMutableSet *began = nil;
        NSMutableSet *moved = nil;
        NSMutableSet *ended = nil;
        NSMutableSet *cancelled = nil;

        // sort the touches by phase so we can handle them similarly to normal event dispatch
        for(UITouch *touch in touches) {
            switch ([touch phase]) {
                case UITouchPhaseBegan:
                    if (!began) began = [NSMutableSet set];
                    [began addObject:touch];
                    break;
                case UITouchPhaseMoved:
                    if (!moved) moved = [NSMutableSet set];
                    [moved addObject:touch];
                    break;
                case UITouchPhaseEnded:
                    if (!ended) ended = [NSMutableSet set];
                    [ended addObject:touch];
                    break;
                case UITouchPhaseCancelled:
                    if (!cancelled) cancelled = [NSMutableSet set];
                    [cancelled addObject:touch];
                    break;
                default:
                    break;
            }
        }
        // call our methods to handle the touches
        if (began)     [gesture touchesBegan:began withEvent:event];
        if (moved)     [gesture touchesMoved:moved withEvent:event];
        if (ended)     [gesture touchesEnded:ended withEvent:event];
        if (cancelled) [gesture touchesCancelled:cancelled withEvent:event];       }
    [super sendEvent:event];
}

Gesture Recognizer

Gesture Recognizer 提供了一種低耦合處理touch event的方式,透過 gesture recognizer 我們可以輕易的將複雜的手勢辨識和後續處理和 view 的程式碼分開。

參考下圖說明了 Gesture Recognizer 的動作原理, Gesture Recognizer 是一種 observer 變體,單獨的 Gesture Recognizer 沒有什麼用處,必須透過 UIView 的 - addGestureRecognizer: attach後才有作用, Gesture Recognizer attached 後會觀察傳送到 view 的 touch event 如果出現設定好的手勢,則透過 target/action 機制,送出 action message。

http://s3.amazonaws.com/ember/RhLan3XJuO5Xtv4TctgSH7jaGqOzmbsH_m.png

下圖為 UIGestureRecognizer 的class體系,UIGestureRecognizer 為 abstract base class。

http://s3.amazonaws.com/ember/uXldoVbTaJREeZF53aKpRwmVEy1iXSBi_m.png

decret gesture 如 tap 只會送一次 action message。 continuous gesture 如 pinch 會連續送出 action messages。

Default Touch-Event Delivery

view attached gesture recognizer 後 touch event 如何傳遞? gesture recognizer 如何監控 touch event?

window 在 Began phase (UITouchPhaseBegan) 和 Moved phase (UITouchPhaseMoved) 會將 touch event 分別送給 gesture recognizer 和 hit-test view。

到了 Ended phase window 會延緩送至 view 的 touch event 先給 gesture recognizer 如果 gesture recognizer 分析後認定是要處理的手勢,會進行下列處理:

  1. set state=UIGestureRecognizerStateRecognized;
  2. send touchesCancelled:withEvent: 至 hit-test view。取消 hit-test view 的 touch 處理。

如果 Ended pahase Gesture Recognizer 確認不是要處理的手勢,動作變成:

  1. set state=UIGestureRecognizerStateFailed;
  2. send touchesEnded:withEvent: 至 hit-test view。

Continuous gesture 動作差不多,只是在 Ended phase 之前 Gesture Recognizer 就會 將 state 設為 UIGestureRecognizerStateBegan,並開始送出 action message。

Cancel or not

上個段落討論了 gesture recognizer 針對 touch events 處理的流程,可以了解到預設 的流程中, gesture recognizer 認出手勢後 (Ended phase) 就會送出 touchesCancelled:withEvent: 取消 hit-test view 的 touch 處理行為,這代表 hit-test view 喪失了對於 touch events 的反應機會。

有時候我們只想針對某個手勢做出一些特定的處理而不影響原始設計,這時可以利用 Gesture Recognizer 提供 的一些 properties 來改變這些預設行為:

  • cancelsTouchesInView (default of YES)
  • delaysTouchesBegan (default of NO)
  • delaysTouchesEnded (default of YES)

cancelsTouchesInView 設成 NO 會取消 Gesture Recognizer 在 Ended phase 對 hit-test view 送出的 touchesCancelled:withEvent: ,也就是說 hit-test view 會依照原始設計的行為處理 touch event。

delaysTouchesBegan 設定為 YES 則所有的 touch event 都不會送到 hit-test view,都會先由 Gesture Recognizer 處理,類似 scroll view 的 delaysContentTouches property,設定後 sub view 就不會收到 touch events 。

delaysTouchesEnded 預設值為 YES , 防止 hit-test view 收到 touchesEnded:withEvent , 比如我們要處理 double tap 手勢,我們必須要設定 UITapGestureRecognizer 的 numberOfTapsRequired = 2 ,如果 delaysTouchesEnded = NO, hit-test view 收到的 touch event 順序為: touchesBegan:withEvent: , touchesEnded:withEvent:, touchesBegan:withEvent: touchesCancelled:withEvent: 如果設為YES則 hit-test view 收到的 touch event 為: touchesBegan:withEvent: , touchesBegan:withEvent:, touchesCancelled:withEvent: touchesCancelled:withEvent: 十分清楚的 delaysTouchesEnded 設為 YES 有效避免 hit-test view 收到完整的 touch event 序列。

If you fail I do

一個 view 可以貼上許多個不同的 Gesture Recognizers , 這些 Gesture Recognizers 可以建立一種簡單的先後關係,"如果你沒有看到想要的手勢,我在登場" 用程式的語言可以這樣說: 如果 Gesture Recognizer A fail 的情形下 Gesture Recognizer B 才接手。

- requireGestureRecognizerToFail:

target/action

UITapGestureRecognizer *tapRecognizer =
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap)];

Gesture Reconizer 可以送出多個 action message,要增加額外的 target 請參考下例:

[tapRecognizer addTarget:frameView action:@selector(handleTap)];

Attach/Remove gesture recognizer

// Attach (retain 會+1)
[view addGestureRecognizer:tapRecognizer];
// remove
[view removeGestureRecognizer:tapRecognizer];
// UIView property
@property(nonatomic,readonly) NSArray *gestureRecognizers

States

下圖說明四種不同 Gesture Recognizer 狀態轉移的情形。 雖然一樣有 Began Ended Cancelled 狀態,但這些和 multitouch phase 沒什麼關連。

http://s3.amazonaws.com/ember/YdUvvVQV5xBShQ2XV3F8qUs9GSi0hL3T_m.png

Custom Gusture Recognizer

// .h 檔裡面增加下面東西
#import <UIKit/UIGestureRecognizerSubclass.h>

- (void)reset;
- (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;

// Implement sample
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    if ([touches count] != 1) {
        self.state = UIGestureRecognizerStateFailed;
        return;
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesMoved:touches withEvent:event];
    if (self.state == UIGestureRecognizerStateFailed) return;
    CGPoint nowPoint = [[touches anyObject] locationInView:self.view];
    CGPoint prevPoint = [[touches anyObject] previousLocationInView:self.view];
    if (!strokeUp) {
        // on downstroke, both x and y increase in positive direction
        if (nowPoint.x >= prevPoint.x && nowPoint.y >= prevPoint.y) {
            self.midPoint = nowPoint;
            // upstroke has increasing x value but decreasing y value
        } else if (nowPoint.x >= prevPoint.x && nowPoint.y <= prevPoint.y) {
            strokeUp = YES;
        } else {
            self.state = UIGestureRecognizerStateFailed;
        }
    }
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesEnded:touches withEvent:event];
    if ((self.state == UIGestureRecognizerStatePossible) && strokeUp) {
        self.state = UIGestureRecognizerStateRecognized;
    }
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesCancelled:touches withEvent:event];
    self.midPoint = CGPointZero;
    strokeUp = NO;
    self.state = UIGestureRecognizerStateFailed;
}

沒有留言:

Related Posts with Thumbnails