事件层次分析(二)——运用案例

view覆盖在button上的触摸案例

事件层次分析(二)——运用案例_第1张图片

如果在上图中想要点击黄色view中button区域内,也响应button事件,其他的照旧,点击其他黄色区域也是响应的黄色view的点击事件。其中黄色view是在button的上面,也就是先添加的button,后添加的黄色view。
可以在黄色view的 pointInside方法中实现以下代码:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    CGRect btnRect = CGRectMake(-20.f, 20.f, 230.f, 25.f);
    if (CGRectContainsPoint(btnRect, point)) {
        return NO;
    } else {
        return [super pointInside:point withEvent:event];
    }
}

其中点是以黄色view的坐标来计算的,所以算出来的btnRect的X坐标为负数,如此便可实现以上功能。
最好的做法是,将点转化为以button为坐标的点,并且判断这个点在不在button上面,这样当改变button的frame的时候,就不用改其他地方了。如下:

UIButton *button = [self superview].subviews[0];
CGPoint btnPoint = [self convertPoint:point toView:button];
if ([button pointInside:btnPoint withEvent:event]) {
    return NO;
}
return [super pointInside:point withEvent:event];

分析UIScrollView上的触摸事件处理

事件层次分析(二)——运用案例_第2张图片

上图中红色是一个大的ScrollView,ScrollView添加在一个大的灰色view背景上,ScrollView上面又添加了一个蓝色的button

scrollView上添加button,点击后事件无法传递到上一级

UIScrollView会拦截事件传递给上一层的view,要想传递必须得重写UIScrollView的touch方法:[self.nextResponder touchesBegan:touches withEvent:event]。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//    [self.nextResponder touchesBegan:touches withEvent:event];
    return [super touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
//     [self.nextResponder touchesMoved:touches withEvent:event];
    return [super touchesMoved:touches withEvent:event];
}

滚动红色ScrollView时,注释的打印:


滚动红色ScrollView时,放开注释的打印:

事件层次分析(二)——运用案例_第3张图片
scrollView上添加button,查看button的高亮显示效果(长按才有效果,但这个时候无法响应scrollView的滑动)
  • canCancelContentTouches

解决办法:设置scrollView的canCancelContentTouches属性为YES,并且在scrollView的方法touchesShouldCancelInContentView中也必须返回YES。

