版本记录
版本号 | 时间 |
---|---|
V1.0 | 2017.12.10 |
前言
APP中的视图就是进行人机交互的,各种点击等手势都是通过与屏幕视图进行交互完成的。这里面很重要的一个就是HitTest方法,它用于寻找响应者以及传递事件,接下来我们就一起研究下HitTest方法的调用机制。
hitTest方法
hitTest:withEvent:
是UIView
的一个方法,该方法会被系统调用,作用就是用于选择最合适的视图来响应触摸事件。
事件的声明周期
每一个事件从产生到结束是有一个过程的,也就是事件的声明周期。下面看一张图。
几个重要的类
1. UITouch
- 一个手指一次触摸屏幕,就对应生成一个UITouch对象。多个手指同时触摸,生成多个UITouch对象。
- 多个手指先后触摸,系统会根据触摸的位置判断是否更新同一个UITouch对象。若两个手指一前一后触摸同一个位置(即双击),那么第一次触摸时生成一个UITouch对象,第二次触摸更新这个UITouch对象(UITouch对象的 tap count 属性值从1变成2);若两个手指一前一后触摸的位置不同,将会生成两个UITouch对象,两者之间没有联系。
- 每个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
这个是事件的真身。触摸的目的就是生成触摸事件供响应者响应,一个触摸事件对应一个UIEvent
对象,其中的type
属性标识了事件的类型。如果一个触摸事件可能是由多个手指同时触摸产生的,触摸对象集合通过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
等很多类都继承它,所以就可以响应事件。响应事件主要靠下面的几个方法。
//手指触碰屏幕,触摸开始
- (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;
寻找合适响应视图的过程
寻找过程
touch->UIApplication->UIWindow->UIViewController.view->subViews->...->view
- 用户点击屏幕产生一个触摸事件,加入到
UIApplication
管理的事件队列中。 -
UIApplication
会从事件队列中取出最早的事件进行分发处理,先发送事件给应用程序的主窗口UIWindow
。 - 主窗口会调用其
hitTest:withEvent:
方法在视图(UIView)层次结构中找到一个最合适的UIView来处理触摸事件。
这里需要说明的是,在寻找响应UIView过程中大致的原理。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
//系统默认会忽略isUserInteractionEnabled设置为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;
}
- 首先在当前视图
hitTest
方法中调用pointInside
方法,判断触摸点是否在当前视图内部。- 如果
pointInside
方法返回NO,当hitTest方法返回的是nil,该视图不处理该事件。- 如果
pointInside
方法返回YES,说明触摸点在该视图内部,则从最上层的子视图开始遍历,为什么从最上层呢?因为subviews数组后添加的视图就是在数组的末尾,遍历当前视图的所有子视图,调用hitTest方法,重复1 - 3 步骤。- 直到有的子视图
hitTest
方法返回非空对象或者全部子视图遍历完毕,则结束该视图的循环遍历。若第一次有子视图的hitTest
方法返回非空对象,则当前视图的hitTest
方法就返回此对象,处理结束;若所有子视图的hitTest
方法都返回nil,则当前视图的hitTest
方法返回当前视图本身,最终由该对象处理触摸事件。
下面我们就借用一个例子说明一下这个寻找的过程。这里例子是别人写的,已经进行了引用标注,在下面列出来参考文章。
这里首先假设:ViewB在ViewC上层,ViewD在ViewE上层,即ViewB的addSubView:执行在ViewC之后,ViewD的addSubView:执行在ViewE之后。
当我们点击屏幕的时候,也就是ViewE的红点,hitTest执行顺序如下所示:下面是pointInside为YES部分,X部分执行pointInside为NO部分,最终hitTest返回ViewE。
- 首先调用A的hitTest方法,触摸点在其内部,pointInside返回YES,遍历子视图,按照先后顺序调用B和C的hitTest方法,记住B在C的上层,首先调用。
- 执行B的hitTest方法,触摸点不在B的内部,pointInside返回NO,hitTest返回nil。
- 执行C的hitTest方法,触摸点在C的内部,pointInside返回YES,遍历子视图,依次调用D和E的hitTest方法。
- D执行hitTest方法,触摸点不在D的内部,pointInside返回NO,hitTest方法返回nil
- E执行hitTest方法,触摸点在E的内部,pointInside返回YES,E没有子视图了,则hitTest返回的就是E本身,这样就找到了需要显示的视图。
事件的传递过程
事件的传递和上边寻找响应视图的hitTest中pointInside返回YES的视图的执行顺序是相反的,都是从最上层的视图开始传递,一直到UIApplication,借用上面的例子就是E来响应事件,E如果不处理,就会传递给下一个响应者C,C如果也不处理,就会传递给A,A如果也不处理,则系统就会忽略该事件了。
下面就是官方文档给出的响应过程。
1. 响应者对事件的操作方式
响应者对于事件的拦截以及传递都是通过touchesBegan:withEvent:
方法控制的,该方法的默认实现是将事件沿着默认的响应链往下传递。
响应者对于接收到的事件有3种操作:
-
不拦截,这个是默认操作
- 事件会自动沿着默认的响应链往下传递
-
拦截,不再往下分发事件
- 重写
touchesBegan:withEvent:
进行事件处理,不调用父类的touchesBegan:withEvent:
- 重写
-
拦截,继续往下分发事件
- 重写
touchesBegan:withEvent:
进行事件处理,同时调用父类的touchesBegan:withEvent:
将事件往下传递
- 重写
2. 响应链中的事件传递规则
每一个响应者对象(UIResponder
对象)都有一个nextResponder
方法,用于获取响应链中当前对象的下一个响应者。因此,一旦事件的最佳响应者确定了,这个事件所处的响应链就确定了。
-
UIView
- 如果视图是根控制器的根视图,那么
nextResponder
就是根控制器对象;否则,其nextResponder为父视图。
- 如果视图是根控制器的根视图,那么
-
UIViewController
- 若控制器的视图是window的根视图,则其
nextResponder
为窗口对象;若控制器是从别的控制器present出来的,则其nextResponder为presenting view controller
。
- 若控制器的视图是window的根视图,则其
-
UIWindow
-
nextResponder
为UIApplication对象。
-
-
UIApplication
- 若当前应用的app delegate是一个UIResponder对象,且不是UIView、UIViewController或app本身,则UIApplication的nextResponder为
app delegate
。
- 若当前应用的app delegate是一个UIResponder对象,且不是UIView、UIViewController或app本身,则UIApplication的nextResponder为
下面看一张图
可以用以下方式打印一个响应链中的每一个响应对象,在最佳响应者的touchBegin:withEvent:方法中调用即可(别忘了调用父类的方法)。
- (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具有更高的事件响应优先级
手势分为离散型手势(discrete gestures)
和持续型手势(continuous gesture)
。系统提供的离散型手势包括点按手势(UITapGestureRecognizer)
和轻扫手势(UISwipeGestureRecognizer )
,其余均为持续型手势。
两者主要区别在于状态变化过程:
-
离散型:
- 识别成功:
Possible —> Recognized
- 识别失败:
Possible —> Failed
- 识别成功:
-
持续型:
- 完整识别:
Possible —> Began —> [Changed] —> Ended
- 不完整识别:
Possible —> Began —> [Changed] —> Cancel
- 完整识别:
Window在将事件传递给hit-tested view之前,会先将事件传递给相关的手势识别器并由手势识别器优先识别。若手势识别器成功识别了事件,就会取消hit-tested view对事件的响应;若手势识别器没能识别事件,hit-tested view才完全接手事件的响应权。也就是说手势的优先级要比UIResponder高。
在手势的一个子类UIGestureRecognizerSubclass.h
中,调用下面几个方法来响应事件。
- (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控制点击事件的响应对象
2. iOS触摸事件全家桶
3. iOS触摸事件详解
后记
未完,待续~~~