iOS全解14:事件的传递和响应机制
按照时间顺序,事件的生命周期是这样的:
事件的产生和传递(事件如何从父控件传递到子控件并寻找到最合适的view、寻找最合适的view的底层实现、拦截事件的处理)->找到最合适的view后事件的处理(touches方法的重写,也就是事件的响应)
在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。以下都是继承自UIResponder的,所以都能接收并处理事件。
UIApplication
UIViewController
UIView(superView、subView)
事件的传递和响应的区别:
事件的传递:是从上到下(父控件到子控件)
事件的响应:是从下到上(顺着响应者链条向上传递:子控件到父控件)
UITouch对象
当用户用手指触摸屏幕时,会创建一个与手指相关的UITouch对象。
作用:
- 保存着跟手指相关的信息,比如触摸的位置、时间、阶段
- 当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置
- 当手指离开屏幕时,系统会销毁相应的UITouch对象
提 示:iPhone开发中,要避免使用双击事件!
iOS中的事件的产生和传递
1、事件的产生
- 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中,为什么是队列而不是栈?因为队列的特点是FIFO,即先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。
- UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)。
- 主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。
找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。
2、事件的传递
触摸事件的传递是从父控件传递到子控件
也就是UIApplication->window->寻找处理事件最合适的view
注 意:
如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件
应用如何找到最合适的控件来处理事件?
1.首先判断主窗口(keyWindow)自己是否能接受触摸事件
2.判断触摸点是否在自己身上
3.子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)
4.view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止。
5.如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。
UIView不能接收触摸事件的三种情况:
- 不允许交互:userInteractionEnabled = NO
- 隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
- 透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。
注 意:
默认UIImageView不能接受触摸事件,因为不允许交互,即userInteractionEnabled = NO。所以如果希望UIImageView可以交互,需要设置UIImageView的userInteractionEnabled = YES。
总结一下
1.点击一个UIView或产生一个触摸事件A,这个触摸事件A会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。
2.UIApplication会从事件对列中取出最前面的事件(此处假设为触摸事件A),把事件A传递给应用程序的主窗口(keyWindow)。
3.窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)
//==================" 系统框架 "==================
#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, // 当手指接触表面时。
UITouchPhaseMoved, // 当手指在表面移动。
UITouchPhaseStationary, // 当手指接触表面,但自上次事件后没有移动。
UITouchPhaseEnded, // 当手指离开表面时。
UITouchPhaseCancelled, // 当触摸没有结束,但我们需要停止跟踪时(例如,将设备面对面)
UITouchPhaseRegionEntered API_AVAILABLE(ios(13.4) // 当触摸进入用户界面区域时
UITouchPhaseRegionMoved API_AVAILABLE(ios(13.4) // 当触摸在用户界面的区域内,但还没有接触或离开该区域时
UITouchPhaseRegionExited API_AVAILABLE(ios(13.4) // 当触摸退出用户界面区域时
};
typedef NS_ENUM(NSInteger, UIForceTouchCapability) {
UIForceTouchCapabilityUnknown = 0,
UIForceTouchCapabilityUnavailable = 1,
UIForceTouchCapabilityAvailable = 2
};
typedef NS_ENUM(NSInteger, UITouchType) {
UITouchTypeDirect, // 用手指(在屏幕上)直接触摸
UITouchTypeIndirect, // 间接触摸(不是屏幕)
UITouchTypePencil, // 添加铅笔名变体
UITouchTypeStylus = UITouchTypePencil, // 触控笔的触摸(不建议使用铅笔)
UITouchTypeIndirectPointer API_AVAILABLE(ios(13.4) //一种表示基于按钮的间接输入设备的触摸,描述从按钮按下到按钮释放的输入顺序
} API_AVAILABLE(ios(9.0));
typedef NS_OPTIONS(NSInteger, UITouchProperties) {
UITouchPropertyForce = (1UL << 0),
UITouchPropertyAzimuth = (1UL << 1),
UITouchPropertyAltitude = (1UL << 2),
UITouchPropertyLocation = (1UL << 3), // 为预测触动
} API_AVAILABLE(ios(9.1));
UIKIT_EXTERN API_AVAILABLE(ios(2.0)) NS_SWIFT_UI_ACTOR
@interface UITouch : NSObject
@property(nonatomic,readonly) NSTimeInterval timestamp; // 记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) UITouchPhase phase; // 当前触摸事件所处的状态
@property(nonatomic,readonly) NSUInteger tapCount; // 短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) UITouchType type
@property(nonatomic,readonly) CGFloat majorRadius; // majorRadius和majorRadiusTolerance以点数表示
@property(nonatomic,readonly) CGFloat majorRadiusTolerance; // majorRadius将是精确的+/- majorRadiusTolerance
@property(nullable,nonatomic,readonly,strong) UIWindow *window; // 触摸产生时所处的窗口
@property(nullable,nonatomic,readonly,strong) UIView *view; // 触摸产生时所处的视图
@property(nullable,nonatomic,readonly,copy) NSArray <UIGestureRecognizer *> *gestureRecognizers; // 触摸手势
- (CGPoint)locationInView:(UIView *)view; // 触摸在view上的位置
- (CGPoint)previousLocationInView:(UIView *)view; // 记录了前一个触摸点的位置
- (CGPoint)preciseLocationInView:(UIView *)view; // 使用这些方法获得额外的精度,可能从触摸可用。
- (CGPoint)precisePreviousLocationInView:(UIView *)view; // 不要使用精确的位置进行命中测试。触摸可能会在视图内部进行测试,但在视图外部有一个精确的位置。
@property(nonatomic,readonly) CGFloat force; //触摸的力,其中1.0代表平均触摸的力
@property(nonatomic,readonly) CGFloat maximumPossibleForce; //使用这个输入机制最大可能的力量
//1 方位角。仅对触控笔触摸类型有效。零弧度点沿X轴正方向。为视图参数传递一个空值将返回相对于触摸窗口的方位角。
//2 指向方位角方向的单位向量。仅对触控笔触摸类型有效。为视图参数传递nil将返回一个相对于触摸窗口的单位向量。
- (CGFloat)azimuthAngleInView:(nullable UIView *)view;
- (CGVector)azimuthUnitVectorInView:(nullable UIView *)view;
// 高度角:仅对触控笔触摸类型有效。0弧度表示触控笔与屏幕表面平行,当M_PI/2弧度表示它是垂直于屏幕表面。
@property(nonatomic,readonly) CGFloat altitudeAngle;
//1 一个索引,允许你关联更新与原始触摸。只有当这个UITouch预期或是一个更新时才保证非nil。
//2 一个属性的集合,它具有估计值,仅表示当前估计的属性
//3 期望在将来有更新的属性集。如果未对估价值进行更新,则当前值为最终估价值。当从边缘进入时,方位/高度值会发生这种情况
@property(nonatomic,readonly) NSNumber * _Nullable estimationUpdateIndex;
@property(nonatomic,readonly) UITouchProperties estimatedProperties;
@property(nonatomic,readonly) UITouchProperties estimatedPropertiesExpectingUpdates;
@end
NS_ASSUME_NONNULL_END
#else
#import <UIKitCore/UITouch.h>
#endif
//==================" 类别 "==================
@interface UIView(UIViewGeometry)
//可以做成动画。如果视图被转换,不要使用框架,因为它不能正确地反映视图的实际位置。使用bounds + center代替。
@property(nonatomic) CGRect frame;
//如果非恒等变换,则使用bounds/center而不是frame。如果边界维数为奇数,中心可能有小数部分
@property(nonatomic) CGRect bounds; // 默认边界是0原点,帧大小。可以做成动画
@property(nonatomic) CGPoint center; // 中心是框架的中心。可以做成动画
@property(nonatomic) CGAffineTransform transform; // 默认是CGAffineTransformIdentity。可以做成动画。请使用这个属性而不是图层上的affineTransform属性
@property(nonatomic) CATransform3D transform3D API_AVAILABLE(ios(13.0),tvos(13.0)); //默认为CATransform3DIdentity。可以做成动画。请使用这个属性而不是图层上的transform属性
@property(nonatomic) CGFloat contentScaleFactor API_AVAILABLE(ios(4.0));
@property(nonatomic,getter=isMultipleTouchEnabled) BOOL multipleTouchEnabled API_UNAVAILABLE(tvos); // default is NO
@property(nonatomic,getter=isExclusiveTouch) BOOL exclusiveTouch API_UNAVAILABLE(tvos); // default is NO
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // 递归调用-pointInside:withEvent:。点在接收器的坐标系统中
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // 如果点在边界内,默认返回YES(判断点在不在方法调用者的坐标系上)
- (CGPoint)convertPoint:(CGPoint)point toView:(nullable UIView *)view;
- (CGPoint)convertPoint:(CGPoint)point fromView:(nullable UIView *)view;
- (CGRect)convertRect:(CGRect)rect toView:(nullable UIView *)view;
- (CGRect)convertRect:(CGRect)rect fromView:(nullable UIView *)view;
@property(nonatomic) BOOL autoresizesSubviews; // 默认为YES。如果设置,子视图将根据它们的autoresizingMask if self进行调整。范围内变化
@property(nonatomic) UIViewAutoresizing autoresizingMask; // 简单的调整。默认是UIViewAutoresizingNone
- (CGSize)sizeThatFits:(CGSize)size; // 返回'best' size以适合给定的大小。实际上不会调整视图的大小。默认是返回现有的视图大小
- (void)sizeToFit; // 调用sizeethatfits:与当前视图边界和改变边界大小。
@end