相信做过这个需求的都会被这个问题困扰过,具体问题如下;
当点击一个按钮后不松开,继续滑动手指的话,如果手指滑动出按钮的范围,按钮应该从高亮状态恢复到正常状态,并且在按钮范围之外松开手指,按钮应该对应的ControlEvents事件应该是:UIControlEventTouchUpOutside。
但是实际上UIButton并不是这样的,它在这种情况下,会有一个按钮范围之外的外围,在这个范围内拖动,让然会当左在按钮范围内处理,在这个范围内松开手指,收到的事件是:UIControlEventTouchUpInside。
我们需要的场景应该是这样的:
当然可以自定义一个控件来实现,但是如果要做到跟UIButton一样,支持各种事件的监听、控件对应的UI状态变化等是一个特别大工程,而且最后还不一定比UIButton好用。UIButton已经提供一套很完善的按钮功能,这里优先的方案肯定是解决掉UIButton的这个任性的小毛病。
网上也搜索过很多,不过基本都没有完美的,要么就是解决滑出的监听,但解决不了再次滑入的恢复,要么就是会丢失调用一个Event事件的回调。
具体调试和实验的过程非常繁琐,而且都是API的调用,并没有太多值得说的地方,直接就列出来代码了,拷贝到自己的继承自UIButton的子类就可以了:
/// 修改按钮滑动范围
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesMoved");
UITouch *touch = touches.anyObject;
BOOL isCurInRect = CGRectContainsPoint(self.bounds, [touch locationInView:self]);
BOOL isPreInRect = CGRectContainsPoint(self.bounds, [touch previousLocationInView:self]);
if (!isCurInRect) { // 现在在外面
if (isPreInRect) { // 之前在里边
// 从里边滑动到外边
} else { // 之前在外边
// 在按钮外拖动
// 在按钮范围外拖动手动发送UIControlEventTouchDragOutside事件
[self sendActionsForControlEvents:UIControlEventTouchDragOutside];
}
} else { // 现在在里边
if (!isPreInRect) { // 之前在外边
// 从外边滑动到里边
// 从按钮范围外滑动回按钮范围内,需要手动调用touchesBegan方法,让按钮进入高亮状态,并开启UIControl的事件监听
//[self beginTrackingWithTouch:touch withEvent:event];
[self touchesBegan:[NSSet setWithObject:touch] withEvent:event];
} else { // 之前在里边
// 在按钮内拖动
}
}
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesEnded");
UITouch *touch = touches.anyObject;
CGPoint point = [touch locationInView:self];
// 如果松开手指后在按钮范围之外
if (!CGRectContainsPoint(self.bounds, point)) {
// 手动触发UIControlEventTouchUpOutside事件
[self sendActionsForControlEvents:UIControlEventTouchUpOutside];
}
[super touchesEnded:touches withEvent:event];
}
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
NSLog(@"continueTrackingWithTouch");
BOOL isCurInRect = CGRectContainsPoint(self.bounds, [touch locationInView:self]);
BOOL isPreInRect = CGRectContainsPoint(self.bounds, [touch previousLocationInView:self]);
if (!isCurInRect) { // 现在在外面
if (isPreInRect) { // 之前在里边
// 从里边滑动到外边
// 从按钮范围内滑动到按钮范围外手动触发UIControlEventTouchDragExit事件并阻断按钮默认事件的执行
[self sendActionsForControlEvents:UIControlEventTouchDragExit];
// 阻断按钮默认事件的事件的执行后,需要手动触发touchesCancelled方法,让按钮从高亮状态变成默认状态
[self touchesCancelled:[NSSet setWithObject:touch] withEvent:event];
return NO;
} else { // 之前在外边
// 在按钮外拖动
// 在按钮范围外滑动时,需要手动触发touchesCancelled方法,让按钮从高亮状态变成默认状态,并阻断按钮默认事件的执行
[self touchesCancelled:[NSSet setWithObject:touch] withEvent:event];
return NO;
}
} else { // 现在在里边
if (!isPreInRect) { // 之前在外边
// 从外边滑动到里边
// 从按钮范围外滑动到按钮范围内,需要手动触发UIControlEventTouchDragEnter事件
[self sendActionsForControlEvents:UIControlEventTouchDragEnter];
return [super continueTrackingWithTouch:touch withEvent:event];
} else { // 之前在里边
// 在按钮内拖动
return [super continueTrackingWithTouch:touch withEvent:event];
}
}
return [super continueTrackingWithTouch:touch withEvent:event];
}
复制代码
现在,以下的ControlEvents全部能够完美的得到正确的回调:
[button addTarget:self action:@selector(touchDownAction) forControlEvents:UIControlEventTouchDown];
[button addTarget:self action:@selector(touchUpInsideAction) forControlEvents:UIControlEventTouchUpInside];
[button addTarget:self action:@selector(touchUpOutsideAction) forControlEvents:UIControlEventTouchUpOutside];
[button addTarget:self action:@selector(touchCancelAction) forControlEvents:UIControlEventTouchCancel];
[button addTarget:self action:@selector(touchDragInsideAction) forControlEvents:UIControlEventTouchDragInside];
[button addTarget:self action:@selector(touchDragOutsideAction) forControlEvents:UIControlEventTouchDragOutside];
[button addTarget:self action:@selector(touchDragEnterAction) forControlEvents:UIControlEventTouchDragEnter];
[button addTarget:self action:@selector(touchDragExitAction) forControlEvents:UIControlEventTouchDragExit];
复制代码