版本記錄
| 版本號 | 時間 |
|---|---|
| V1.0 | 2017.12.10 |
前言
APP中的視圖就是進行人機交互的,各種點擊等手勢都是通過與屏幕視圖進行交互完成的。這里面很重要的一個就是HitTest方法,它用于尋找響應(yīng)者以及傳遞事件,接下來我們就一起研究下HitTest方法的調(diào)用機制。
hitTest方法
hitTest:withEvent:是UIView的一個方法,該方法會被系統(tǒng)調(diào)用,作用就是用于選擇最合適的視圖來響應(yīng)觸摸事件。
事件的聲明周期
每一個事件從產(chǎn)生到結(jié)束是有一個過程的,也就是事件的聲明周期。下面看一張圖。

幾個重要的類
1. UITouch
- 一個手指一次觸摸屏幕,就對應(yīng)生成一個UITouch對象。多個手指同時觸摸,生成多個UITouch對象。
- 多個手指先后觸摸,系統(tǒng)會根據(jù)觸摸的位置判斷是否更新同一個UITouch對象。若兩個手指一前一后觸摸同一個位置(即雙擊),那么第一次觸摸時生成一個UITouch對象,第二次觸摸更新這個UITouch對象(UITouch對象的 tap count 屬性值從1變成2);若兩個手指一前一后觸摸的位置不同,將會生成兩個UITouch對象,兩者之間沒有聯(lián)系。
- 每個UITouch對象記錄了觸摸的一些信息,包括觸摸時間、位置、階段、所處的視圖、窗口等信息。
- 手指離開屏幕一段時間后,確定該UITouch對象不會再被更新將被釋放。
/#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>
#import <UIKit/UIKitDefines.h>
NS_ASSUME_NONNULL_BEGIN
@class UIWindow, UIView, UIGestureRecognizer;
typedef NS_ENUM(NSInteger, UITouchPhase) {
UITouchPhaseBegan, // whenever a finger touches the surface.
UITouchPhaseMoved, // whenever a finger moves on the surface.
UITouchPhaseStationary, // whenever a finger is touching the surface but hasn't moved since the previous event.
UITouchPhaseEnded, // whenever a finger leaves the surface.
UITouchPhaseCancelled, // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face)
};
typedef NS_ENUM(NSInteger, UIForceTouchCapability) {
UIForceTouchCapabilityUnknown = 0,
UIForceTouchCapabilityUnavailable = 1,
UIForceTouchCapabilityAvailable = 2
};
typedef NS_ENUM(NSInteger, UITouchType) {
UITouchTypeDirect, // A direct touch from a finger (on a screen)
UITouchTypeIndirect, // An indirect touch (not a screen)
UITouchTypeStylus NS_AVAILABLE_IOS(9_1), // A touch from a stylus
} NS_ENUM_AVAILABLE_IOS(9_0);
typedef NS_OPTIONS(NSInteger, UITouchProperties) {
UITouchPropertyForce = (1UL << 0),
UITouchPropertyAzimuth = (1UL << 1),
UITouchPropertyAltitude = (1UL << 2),
UITouchPropertyLocation = (1UL << 3), // For predicted Touches
} NS_AVAILABLE_IOS(9_1);
NS_CLASS_AVAILABLE_IOS(2_0) @interface UITouch : NSObject
@property(nonatomic,readonly) NSTimeInterval timestamp;
@property(nonatomic,readonly) UITouchPhase phase;
@property(nonatomic,readonly) NSUInteger tapCount; // touch down within a certain point within a certain amount of time
@property(nonatomic,readonly) UITouchType type NS_AVAILABLE_IOS(9_0);
// majorRadius and majorRadiusTolerance are in points
// The majorRadius will be accurate +/- the majorRadiusTolerance
@property(nonatomic,readonly) CGFloat majorRadius NS_AVAILABLE_IOS(8_0);
@property(nonatomic,readonly) CGFloat majorRadiusTolerance NS_AVAILABLE_IOS(8_0);
@property(nullable,nonatomic,readonly,strong) UIWindow *window;
@property(nullable,nonatomic,readonly,strong) UIView *view;
@property(nullable,nonatomic,readonly,copy) NSArray <UIGestureRecognizer *> *gestureRecognizers NS_AVAILABLE_IOS(3_2);
- (CGPoint)locationInView:(nullable UIView *)view;
- (CGPoint)previousLocationInView:(nullable UIView *)view;
// Use these methods to gain additional precision that may be available from touches.
// Do not use precise locations for hit testing. A touch may hit test inside a view, yet have a precise location that lies just outside.
- (CGPoint)preciseLocationInView:(nullable UIView *)view NS_AVAILABLE_IOS(9_1);
- (CGPoint)precisePreviousLocationInView:(nullable UIView *)view NS_AVAILABLE_IOS(9_1);
// Force of the touch, where 1.0 represents the force of an average touch
@property(nonatomic,readonly) CGFloat force NS_AVAILABLE_IOS(9_0);
// Maximum possible force with this input mechanism
@property(nonatomic,readonly) CGFloat maximumPossibleForce NS_AVAILABLE_IOS(9_0);
// Azimuth angle. Valid only for stylus touch types. Zero radians points along the positive X axis.
// Passing a nil for the view parameter will return the azimuth relative to the touch's window.
- (CGFloat)azimuthAngleInView:(nullable UIView *)view NS_AVAILABLE_IOS(9_1);
// A unit vector that points in the direction of the azimuth angle. Valid only for stylus touch types.
// Passing nil for the view parameter will return a unit vector relative to the touch's window.
- (CGVector)azimuthUnitVectorInView:(nullable UIView *)view NS_AVAILABLE_IOS(9_1);
// Altitude angle. Valid only for stylus touch types.
// Zero radians indicates that the stylus is parallel to the screen surface,
// while M_PI/2 radians indicates that it is normal to the screen surface.
@property(nonatomic,readonly) CGFloat altitudeAngle NS_AVAILABLE_IOS(9_1);
// An index which allows you to correlate updates with the original touch.
// Is only guaranteed non-nil if this UITouch expects or is an update.
@property(nonatomic,readonly) NSNumber * _Nullable estimationUpdateIndex NS_AVAILABLE_IOS(9_1);
// A set of properties that has estimated values
// Only denoting properties that are currently estimated
@property(nonatomic,readonly) UITouchProperties estimatedProperties NS_AVAILABLE_IOS(9_1);
// A set of properties that expect to have incoming updates in the future.
// If no updates are expected for an estimated property the current value is our final estimate.
// This happens e.g. for azimuth/altitude values when entering from the edges
@property(nonatomic,readonly) UITouchProperties estimatedPropertiesExpectingUpdates NS_AVAILABLE_IOS(9_1);
@end
NS_ASSUME_NONNULL_END
2. UIEvent
這個是事件的真身。觸摸的目的就是生成觸摸事件供響應(yīng)者響應(yīng),一個觸摸事件對應(yīng)一個UIEvent對象,其中的type屬性標(biāo)識了事件的類型。如果一個觸摸事件可能是由多個手指同時觸摸產(chǎn)生的,觸摸對象集合通過allTouches屬性獲取。
#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>
#import <UIKit/UIKitDefines.h>
NS_ASSUME_NONNULL_BEGIN
@class UIWindow, UIView, UIGestureRecognizer, UITouch;
typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches,
UIEventTypeMotion,
UIEventTypeRemoteControl,
UIEventTypePresses NS_ENUM_AVAILABLE_IOS(9_0),
};
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,
};
NS_CLASS_AVAILABLE_IOS(2_0) @interface UIEvent : NSObject
@property(nonatomic,readonly) UIEventType type NS_AVAILABLE_IOS(3_0);
@property(nonatomic,readonly) UIEventSubtype subtype NS_AVAILABLE_IOS(3_0);
@property(nonatomic,readonly) NSTimeInterval timestamp;
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches;
#else
- (nullable NSSet <UITouch *> *)allTouches;
#endif
- (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window;
- (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view;
- (nullable NSSet <UITouch *> *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture NS_AVAILABLE_IOS(3_2);
// An array of auxiliary UITouch’s for the touch events that did not get delivered for a given main touch. This also includes an auxiliary version of the main touch itself.
- (nullable NSArray <UITouch *> *)coalescedTouchesForTouch:(UITouch *)touch NS_AVAILABLE_IOS(9_0);
// An array of auxiliary UITouch’s for touch events that are predicted to occur for a given main touch. These predictions may not exactly match the real behavior of the touch as it moves, so they should be interpreted as an estimate.
- (nullable NSArray <UITouch *> *)predictedTouchesForTouch:(UITouch *)touch NS_AVAILABLE_IOS(9_0);
@end
NS_ASSUME_NONNULL_END
3. UIReponder
先看一下API
#import <Foundation/Foundation.h>
#import <UIKit/UIKitDefines.h>
#import <UIKit/UIEvent.h>
#import <UIKit/UIPasteConfigurationSupporting.h>
NS_ASSUME_NONNULL_BEGIN
@class UIPress;
@class UIPressesEvent;
@protocol UIResponderStandardEditActions <NSObject>
@optional
- (void)cut:(nullable id)sender NS_AVAILABLE_IOS(3_0);
- (void)copy:(nullable id)sender NS_AVAILABLE_IOS(3_0);
- (void)paste:(nullable id)sender NS_AVAILABLE_IOS(3_0);
- (void)select:(nullable id)sender NS_AVAILABLE_IOS(3_0);
- (void)selectAll:(nullable id)sender NS_AVAILABLE_IOS(3_0);
- (void)delete:(nullable id)sender NS_AVAILABLE_IOS(3_2);
- (void)makeTextWritingDirectionLeftToRight:(nullable id)sender NS_AVAILABLE_IOS(5_0);
- (void)makeTextWritingDirectionRightToLeft:(nullable id)sender NS_AVAILABLE_IOS(5_0);
- (void)toggleBoldface:(nullable id)sender NS_AVAILABLE_IOS(6_0);
- (void)toggleItalics:(nullable id)sender NS_AVAILABLE_IOS(6_0);
- (void)toggleUnderline:(nullable id)sender NS_AVAILABLE_IOS(6_0);
- (void)increaseSize:(nullable id)sender NS_AVAILABLE_IOS(7_0);
- (void)decreaseSize:(nullable id)sender NS_AVAILABLE_IOS(7_0);
@end
NS_CLASS_AVAILABLE_IOS(2_0) @interface UIResponder : NSObject <UIResponderStandardEditActions>
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
#else
- (nullable UIResponder*)nextResponder;
#endif
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly) BOOL canBecomeFirstResponder; // default is NO
#else
- (BOOL)canBecomeFirstResponder; // default is NO
#endif
- (BOOL)becomeFirstResponder;
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly) BOOL canResignFirstResponder; // default is YES
#else
- (BOOL)canResignFirstResponder; // default is YES
#endif
- (BOOL)resignFirstResponder;
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly) BOOL isFirstResponder;
#else
- (BOOL)isFirstResponder;
#endif
// Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application. Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);
// Generally, all responders which do custom press handling should override all four of these methods.
// Your responder will receive either pressesEnded:withEvent or pressesCancelled:withEvent: for each
// press it is handling (those presses it received in pressesBegan:withEvent:).
// pressesChanged:withEvent: will be invoked for presses that provide an analog value
// (like thumbsticks or analog push buttons)
// *** You must handle cancelled presses to ensure correct behavior in your application. Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);
- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender NS_AVAILABLE_IOS(3_0);
// Allows an action to be forwarded to another target. By default checks -canPerformAction:withSender: to either return self, or go up the responder chain.
- (nullable id)targetForAction:(SEL)action withSender:(nullable id)sender NS_AVAILABLE_IOS(7_0);
@property(nullable, nonatomic,readonly) NSUndoManager *undoManager NS_AVAILABLE_IOS(3_0);
@end
typedef NS_OPTIONS(NSInteger, UIKeyModifierFlags) {
UIKeyModifierAlphaShift = 1 << 16, // This bit indicates CapsLock
UIKeyModifierShift = 1 << 17,
UIKeyModifierControl = 1 << 18,
UIKeyModifierAlternate = 1 << 19,
UIKeyModifierCommand = 1 << 20,
UIKeyModifierNumericPad = 1 << 21,
} NS_ENUM_AVAILABLE_IOS(7_0);
NS_CLASS_AVAILABLE_IOS(7_0) @interface UIKeyCommand : NSObject <NSCopying, NSSecureCoding>
- (instancetype)init NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
@property (nullable,nonatomic,readonly) NSString *input;
@property (nonatomic,readonly) UIKeyModifierFlags modifierFlags;
@property (nullable,nonatomic,copy) NSString *discoverabilityTitle NS_AVAILABLE_IOS(9_0);
// The action for UIKeyCommands should accept a single (id)sender, as do the UIResponderStandardEditActions above
// Creates an key command that will _not_ be discoverable in the UI.
+ (UIKeyCommand *)keyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)modifierFlags action:(SEL)action;
// Key Commands with a discoverabilityTitle _will_ be discoverable in the UI.
+ (UIKeyCommand *)keyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)modifierFlags action:(SEL)action discoverabilityTitle:(NSString *)discoverabilityTitle NS_AVAILABLE_IOS(9_0);
@end
@interface UIResponder (UIResponderKeyCommands)
@property (nullable,nonatomic,readonly) NSArray<UIKeyCommand *> *keyCommands NS_AVAILABLE_IOS(7_0); // returns an array of UIKeyCommand objects<
@end
@class UIInputViewController;
@class UITextInputMode;
@class UITextInputAssistantItem;
@interface UIResponder (UIResponderInputViewAdditions)
// Called and presented when object becomes first responder. Goes up the responder chain.
@property (nullable, nonatomic, readonly, strong) __kindof UIView *inputView NS_AVAILABLE_IOS(3_2);
@property (nullable, nonatomic, readonly, strong) __kindof UIView *inputAccessoryView NS_AVAILABLE_IOS(3_2);
/// This method is for clients that wish to put buttons on the Shortcuts Bar, shown on top of the keyboard.
/// You may modify the returned inputAssistantItem to add to or replace the existing items on the bar.
/// Modifications made to the returned UITextInputAssistantItem are reflected automatically.
/// This method should not be overriden. Goes up the responder chain.
@property (nonnull, nonatomic, readonly, strong) UITextInputAssistantItem *inputAssistantItem NS_AVAILABLE_IOS(9_0) __TVOS_PROHIBITED __WATCHOS_PROHIBITED;
// For viewController equivalents of -inputView and -inputAccessoryView
// Called and presented when object becomes first responder. Goes up the responder chain.
@property (nullable, nonatomic, readonly, strong) UIInputViewController *inputViewController NS_AVAILABLE_IOS(8_0);
@property (nullable, nonatomic, readonly, strong) UIInputViewController *inputAccessoryViewController NS_AVAILABLE_IOS(8_0);
/* When queried, returns the current UITextInputMode, from which the keyboard language can be determined.
* When overridden it should return a previously-queried UITextInputMode object, which will attempt to be
* set inside that app, but not persistently affect the user's system-wide keyboard settings. */
@property (nullable, nonatomic, readonly, strong) UITextInputMode *textInputMode NS_AVAILABLE_IOS(7_0);
/* When the first responder changes and an identifier is queried, the system will establish a context to
* track the textInputMode automatically. The system will save and restore the state of that context to
* the user defaults via the app identifier. Use of -textInputMode above will supersede use of -textInputContextIdentifier. */
@property (nullable, nonatomic, readonly, strong) NSString *textInputContextIdentifier NS_AVAILABLE_IOS(7_0);
// This call is to remove stored app identifier state that is no longer needed.
+ (void)clearTextInputContextIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(7_0);
// If called while object is first responder, reloads inputView, inputAccessoryView, and textInputMode. Otherwise ignored.
- (void)reloadInputViews NS_AVAILABLE_IOS(3_2);
@end
// These are pre-defined constants for use with the input property of UIKeyCommand objects.
UIKIT_EXTERN NSString *const UIKeyInputUpArrow NS_AVAILABLE_IOS(7_0);
UIKIT_EXTERN NSString *const UIKeyInputDownArrow NS_AVAILABLE_IOS(7_0);
UIKIT_EXTERN NSString *const UIKeyInputLeftArrow NS_AVAILABLE_IOS(7_0);
UIKIT_EXTERN NSString *const UIKeyInputRightArrow NS_AVAILABLE_IOS(7_0);
UIKIT_EXTERN NSString *const UIKeyInputEscape NS_AVAILABLE_IOS(7_0);
@interface UIResponder (ActivityContinuation)
@property (nullable, nonatomic, strong) NSUserActivity *userActivity NS_AVAILABLE_IOS(8_0);
- (void)updateUserActivityState:(NSUserActivity *)activity NS_AVAILABLE_IOS(8_0);
- (void)restoreUserActivityState:(NSUserActivity *)activity NS_AVAILABLE_IOS(8_0);
@end
#if TARGET_OS_IOS
@interface UIResponder (UIPasteConfigurationSupporting) <UIPasteConfigurationSupporting>
@end
#endif
NS_ASSUME_NONNULL_END
我們的UIView、UIViewController、UIApplication、AppDelegate等很多類都繼承它,所以就可以響應(yīng)事件。響應(yīng)事件主要靠下面的幾個方法。
//手指觸碰屏幕,觸摸開始
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指在屏幕上移動
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指離開屏幕,觸摸結(jié)束
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//觸摸結(jié)束前,某個系統(tǒng)事件中斷了觸摸,例如電話呼入
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
尋找合適響應(yīng)視圖的過程
尋找過程
touch->UIApplication->UIWindow->UIViewController.view->subViews->...->view
- 用戶點擊屏幕產(chǎn)生一個觸摸事件,加入到
UIApplication管理的事件隊列中。 -
UIApplication會從事件隊列中取出最早的事件進行分發(fā)處理,先發(fā)送事件給應(yīng)用程序的主窗口UIWindow。 - 主窗口會調(diào)用其
hitTest:withEvent:方法在視圖(UIView)層次結(jié)構(gòu)中找到一個最合適的UIView來處理觸摸事件。
這里需要說明的是,在尋找響應(yīng)UIView過程中大致的原理。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
//系統(tǒng)默認會忽略isUserInteractionEnabled設(shè)置為NO、隱藏、alpha小于等于0.01的視圖
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
if ([self pointInside:point withEvent:event]) {
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return self;
}
return nil;
}
- 首先在當(dāng)前視圖
hitTest方法中調(diào)用pointInside方法,判斷觸摸點是否在當(dāng)前視圖內(nèi)部。- 如果
pointInside方法返回NO,當(dāng)hitTest方法返回的是nil,該視圖不處理該事件。- 如果
pointInside方法返回YES,說明觸摸點在該視圖內(nèi)部,則從最上層的子視圖開始遍歷,為什么從最上層呢?因為subviews數(shù)組后添加的視圖就是在數(shù)組的末尾,遍歷當(dāng)前視圖的所有子視圖,調(diào)用hitTest方法,重復(fù)1 - 3 步驟。- 直到有的子視圖
hitTest方法返回非空對象或者全部子視圖遍歷完畢,則結(jié)束該視圖的循環(huán)遍歷。若第一次有子視圖的hitTest方法返回非空對象,則當(dāng)前視圖的hitTest方法就返回此對象,處理結(jié)束;若所有子視圖的hitTest方法都返回nil,則當(dāng)前視圖的hitTest方法返回當(dāng)前視圖本身,最終由該對象處理觸摸事件。
下面我們就借用一個例子說明一下這個尋找的過程。這里例子是別人寫的,已經(jīng)進行了引用標(biāo)注,在下面列出來參考文章。

