一、UIResponder和响应链的组成,如图一所示。
1、如果initial View的上的事件没有响应,它会继续向它的superView传递响应时间,如果superView依旧没反应,依次向上寻找。
2、事例,如图二所示。
发现btn.nextResponder.nextResponder.nextResponder
响应者为nil
,原因是view
在viewDidLoad
中没有完全形成。
view
最终的形成在viewDidApear
中,如图三所示,按钮的响应链如图三所示。UIView->ViewController->UIWindow->UIApplication->AppDelegate
二、Hit-Test找到view的流程
1、Hit-Test找View流程 如图四所示:
例子1:点击红色视图,查看hitTest流程。
代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
[self createViewHierachy];
}
- (void)createViewHierachy {
EOCLightGrayView *grayView = [[EOCLightGrayView alloc] initWithFrame:CGRectMake(50.f, 100.f, 260.f, 200.f)];
EOCRedView *redView = [[EOCRedView alloc] initWithFrame:CGRectMake(0.f, 0.f, 120.f, 100.f)];
EOCBlueView *blueView = [[EOCBlueView alloc] initWithFrame:CGRectMake(140.f, 100.f, 100.f, 100.f)];
EOCYellowView *yellowView = [[EOCYellowView alloc] initWithFrame:CGRectMake(50.f, 360.f, 200.f, 200.f)];
[self.view addSubview:grayView];
[grayView addSubview:redView];
[grayView addSubview:blueView];
[self.view addSubview:yellowView];
}
重写颜色视图的方法
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(@"%@ pointInside", self.EOCBgColorString);
return [super pointInside:point withEvent:event];
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(@"%@ hitTest", self.EOCBgColorString);
return [super hitTest:point withEvent:event];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%@ touchBegan", self.EOCBgColorString);
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%@ touchesMoved", self.EOCBgColorString);
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%@ touchesEnded", self.EOCBgColorString);
[super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%@ touchesCancelled", self.EOCBgColorString);
[super touchesCancelled:touches withEvent:event];
}
界面如图五所示:
结果如下:
yellowColorView hitTest
yellowColorView pointInside
ligthGrayColorView hitTest
ligthGrayColorView pointInside
blueColorView hitTest
blueColorView pointInside
redColorView hitTest
redColorView pointInside
yellowColorView hitTest
yellowColorView pointInside
ligthGrayColorView hitTest
ligthGrayColorView pointInside
blueColorView hitTest
blueColorView pointInside
redColorView hitTest
redColorView pointInside
redColorView touchBegan 0
ligthGrayColorView touchBegan
-[EOCTouchEventViewCtrl touchesBegan:withEvent:]
redColorView touchesEnded 3
ligthGrayColorView touchesEnded
-[EOCTouchEventViewCtrl touchesEnded:withEvent:]
分析:按照如图四的寻找结构,首先它会寻找黄色的视图,如果发现黄色的视图没有响应这个点击事件,会寻找同级下的灰色视图,如果发现触摸点在灰色视图范围内,则遍历它的子视图,以此类推。注:后添加的视图先寻找。
例子2:点击灰色视图,查看hitTest流程:
结果如下:
yellowColorView hitTest
yellowColorView pointInside
ligthGrayColorView hitTest
ligthGrayColorView pointInside
blueColorView hitTest
blueColorView pointInside
redColorView hitTest
redColorView pointInside
yellowColorView hitTest
yellowColorView pointInside
ligthGrayColorView hitTest
ligthGrayColorView pointInside
blueColorView hitTest
blueColorView pointInside
redColorView hitTest
redColorView pointInside
ligthGrayColorView touchBegan
-[EOCTouchEventViewCtrl touchesBegan:withEvent:]
ligthGrayColorView touchesEnded
-[EOCTouchEventViewCtrl touchesEnded:withEvent:]
如果发现灰色视图的pointInside返回为YES,它会继续寻找子视图。
例子3:点击黄色视图,查看hitTest流程:
结果如下
yellowColorView hitTest
yellowColorView pointInside
yellowColorView hitTest
yellowColorView pointInside
yellowColorView touchBegan
-[EOCTouchEventViewCtrl touchesBegan:withEvent:]
yellowColorView touchesEnded
-[EOCTouchEventViewCtrl touchesEnded:withEvent:]
如果黄色视图的pointInside返回为YES,它没有子视图,就停止寻找。
2、如果点击图五种的红色视图,让蓝色视图响应。该怎么做?
重写蓝色视图中的pointInside方法,返回YES,阻止他进行下一步的向兄弟视图的寻找,如图四所示查找视图的流程。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(@"%@ pointInside", self.EOCBgColorString);
return YES;//[super pointInside:point withEvent:event];
}
打印结果如下所示:
yellowColorView hitTest
yellowColorView pointInside
ligthGrayColorView hitTest
ligthGrayColorView pointInside
blueColorView hitTest
blueColorView pointInside
yellowColorView hitTest
yellowColorView pointInside
ligthGrayColorView hitTest
ligthGrayColorView pointInside
blueColorView hitTest
blueColorView pointInside
blueColorView touchBegan
ligthGrayColorView touchBegan
-[EOCTouchEventViewCtrl touchesBegan:withEvent:]
blueColorView touchesEnded
ligthGrayColorView touchesEnded
-[EOCTouchEventViewCtrl touchesEnded:withEvent:]
3、影响Hit-Test流程的因素:有alpha、hidden、userInteractionEnabled,当alpha的值小于等于0.01,Hit-Test就会受影响。
Hit-Test的真正逻辑:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(@"%@ hitTest", self.EOCBgColorString);
if (self.alpha <= 0.01 || self.hidden || !self.userInteractionEnabled) {
return nil;
}
NSArray *subViews = [[self.subviews reverseObjectEnumerator] allObjects];
for (id view in subViews) {
CGPoint convertPoint = [self convertPoint:point toView:view];
if ([view pointInside:convertPoint withEvent:event]) {
return view;
}
}
return self;
}
三、使用Hit-Test的小案例
1、扩大按钮的响应范围。
如图六所示,1表示按钮,2表示按钮要扩大的范围。
- (void)viewDidLoad {
[super viewDidLoad];
CustomBtn * btn = [[CustomBtn alloc] initWithFrame:CGRectMake(100, 100, 10, 10)];
btn.backgroundColor = [UIColor redColor];
[btn addTarget:self action:@selector(btnClicked) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
}
- (void)btnClicked{
NSLog(@"%s",__func__);
}
@implementation CustomBtn
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
CGRect frame = [self getScaleFrame];
//NSLog(@"frame = %@,point = %@",NSStringFromCGRect(frame),NSStringFromCGPoint(point));
return CGRectContainsPoint(frame, point);
//return YES;
}
- (CGRect)getScaleFrame{
CGRect tmpFrame = CGRectMake(-15, -15, 40, 40);
return tmpFrame;
}
@end
如果CustomBtn
中的pointInside
返回YES,那么点击视图的任何地方都会响应按钮的事件。
2、如果按钮添加到视图可见范围之外的地方,按钮超出父视图的部分无法响应点击事件。如图六所示:
- (void)viewDidLoad {
[super viewDidLoad];
RedView * redView = [[RedView alloc] initWithFrame:CGRectMake(200, 200, 100, 100)];
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];
CustomBtn * btn = [[CustomBtn alloc] initWithFrame:CGRectMake(85, 85, 30, 30)];
btn.backgroundColor = [UIColor blueColor];
[btn addTarget:self action:@selector(btnClicked:) forControlEvents:UIControlEventTouchUpInside];
[redView addSubview:btn];
}
如何解决这个问题?
(一)重写红色视图的
pointInside
方法,返回YES,它会自动寻找子视图的响应事件。否则直接寻找兄弟视图的响应事件。(二)重写hitTest方法。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(@"%@ hitTest", self.EOCBgColorString);
if (self.alpha <= 0.01 || self.hidden || !self.userInteractionEnabled) {
return nil;
}
NSArray *subViews = [[self.subviews reverseObjectEnumerator] allObjects];
for (id view in subViews) {
//将point从self坐标系中的点转换成view坐标系中的点
CGPoint convertPoint = [self convertPoint:point toView:view];
if ([view pointInside:convertPoint withEvent:event]) {
return view;
}
}
return self;
}
四、手势识别
1、手势的状态
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
UIGestureRecognizerStatePossible, // the recognizer has not yet recognized its gesture, but may be evaluating touch events. this is the default state
UIGestureRecognizerStateBegan, // the recognizer has received touches recognized as the gesture. the action method will be called at the next turn of the run loop
UIGestureRecognizerStateChanged, // the recognizer has received touches recognized as a change to the gesture. the action method will be called at the next turn of the run loop
UIGestureRecognizerStateEnded, // the recognizer has received touches recognized as the end of the gesture. the action method will be called at the next turn of the run loop and the recognizer will be reset to UIGestureRecognizerStatePossible
UIGestureRecognizerStateCancelled, // the recognizer has received touches resulting in the cancellation of the gesture. the action method will be called at the next turn of the run loop. the recognizer will be reset to UIGestureRecognizerStatePossible
UIGestureRecognizerStateFailed, // the recognizer has received a touch sequence that can not be recognized as the gesture. the action method will not be called and the recognizer will be reset to UIGestureRecognizerStatePossible
// Discrete Gestures – gesture recognizers that recognize a discrete event but do not report changes (for example, a tap) do not transition through the Began and Changed states and can not fail or be cancelled
UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded // the recognizer has received touches recognized as the gesture. the action method will be called at the next turn of the run loop and the recognizer will be reset to UIGestureRecognizerStatePossible
};
手势的是从哪里看是识别的呢?
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
RedView * view = [[RedView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
view.backgroundColor = [UIColor redColor];
view.userInteractionEnabled = YES;
RedViewPanGesture * pan = [[RedViewPanGesture alloc] initWithTarget:self action:@selector(panEvent:)];
[view addGestureRecognizer:pan];
[self.view addSubview:view];
}
- (void)panEvent:(UIPanGestureRecognizer *)sender{
NSLog(@"%s",__func__);
}
@end
@implementation RedViewPanGesture
- (instancetype)initWithTarget:(id)target action:(SEL)action
{
if (self = [super initWithTarget:target action:action]) {
self.delegate = self;
}
return self;
}
#pragma mark - GestureDelegate method
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
NSLog(@"%s",__func__);
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
NSLog(@"%s",__func__);
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
NSLog(@"%s",__func__);
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
//我被另外一个手势变成Fail
NSLog(@"%s",__func__);
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
NSLog(@"%s",__func__);
return YES;
}
@end
打印结果:
-[RedViewPanGesture gestureRecognizer:shouldReceiveTouch:]
-[RedViewPanGesture gestureRecognizer:shouldRequireFailureOfGestureRecognizer:]
-[RedViewPanGesture gestureRecognizer:shouldRequireFailureOfGestureRecognizer:]
-[RedViewPanGesture gestureRecognizerShouldBegin:]
-[ViewController panEvent:]
结论:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
在手势开始的时候,就会调用手势的事件方法。
2、手势影响View的方法
2.1、delaysTouchesBegan
这是手势的一个属性,默认状态下这个属性为NO,表示当手势未被识别时,会调用视图的触摸方法(hit-test、touchMove、pointInside)的方法。如果这个手势的属性为YES,表示,手势识别,不会调用视图的触摸方法(hit-test、touchMove、pointInside)的方法,如果手势识别失败,则会调用视图的触摸方法。
2.2、cancelsTouchesInView
这个是手势的一个属性,默认状态为YES,当手势被识别的时候,会阻止视图的触摸事件。当为NO时,手势被识别时,不会阻止视图的触摸事件。
例:给按钮添加点击手势,当cancelsTouchesInView的方法为YES时,只会执行点击手势响应的方法,不会执行按钮的点击事件。当设置了该属性的方法为NO,按钮视图执行了按钮的点击事件,也执行了添加到按钮上的点击方法。
- (void)viewDidLoad {
[super viewDidLoad];
UIButton * btn = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
[btn setTitle:@"按钮" forState:UIControlStateNormal];
[btn addTarget:self action:@selector(btnClicked) forControlEvents:UIControlEventTouchUpInside];
btn.backgroundColor = [UIColor redColor];
UITapGestureRecognizer * tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapEvent)];
tap.cancelsTouchesInView = NO;
[btn addGestureRecognizer:tap];
[self.view addSubview:btn];
}
- (void)tapEvent{
NSLog(@"%s",__func__);
}
- (void)btnClicked{
NSLog(@"%s",__func__);
}
2.3、delaysTouchesEnded
这是手势的一个属性,默认状态下这个属性为YES,当手势识别成功后,会将touchesCancelled消息发送给视图发送hit-test消息,手势识别失败后,会延迟0.15ms,期间没有收到别的touch才会发送touchesEnded,如果设置成NO,手势识别失败后会立马发送touchesEnded已借宿当前的触摸。