iOS的工作分为3大类型
在开发中,最常用到的便是Touch Events(接触工作) ,基本贯穿于每个App中,也是本文的主角~ 因而文中所说工作均特指接触工作。
接下来,记载、触及的问题大致包含:
- 工作是怎样寻觅工作的最佳呼应者
- 工作的呼应及在呼应链中的传递
寻觅工作的最佳呼应者(Hit-Testing)
当咱们接触屏幕的某个可呼应的功用点后,终究都会由UIView或者承继UIView的控件来呼应
那咱们先来看下UIView的两个办法:
// recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
//回来寻觅到的终究呼应这个工作的视图
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
// default returns YES if point is in bounds
//判别某一个点击的方位是否在视图规模内
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
每个UIView目标都有一个hitTest: withEvent:
办法,这个办法是Hit-Testing
进程中最核心的存在,其作用是问询工作在当时视图中的呼应者,一起又是作为工作传递的桥梁。
看看它是什么时候被调用的
- 当手指接触屏幕,UIApplication接收到手指的接触工作之后,就会去调用UIWindow的
hitTest: withEvent:
办法 - 在
hitTest: withEvent:
办法中会调用pointInside: withEvent:
去判别当时点击的point是否归于UIWindow规模内,假如是,就会以倒序的办法遍历它的子视图,即越后增加的视图,越先遍历 - 子视图也调用自身的
hitTest: withEvent:
办法,来查找终究呼应的视图
再来看个示例:
视图层级如下(同一层级的视图越在下面,表明越后增加):
A
├── B
│ └── D
└── C
├── E
└── F
现在假定在E视图所在的屏幕方位触发一个接触,App接收到这个接触工作工作后,先将工作传递给UIWindow,然后自下而上开始在子视图中寻觅最佳呼应者。工作传递的次序如下所示:
- UIWindow将工作传递给其子视图A
- A判别自身能呼应该工作,持续将工作传递给C(由于视图C比视图B后增加,因而优先传给C)。
- C判别自身能呼应工作,持续将工作传递给F(同理F比E后增加)。
- F判别自身不能呼应工作,C又将工作传递给E。
- E判别自身能呼应工作,一起E现已没有子视图,因而终究E便是最佳呼应者。
以上,便是寻觅最佳呼应者的整个进程。
接下来,来看下hitTest: withEvent:
办法里,都做些了什么?
咱们现已知道工作在呼应者之间的传递,是视图经过判别自身能否呼应工作来决议是否持续向子视图传递,那么判别呼应的条件是什么呢?
视图呼应工作的条件:
- 允许交互:
userInteractionEnabled = YES
- 制止躲藏:
hidden = NO
- 透明度:
alpha > 0.01
- 接触点的方位:经过
pointInside: withEvent:
办法判别接触点是否在视图的坐标规模内
代码的体现大约如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
//3种状况无法呼应工作
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: withEvent:
来搞些什么工作呢
使超出父视图坐标规模的子视图也能呼应工作
视图层级如下:
css
A
├── B
如上图所示,视图B有一部分是不在父视图A的坐标规模内的,当咱们接触视图B的上半部分,是不会呼应工作的。当然,咱们能够经过重写视图A的hitTest: withEvent:
办法来解决这个需求。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *view = [super hitTest:point withEvent:event];
//假如找不到适宜的呼应者
if (view == nil) {
//视图B坐标系的转化
CGPoint newPoint = [self.deleteButton convertPoint:point fromView:self];
if (CGRectContainsPoint(self.deleteButton.bounds, newPoint)) {
// 满意条件,回来视图B
view = self.deleteButton;
}
}
return view;
}
在视图A的hitTest: withEvent:
办法中判别接触点,是否位于视图B的视图规模内,假如归于,则回来视图B。这样一来,当咱们点击视图B的任何方位都能够呼应工作了。
工作的呼应及在呼应链中的传递
经历Hit-Testing后,UIApplication现已知道工作的最佳呼应者是谁了,接下来要做的工作便是:
- 将工作传递给最佳呼应者呼应
- 工作沿着呼应链传递
工作传递给最佳呼应者
最佳呼应者具有最高的工作呼应优先级,因而UIApplication会先将工作传递给它供其呼应。
UIApplication中有个sendEvent:
的办法,在UIWindow中相同也能够发现一个相同的办法。UIApplication是经过这个办法把工作发送给UIWindow,然后UIWindow经过相同的接口,把工作发送给最佳呼应者。
以寻觅工作的最佳呼应者一节中点击视图E为例,在EView的touchesBegan:withEvent:
上打个断点检查调用栈就能看清这一进程:
当工作传递给最佳呼应者后,呼应者呼应这个工作,则这个工作到此就完毕了,它会被开释。假定呼应者没有呼应这个工作,那么它将何去何从?工作将会沿着呼应链自上而下传递。
注意: 寻觅最佳呼应者一节中也提到了工作的传递,与此处所说的工作的传递有本质区别。上面所说的工作传递的目的是为了寻觅工作的最佳呼应者,是自下而上(父视图到子视图)的传递;而这儿的工作传递目的是呼应者做出对工作的呼应,这个进程是自上而下(子视图到父视图)的。前者为“寻觅”,后者为“呼应”。
工作沿着呼应链传递
在UIKit中有一个类:UIResponder,它是一切能够呼应工作的类的基类。来看下它的头文件的几个特点和办法
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
--------------省掉部分代码------------
// 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);
UIApplication,UIViewController和UIView都是承继自它,都有一个nextResponder
办法,用于获取呼应链中当时目标的下一个呼应者,也经过nextResponder
来串成呼应链。
在App中,一切的视图都是根据树状层次结构组织起来的,因而,每个View都有自己的SuperView。当一个View被add到SuperView上的时候,它的nextResponder
特点就会被指向它的SuperView,各个不同呼应者的指向如下:
- UIView 若视图是操控器的根视图,则其
nextResponder
为操控器目标;不然,其nextResponder
为父视图。 - UIViewController 若操控器的视图是window的根视图,则其
nextResponder
为窗口目标;若操控器是从别的操控器present出来的,则其nextResponder
为presenting view controller。 - UIWindow
nextResponder
为UIApplication目标。 - UIApplication 若当时使用的app delegate是一个UIResponder目标,且不是UIView、UIViewController或app自身,则UIApplication的
nextResponder
为app delegate。
这样,整个App就经过nextResponder
串成了一条链,也便是咱们所说的呼应链,子视图指向父视图构成的呼应链。
看一下官网关于呼应链的示例展现
若接触发生在UITextField上,则工作的传递次序是:
- UITextField ——> UIView ——> UIView ——> UIViewController ——> UIWindow ——> UIApplication ——> UIApplicationDelegte
图中虚线箭头是指若该UIView是作为UIViewController根视图存在的,则其nextResponder
为UIViewController目标;若是直接add在UIWindow上的,则其nextResponder
为UIWindow目标。
呼应者关于工作的阻拦以及传递都是经过touchesBegan:withEvent:
办法操控的,该办法的默认实现是将工作沿着默认的呼应链往下传递。
呼应者关于接收到的工作有3种操作:
- 不阻拦,默认操作 工作会自动沿着默认的呼应链往下传递
- 阻拦,不再往下分发工作 重写
touchesBegan:withEvent:
进行工作处理,不调用父类的touchesBegan:withEvent:
- 阻拦,持续往下分发工作 重写
touchesBegan:withEvent:
进行工作处理,一起调用父类的touchesBegan:withEvent:
将工作往下传递
因而,你也能够经过touchesBegan:withEvent:
办法搞点工作~
总结
接触工作先经过自下而上(父视图–>子视图)的传递办法寻觅最佳呼应者,
然后以自上而下(子视图–>父视图)的办法在呼应链中传递。