canCancelContentTouches用来控制是否传递touchCancel给subView,这个需要和touchesShouldCancelInContentView方法结合起来使用,如果canCancelContentTouches为YES,会调用touchesShouldCancelInContentView方法,但是如果该方法返回NO,则不会发送touchCancel消息给subView;如果canCancelContentTouches为NO,则不会调用。

  • delaysContentTouches
    这个属性确定是scrollView是否对subView的touch事件延迟,如果为NO的话,会立即响应subView的touch事件,如果为YES,那么先判断scrollView的手势

  • touchesShouldBegin:withEvent:inContentView
    -(BOOL)touchesShouldBegin:(NSSet *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view这个方法用来确定scrollView是否接收subView的touch事件,并且当你设置这个返回YES的时候,在button的touchBegin上来查看栈信息,可以看到button的事件是由手势来分发的,应该是scrollView对其进行了一层处理

    事件层次分析(二)——运用案例_第4张图片

UIButton的几大与边界相关的event的分析

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event; // touch is sometimes nil if cancelTracking calls through to this.
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event;   // event may be nil if cancelled for non-event reasons, e.g. removed from window

button的这几个方法是对其touch方法进行的封装,因为button只会产生一个touch,可以在这里面进行一些操作,比如判断button触摸的范围变化等

- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event
{
    CGFloat offset = 70.f;
    CGRect bigRect = CGRectInset(self.bounds, -offset, -offset);
    BOOL isOutOfBigRect = CGRectContainsPoint(bigRect, [touch locationInView:self]);
    BOOL isPreviousInBigRect = CGRectContainsPoint(bigRect, [touch previousLocationInView:self]);
    if (!isOutOfBigRect) {  //在外面
        if (isPreviousInBigRect) {  //之前在里面
            //发送事件
            [self sendActionsForControlEvents:UIControlEventTouchDragExit];
        }
    } else {  //在里面
        if (!isPreviousInBigRect) {  //之前在外面
            //发送事件
            [self sendActionsForControlEvents:UIControlEventTouchDragEnter];
        }
    }
    return YES;
}

如上,可以给button设置一个边界,从而在边界内外而做不同的操作。需要注意的是通过sendActionsForControlEvents来响应的button事件,只是调用了一个方法,模拟的点击事件,所以并不能取到event,触摸的点。

当在button一个范围外button的title是EightClock,在里面是八点钟学院,也可以在button的点击事件中来完成,只是需要把event传过来进行判断,如下:

 [_btn addTarget:self action:@selector(btnAction:withEvent:) forControlEvents:UIControlEventTouchDragInside];
 [_btn addTarget:self action:@selector(btnAction:withEvent:) forControlEvents:UIControlEventTouchDragOutside];
- (void)btnAction:(UIButton *)btn withEvent:(UIEvent *)event {
    
        CGFloat offset = 70.f;
        UITouch *touch = [[event allTouches] anyObject];
        CGRect bigRect = CGRectInset(btn.bounds, -offset, -offset);
        BOOL isOutOfBigRect = CGRectContainsPoint(bigRect, [touch locationInView:btn]);
        BOOL isPreviousInBigRect = CGRectContainsPoint(bigRect, [touch previousLocationInView:btn]);
        if (!isOutOfBigRect) {  //在外面
            if (isPreviousInBigRect) {  //之前在里面
                //发送事件
//               UIControlEventTouchDragExit
                [btn setTitle:@"EightClock" forState:UIControlStateNormal];
            } else { //之前在外面
                //                UIControlEventTouchDragOutside
            }
        } else {  //在里面
            if (!isPreviousInBigRect) {  //之前在外面
//                UIControlEventTouchDragEnter
                [btn setTitle:@"八点钟学院" forState:UIControlStateNormal];
            } else {  //之前在里面
                //UIControlEventTouchDragInside
            }
        }
}

有趣的UIScrollView

事件层次分析(二)——运用案例_第5张图片

如上图,和平常的一张图片显示整个屏幕宽不同,一个屏幕中会显示三张图片。初看可能觉得无从下手,但是我们仔细分析就会发现其实每次scrollview滚动的距离只是图片的宽加上图片之间的距离,而控制scrollview滚动距离的就是scrollview的宽以及contentSize,明白了这些,剩下的就好办了:

- (void)createScrollView
{
    int count = 5;
    [self.view addSubview:self.customView];
    self.view.clipsToBounds = YES;
    CGFloat scrollViewWidth = self.view.eocW-60.f;
    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(20.f, 0.f, scrollViewWidth, scrollViewWidth/2)];
    scrollView.showsHorizontalScrollIndicator = NO;
    scrollView.backgroundColor = [UIColor clearColor];
    scrollView.contentSize = CGSizeMake(scrollViewWidth *count, scrollViewWidth/2);
    scrollView.pagingEnabled = YES;
    scrollView.clipsToBounds = NO;
    [self.customView addSubview:scrollView];
    
    NSArray *imageArr = @[@"0", @"1", @"2", @"3", @"4"];
    
    //添加图片
    for (int i=0; i

其中核心代码是UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(20.f, 0.f, scrollViewWidth, scrollViewWidth/2)];scrollView.clipsToBounds = NO;,scrollView.contentSize = CGSizeMake(scrollViewWidth *count, scrollViewWidth/2);其中第二句代码是控制一个屏幕显示三张图片的。

其中要将self.view.clipsToBounds 设置为 YES;不然会有下图现象:


事件层次分析(二)——运用案例_第6张图片
Untitled.gif

但是这样会有一个问题就是点击屏幕边缘的时候就滑动不了,因为scrollview的宽度不是全屏的,所以在两边并不是scrollview,而是我们自定义的UIview,如图

事件层次分析(二)——运用案例_第7张图片

解决办法有两个:

  • 解法一:
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    NSLog(@"EOCScrollView pointInside");
    CGFloat width = [UIScreen mainScreen].bounds.size.width;
    CGRect rect = CGRectMake(width - 40, 0.f, 40.f, self.eocH);
    if (CGRectContainsPoint(rect, point)) {
        return YES;
    } else {
        return [super pointInside:point withEvent:event];
    }
}

重写scrollview的pointInside方法,算出最右边不响应的范围,当点在那个范围内时,返回yes,但是这样范围不太好算,而已范围也比较多,很不方便,不推荐。

  • 解法二:
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    NSLog(@"EOCView hitTest");
    NSArray *subViews = [[self.subviews reverseObjectEnumerator] allObjects]; // 交换顺序,后添加的先遍历
    for (id view in subViews) {

        if ([view isKindOfClass:[UIScrollView class]]) {
            return view;
        }
    }
    return [super hitTest:point withEvent:event];
}

