Note: There is an improved version of the code from this blog post right here.
You probably know that UIButton allows you to select an image or background image with alpha, and it will respect the alpha. For example, if I create four images that look like this:
I can then use create custom buttons in Interface Builder using these images, and whatever is behind the transparent parts of the button will show through (assuming the button is not marked
opaque. However,
UIButton's hit-testing doesn't take the transparency into account, which means if you overlap these buttons in Interface Builder so they look like this, for example:
If you click here:
The default hit-testing is going to result in the green diamond button getting pressed, not the blue one. While this might be what you want some of the time, typically this won't be the behavior want. So, how do you get it to work like that? It's actually pretty easy, you just need to subclass
UIButton and override the hit testing method.
But, first, we need a way to determine if a given point on an image is transparent. Unfortunately,
UIImage is an opaque type without a mechanism to give us easy access to the bitmap data the way
NSBitmapRepresentation does for
NSImages in Cocoa. But, every
UIImage instance does have a property called
CGImage that gives us access to the underlying image data, and Apple has very nicely published a
tech note telling how to get access to the underlying bitmap data from a CGImageRef.
Using the information in that technote, we can easily craft a category on
UIImage with a method that takes a
CGPoint as an argument and returns either
YES or
NO depending on whether the alpha value that corresponds to that point is transparent (
0).
UIImage-Alpha.h#import <UIKit/UIKit.h>
@interface UIImage(Alpha)
- (NSData *)ARGBData;
- (BOOL)isPointTransparent:(CGPoint)point;
@end
UIImage-Alpha.mCGContextRef 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; return [NSData dataWithBytes:data length:dataSize];
}
- (BOOL)isPointTransparent:(CGPoint)point
{
NSData *rawData = [self ARGBData]; 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
Once we have the ability to tell if a particular point on an image is transparent, we can then create our own subclass of
UIButton and override the
hitTest:withEvent: method to do a slightly more sophisticated hit test than
UIButton's. The way this works is that we need to return an instance of
UIView. If the point is not a hit on this view or one of its subclasses, we return
nil.. If it's a hit on a subview, we return the subview that was hit, and if it's a hit on this view, we return
self.
However, we can simplify this a little because, although
UIButton, inherits from
UIView and can technically have subviews, it is exceedingly uncommon to do so and, in fact, Interface Builder won't allow it. So, we don't have to worry about subviews in our implementation unless we're doing something really unusual. Here's a simple subclass of
UIButton that does hit-testing based on the alpha channel of the image or background image of the button, but assumes there are no subviews.
IrregularShapedButton.h#import <UIKit/UIKit.h>
@interface IrregularShapedButton : UIButton {
}
@end
IrregularShapedButton.m#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) displayedImage = [self backgroundImageForState:[self state]];
if (displayedImage == nil) return self;
BOOL isTransparent = [displayedImage isPointTransparent:point];
if (isTransparent)
return nil;
}
return self;
}
@end
If we change the class of the four image buttons in Interface Builder from
UIImage to
IrregularShapedButton, they will work as expected. You can try the code out by
downloading the Xcode project. Improvements and bug-fixes are welcome.
Curiously, the documentation for hitTest:withEvent: in UIView says This method ignores views that are hidden, that have disabled user interaction, or have an alpha level less than 0.1.. In my testing, this is actually not true, though I am unsure whether it's a documentation bug or an implementation bug.
Update: My Google-Fu failed me. I did search for existing implementations and tutorials about this subject before I wrote the posting (I hate reinventing the wheel), but I failed to find
Ole Begemann's implementation of this from a few months ago. It's worth checking out
his implementation to see different approaches to solving the same problem. There's also some discussion in the comments about the differences in our implementations that may be of interest if you like knowing the nitty-gritty details. Plus, his diamonds are prettier than mine.
Update 2: Alfons Hoogervorst tweaked the code and
showed how you could reduce the overhead by creating an alpha-only context.