這里首先假設(shè):ViewB在ViewC上層,ViewD在ViewE上層,即ViewB的addSubView:執(zhí)行在ViewC之后,ViewD的addSubView:執(zhí)行在ViewE之后。
當(dāng)我們點擊屏幕的時候,也就是ViewE的紅點,hitTest執(zhí)行順序如下所示:下面是pointInside為YES部分,X部分執(zhí)行pointInside為NO部分,最終hitTest返回ViewE。

- 首先調(diào)用A的hitTest方法,觸摸點在其內(nèi)部,pointInside返回YES,遍歷子視圖,按照先后順序調(diào)用B和C的hitTest方法,記住B在C的上層,首先調(diào)用。
- 執(zhí)行B的hitTest方法,觸摸點不在B的內(nèi)部,pointInside返回NO,hitTest返回nil。
- 執(zhí)行C的hitTest方法,觸摸點在C的內(nèi)部,pointInside返回YES,遍歷子視圖,依次調(diào)用D和E的hitTest方法。
- D執(zhí)行hitTest方法,觸摸點不在D的內(nèi)部,pointInside返回NO,hitTest方法返回nil
- E執(zhí)行hitTest方法,觸摸點在E的內(nèi)部,pointInside返回YES,E沒有子視圖了,則hitTest返回的就是E本身,這樣就找到了需要顯示的視圖。
事件的傳遞過程
事件的傳遞和上邊尋找響應(yīng)視圖的hitTest中pointInside返回YES的視圖的執(zhí)行順序是相反的,都是從最上層的視圖開始傳遞,一直到UIApplication,借用上面的例子就是E來響應(yīng)事件,E如果不處理,就會傳遞給下一個響應(yīng)者C,C如果也不處理,就會傳遞給A,A如果也不處理,則系統(tǒng)就會忽略該事件了。
下面就是官方文檔給出的響應(yīng)過程。

