有的时候,我们需要使用非规则形状的按钮。UIButton允许你选择带有alpha通道的图像。比如,我使用下面四个图像:


image


然后用Interface Builder创建用户定义按钮,你可以透过图像的透明部分看到后面的按钮(假定按钮未定义为opaque)。.然而 UIButton 的点击测试(hit-testing)并未考虑图像的透明性,所以当你将图像重叠放置时,如图所示:


image


如果你点击此处:


image



默认的点击测试的结果是绿色菱形按钮被按下,而不是蓝色按钮。当然这可能就是你需要的效果,但大部分情况下并非如你所愿。那么怎样才能让你的程序正常工作?实际上很简单,你只需要一个UIButton的子类并重写点击测试方法。

然而,首先你需要一个方法能确定图像上指定点是透明的。遗憾的是UIImage无法像Cocoa为NSImage提供的NSBitmapRepresentation 那样方便地访问位图数据。但是每个UIImage都具有一个称为CGImage的属性可以访问内部图像数据,Apple发布了一篇技术文章介绍了怎样通过CGImageRef访问内部位图数据


根据这篇文章的介绍,我们很容易就写出一个方法,它以CGPoint为参数,根据该点是否透明(0)与否返回YES或NO。


UIImage-Alpha.h

1
2
3
4
5
6
7
8
#import

@interface UIImage(Alpha)

- (NSData *)ARGBData;
- (BOOL)isPointTransparent:(CGPoint)point;

@end


UIImage-Alpha.m

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
CGContextRef CreateARGBBitmapContext (CGImageRef inImage)
{
    CGContextRef    context = NULL;
    CGColorSpaceRef colorSpace;
    void *          bitmapData;
    int             bitmapByteCount;
    int             bitmapBytesPerRow;

    size_t pixelsWide = CGImageGetWidth(inImage);
    size_t pixelsHigh = CGImageGetHeight(inImage);
    bitmapBytesPerRow   = (pixelsWide * 4);
    bitmapByteCount     = (bitmapBytesPerRow * pixelsHigh);

    colorSpace = CGColorSpaceCreateDeviceRGB();
    if (colorSpace == NULL)
        return nil;

    bitmapData = ( bitmapByteCount );
    if (bitmapData == NULL)
    {
       CGColorSpaceRelease( colorSpace );
       return nil;
    }

    context = CGBitmapContextCreate (bitmapData,
                                     pixelsWide,
                                     pixelsHigh,
                                     8,
                                     bitmapBytesPerRow,
                                     colorSpace,
                                     kCGImageAlphaPremultipliedFirst);

    if (context == NULL)
    {
        (bitmapData);
        (stderr"Context not created!");
    }

    CGColorSpaceRelease( colorSpace );

    return context;
}

@implementation UIImage(Alpha)

- (NSData *)ARGBData
{
    CGContextRef cgctx = CreateARGBBitmapContext(self.CGImage);
    if (cgctx == NULL)         
        return nil;

    size_t w = CGImageGetWidth(self.CGImage);
    size_t h = CGImageGetHeight(self.CGImage);
    CGRect rect = {{0,0},{w,h}};
    CGContextDrawImage(cgctx, rect, self.CGImage)

    void *data = CGBitmapContextGetData (cgctx);
    CGContextRelease(cgctx);     

    if (!data)       
        return nil;

    size_t dataSize = 4 * w * h; // ARGB = 4 8-bit components
    return [NSData dataWithBytes:data length:dataSize];
}    

- (BOOL)isPointTransparent:(CGPoint)point
{
    NSData *rawData = [self ARGBData];  // See about caching this
    if (rawData == nil)
       return NO;

    size_t bpp = 4;
    size_t bpr = self.size.width * 4;

    NSUInteger index = point.x * bpp + (point.y * bpr);
    char *rawDataBytes = (char *)[rawData bytes];

    return rawDataBytes[index] == 0;

}

@end

一旦我们有能力确定图像中的某点是否透明,我们就可以编写UIButton的子类,重写hitTest:withEvent: 方法。它将返回一个UIView的实例。如果该点在此视图或其子视图中未被点击,那么将返回nil。如果点击在其子视图,那么将返回点击中的子视图,如果点击中视图,那么返回视图本身。


然而,我们可以进行一些简化,这是因为尽管UIButton继承了UIView,技术上可能具有子视图,但这非常的少见,而且Interface Builder并不支持这样做。所以在本文的实现中并不考虑子视图。


IrregularShapedButton.h

1
2
3
4
5
6
7
#import

@interface IrregularShapedButton : UIButton {

}

@end

IrregularShapedButton.m

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
#import "IrregularShapedButton.h"
#import "UIImage-Alpha.h"

@implementation IrregularShapedButton

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (!CGRectContainsPoint([self bounds], point))
        return nil;
    else
    {
        UIImage *displayedImage = [self imageForState:[self state]];
        if (displayedImage == nil) // No image found, try for background image
        displayedImage = [self backgroundImageForState:[self state]];
        if (displayedImage == nil) // No image could be found, fall back to
            return self;        

        BOOL isTransparent = [displayedImage isPointTransparent:point];
        if (isTransparent)
            return nil;

    }

    return self;
}

@end
将Interface Builder中的四个图像按钮改为IrregularShapedButton,它们将正常工作了。 原文见:Irregularly Shaped UIButton