有的時候,我們需要使用非規則形狀的按鈕。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,它們將正常工作了。