1. 響應(yīng)者對事件的操作方式
響應(yīng)者對于事件的攔截以及傳遞都是通過touchesBegan:withEvent:
方法控制的,該方法的默認實現(xiàn)是將事件沿著默認的響應(yīng)鏈往下傳遞。
響應(yīng)者對于接收到的事件有3種操作:
-
不攔截,這個是默認操作
- 事件會自動沿著默認的響應(yīng)鏈往下傳遞
-
攔截,不再往下分發(fā)事件
- 重寫
touchesBegan:withEvent:進行事件處理,不調(diào)用父類的touchesBegan:withEvent:
- 重寫
-
攔截,繼續(xù)往下分發(fā)事件
- 重寫
touchesBegan:withEvent:進行事件處理,同時調(diào)用父類的touchesBegan:withEvent:將事件往下傳遞
- 重寫
2. 響應(yīng)鏈中的事件傳遞規(guī)則
每一個響應(yīng)者對象(UIResponder對象)都有一個nextResponder方法,用于獲取響應(yīng)鏈中當(dāng)前對象的下一個響應(yīng)者。因此,一旦事件的最佳響應(yīng)者確定了,這個事件所處的響應(yīng)鏈就確定了。
-
UIView- 如果視圖是根控制器的根視圖,那么
nextResponder就是根控制器對象;否則,其nextResponder為父視圖。
- 如果視圖是根控制器的根視圖,那么
-
UIViewController- 若控制器的視圖是window的根視圖,則其
nextResponder為窗口對象;若控制器是從別的控制器present出來的,則其nextResponder為presenting view controller。
- 若控制器的視圖是window的根視圖,則其
-
UIWindow-
nextResponder為UIApplication對象。
-
-
UIApplication- 若當(dāng)前應(yīng)用的app delegate是一個UIResponder對象,且不是UIView、UIViewController或app本身,則UIApplication的nextResponder為
app delegate。
- 若當(dāng)前應(yīng)用的app delegate是一個UIResponder對象,且不是UIView、UIViewController或app本身,則UIApplication的nextResponder為
下面看一張圖

