有的时候,我们需要使用非规则形状的按钮。UIButton允许你选择带有alpha通道的图像。比如,我使用下面四个图像:
然后用Interface Builder创建用户定义按钮,你可以透过图像的透明部分看到后面的按钮(假定按钮未定义为opaque)。.然而 UIButton 的点击测试(hit-testing)并未考虑图像的透明性,所以当你将图像重叠放置时,如图所示:
如果你点击此处:
默认的点击测试的结果是绿色菱形按钮被按下,而不是蓝色按钮。当然这可能就是你需要的效果,但大部分情况下并非如你所愿。那么怎样才能让你的程序正常工作?实际上很简单,你只需要一个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 <UIKit/UIKit.h>
@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 = malloc ( bitmapByteCount ); if (bitmapData == NULL ) { CGColorSpaceRelease ( colorSpace ); return nil; } context = CGBitmapContextCreate (bitmapData, pixelsWide, pixelsHigh, 8, bitmapBytesPerRow, colorSpace, kCGImageAlphaPremultipliedFirst ); if (context == NULL ) { free (bitmapData ); fprintf ( 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 <UIKit/UIKit.h>
@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,它们将正常工作了。