有的时候,我们需要使用非规则形状的按钮。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 @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 |