重写scrollview父控件的hitTest方法,遍历它的子视图,找到里面的scrollview,然后返回它。这样写就是说只要包含这个scrollview就会返回它,也就是说只要在父控件上的操作都会响应scrollview的事件。

UIScreenEdgePanGestureRecognizer

事件层次分析(二)——运用案例_第8张图片

这是系统的边界手势,我们要想获取它,并且实现自己的边界手势,如下:

事件层次分析(二)——运用案例_第9张图片

就需要让系统的边界手势失效,并自己写一个手势方法

- (void)createScreenGestureView {
    UIScreenEdgePanGestureRecognizer *screenEdgePanGesture = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(panAction:)];
    
    NSArray *gestureArray = self.navigationController.view.gestureRecognizers;
    for (UIGestureRecognizer *gesture in gestureArray) {
        if ([gesture isKindOfClass:[UIScreenEdgePanGestureRecognizer class]]) { //找到系统的边界手势
            [gesture requireGestureRecognizerToFail:screenEdgePanGesture];  //让系统的边界手势失效
        }
    }
    screenEdgePanGesture.edges = UIRectEdgeLeft;  //可以判断是左边还是右边响应手势
    [self.view addGestureRecognizer:screenEdgePanGesture];   //最好加在self.view上面,这样边界的从顶到底才都能响应
}

#pragma mark - event response
- (void)panAction:(UIScreenEdgePanGestureRecognizer *)gesture
{
    UIView *view = [self.view hitTest:[gesture locationInView:gesture.view] withEvent:nil];  // 获取手势响应的是在哪个view上
    NSLog(@"view.tag %ld, gesture.view %ld", view.tag, gesture.view.tag);
    if (UIGestureRecognizerStateBegan == gesture.state || UIGestureRecognizerStateChanged == gesture.state) {  // 判断手势状态,从而判断手势是否是连续的
        CGPoint translationPoint = [gesture translationInView:gesture.view];   //获取到的是手指移动后,在相对坐标中的偏移量
        _backgroundView.center = CGPointMake(center_x+translationPoint.x, center_y);
    } else {
        [UIView animateWithDuration:.3f animations:^{
            _backgroundView.center = CGPointMake(center_x, center_y);
        }];
        
    }
}

其中我们可以用hitTest方法来获取手势所响应的是哪个viewUIView *view = [self.view hitTest:[gesture locationInView:gesture.view] withEvent:nil]

在scrollview上面添加一个slider

事件层次分析(二)——运用案例_第10张图片

要实现滚动scrollview的时候,只是响应scrollview,点击slider的时候,只是响应slider事件,不响应scrollview。
直接添加 _scrollView.delaysContentTouches = NO;这句代码即可,从上文可知,这句代码也就是不延迟scrollview上面的touch事件,即立即响应slider事件。

有手势事件的控制器,控制器上添加tableView,不能响应tableView的didSelectRowAtIndexPath事件

如果一个控制器上添加一个UITapGestureRecognizer手势,或者它继承自的基类控制器上添加的有UITapGestureRecognizer手势,那么当这个控制器上面添加有tableView的时候,didSelectRowAtIndexPath的点击事件不会影响。

因为,tableView的didSelectRowAtIndexPath事件是由tableView的touch事件来实现的,当点击事件发生后,事件的响应一般是先响应手势事件,再是touch事件,但是默认情况下手势事件响应过后,会取消touch事件,所以tableView的didSelectRowAtIndexPath不能响应。

_tapGesture = [[RedColorTapGesture alloc] initWithTarget:self action:@selector(tapGestureEvent:)];
_tapGesture.cancelsTouchesInView = NO;  //如果为YES,手势识别了,会取消touch事件
[self.view addGestureRecognizer:_tapGesture];

加了上面代码,当将手势的cancelsTouchesInView属性设置为NO的时候,tableView的didSelectRowAtIndexPath和手势事件都会影响。

如果我们想当点击cell的时候,禁止手势事件,需要自定义一个UITapGestureRecognizer手势,如下:

//- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
//    //找到你这个手势的view,如果这个view是cell,那么手势不响应
//    UIView *view = gestureRecognizer.view;  //gestureRecognizer.view = 永远获取的是你这个手势绑定的view,所以否决
//    return NO;
//}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    UIView *view = touch.view;
    return ![view isKindOfClass:NSClassFromString(@"UITableViewCellContentView")];  //如果是这个view,不响应touch
}

需要使用下面这个代理方法,不能使用上面那个代理方法,因为获取的view是绑定手势的view,不能获取到touch事件点击的view。

相关博客

你可能感兴趣的:(事件层次分析(二)——运用案例)