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;不然会有下图现象:
但是这样会有一个问题就是点击屏幕边缘的时候就滑动不了,因为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。