如何自定义一个不规则的Button

有些时候我们会碰到自定义不规则 button 的需求,我们会采用设置 button 图片的方式来解决,但是这种方式针对饼状图按钮或者其他复杂的样式实现起来就会有困难,而且这种方式在响应区域上也存在一定的问题。下面我就和大家分享一个比较好的方法。

原理

iOS 系统当中的事件响应是有一个特殊的响应链和查找规则来确定的,今天我们要涉及的是一个叫做 hit-Testing 的方式。

在 hit-Testing 当中,View 按照相应规则查找响应控件时会调用

1
2
3
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event; // default returns YES if point is in bounds

这两个方法。在这两个方法当中会有操作在当前视图坐标系当中的位置,我们可以根据位置确定该操作是否响应,进而达到实现不规则的响应区域。

实现

以一个响应区域为半径 20 的圆为例,简单写一下实现。(其他图形也都可以通过数学方式表达出来)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
//当父类返回为NO时直接返回(操作在该控件外)
BOOL superResult = [super pointInside:point withEvent:event];
if (!superResult) {
return superResult;
}

return [self checkIsResponse:point];
}

/**
* 是否响应的计算
*
* @param point 操作的point
*
* @return 是否响应
*/
- (BOOL)checkIsResponse:(CGPoint)point
{
//圆心为(20,20),半径为20
if (pow(point.x-20, 2) + pow(point.y-20, 2) <= pow(20, 2)) {
return YES;
}else{
return NO;
}
}

其他比较特殊的都可以在上文提到的两个方法当中采用数学计算实现。

优化

上面提到的

1
2
3
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event; // default returns YES if point is in bounds

这两个方法,视图当中有几个子视图就会调用多少次,因为后续的都是对同一个操作点进行计算,没有必要,故代码修改如下:(参考了 OBShapedButton 的写法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@interface HitTestView ()

@property (nonatomic, assign) CGPoint previousTouchPoint;
@property (nonatomic, assign) BOOL previousTouchHitTestResponse;

@end

@implementation HitTestView

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
//当父类返回为NO时直接返回(操作在该控件外)
BOOL superResult = [super pointInside:point withEvent:event];
if (!superResult) {
return superResult;
}

return [self checkIsResponse:point];
}

/**
* 是否响应的计算
*
* @param point 操作的point
*
* @return 是否响应
*/
- (BOOL)checkIsResponse:(CGPoint)point
{
if (CGPointEqualToPoint(point, self.previousTouchPoint)) {
return self.previousTouchHitTestResponse;
} else {
self.previousTouchPoint = point;
}

//圆心为(20,20),半径为20
if (pow(point.x-20, 2) + pow(point.y-20, 2) <= pow(20, 2)) {
self.previousTouchHitTestResponse = YES;
return self.previousTouchHitTestResponse;
}else{
self.previousTouchHitTestResponse = NO;
return self.previousTouchHitTestResponse;
}
}

@end

这样减少了不必要的计算,提高了效率。

多说两句

OBShapedButton 采用的通过按钮图片来判断对应的响应区域,如果没有比较特殊的需求的话,OBShapedButton 是一个不错的选择。

参考博文和资料:

https://github.com/ole/OBShapedButton