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

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。

相关博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容