有的时候,我们需要使用非规则形状的按钮。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,它们将正常工作了。