美文网首页
事件层次分析(二)——运用案例

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

作者: 简_爱SimpleLove | 来源:发表于2017-04-12 16:57 被阅读53次

    view覆盖在button上的触摸案例


    如果在上图中想要点击黄色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上的触摸事件处理


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

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

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

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

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


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

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

    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

    如上图,和平常的一张图片显示整个屏幕宽不同,一个屏幕中会显示三张图片。初看可能觉得无从下手,但是我们仔细分析就会发现其实每次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<count; i++) {
            
            CGFloat imageWidth = scrollViewWidth-20.f;
            
            UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:imageArr[i]]];
            imageView.frame = CGRectMake(i*(imageWidth+20.f)+20.f, 0.f, imageWidth, scrollViewWidth/2);
            
            [scrollView addSubview:imageView];
            
        }
    }
    

    其中核心代码是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;不然会有下图现象:


    Untitled.gif

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

    解决办法有两个:

    • 解法一:
    -(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


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

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

    - (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


    要实现滚动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。

    相关博客

    相关文章

      网友评论

          本文标题:事件层次分析(二)——运用案例

          本文链接:https://www.haomeiwen.com/subject/qypsattx.html