可以用以下方式打印一個響應(yīng)鏈中的每一個響應(yīng)對象,在最佳響應(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]);
}
}
3. 手勢識別器比UIResponder具有更高的事件響應(yīng)優(yōu)先級
手勢分為離散型手勢(discrete gestures)和持續(xù)型手勢(continuous gesture)。系統(tǒng)提供的離散型手勢包括點按手勢(UITapGestureRecognizer)和輕掃手勢(UISwipeGestureRecognizer ),其余均為持續(xù)型手勢。
兩者主要區(qū)別在于狀態(tài)變化過程:
-
離散型:
- 識別成功:
Possible —> Recognized - 識別失?。?code>Possible —> Failed
- 識別成功:
-
持續(xù)型:
- 完整識別:
Possible —> Began —> [Changed] —> Ended - 不完整識別:
Possible —> Began —> [Changed] —> Cancel
- 完整識別:
Window在將事件傳遞給hit-tested view之前,會先將事件傳遞給相關(guān)的手勢識別器并由手勢識別器優(yōu)先識別。若手勢識別器成功識別了事件,就會取消hit-tested view對事件的響應(yīng);若手勢識別器沒能識別事件,hit-tested view才完全接手事件的響應(yīng)權(quán)。也就是說手勢的優(yōu)先級要比UIResponder高。
在手勢的一個子類UIGestureRecognizerSubclass.h中,調(diào)用下面幾個方法來響應(yīng)事件。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
參考文章
1. iOS-使用hitTest控制點擊事件的響應(yīng)對象
2. iOS觸摸事件全家桶
3. iOS觸摸事件詳解
后記
未完,待續(xù)~~~
