Forewarning: I am putting this up because it is good code, and a neat illustration of Objective-C's ability to descend into the depths of C some low-level bit-bashing.
If you need to implement irregularly shaped buttons, this is for you. Download the code, and the article will help you understand it. If you want to learn from it, you will need to pick through the code, and only then will my preamble be of use to you.
I followed with interest Jeff LaMarche's blog posts on creating irregularly shaped buttons:
and the follow up:
The comments in the follow-up contain some good suggestions:
- use of calloc instead of malloc -- the code as is buggy, as malloc fails to initialize elements to 0
- use of a
bitArray
instead of a byteArray
to reduce memory usage - and initial bounding box check, to reduce computation
The comments also point out that the code leaks memory.
I have put together a third generation to this project, which:
- implements all of the above suggestions
- fixes the memory leaks
- overrides
pointInside : withEvent :
rather than hitTest : withEvent :
(see documentation for hitTest
) - removes the lazy load, instead performing initialization when the
UIButton
object is initialized, or when its image is changed - allows you to set a threshold alpha level, beyond which presses will not register
This work involved a complete restructuring -- rather than have one low-level routine grab the Alpha data, creating memory for it, before throwing it to another one, etc., I have packed all of the low-level stuff into a single routine that emits an auto released NSData
object. This routine is careful to free all memory it allocates.
I took out the lazy load -- it is a bad idea to perform intensive calculation the first time an object is pressed -- this gives an inconsistent UI experience, which is exasperating. Also, by using a bitArray
, the memory footprint is reduced by eight times, so there is less point in trying to save memory. You might think it would be enough to override the setImage
and setBackgroundImage
methods of UIButton
. However, if it is created from a NIB, these methods do not get invoked. It must be that the iVar
gets set directly.
I noticed by setting breakpoints that pointInside : withEvent :
gets hit three times for each press. This is a mystery to me -- even looking at the call stack doesn't switch on the light bulb. However, it reinforces the importance of performing minimal processing to test whether the point will register a hit.
Overall, this project is a nice little exercise in memory management and optimization, and shows off the ability of Objective-C to harness the power of C. If you pick through the code, you can see it also gives some insight as to the nature of bitmaps and bitmap contexts.
@interface TestViewController : UIViewController
{ }
- (IBAction) buttonClick : (id) sender;
@end
#import "TestViewController.h"
@implementation TestViewController
- (IBAction) buttonClick : (id) sender
{
NSLog(@"Clicked:%@", sender);
}
- (void) dealloc
{
[super dealloc];
}
@end
@class AlphaMask;
@interface clickThruButton : UIButton
{
@private AlphaMask* _alphaMask;
}
@end
#import "clickThruButton.h"
#import "AlphaMask.h"
@interface clickThruButton ()
@property (nonatomic, retain) AlphaMask* alphaMask;
- (void) myInit;
- (void) setMask;
@end
@implementation clickThruButton
@synthesize alphaMask = _alphaMask;
#pragma mark init
- (void)awakeFromNib
{
[super awakeFromNib];
[self myInit];
}
- (id) initWithFrame: (CGRect) aRect
{
self = [super initWithFrame: aRect];
if (self)
[self myInit];
return self;
}
- (void) myInit
{
uint8_t threshold = 0x00;
self.alphaMask = [[AlphaMask alloc] initWithThreshold: threshold];
[self setMask];
}
#pragma mark if image changes...
- (void) setBackgroundImage: (UIImage *) _image
forState: (UIControlState) _state
{
[super setBackgroundImage: _image
forState: _state];
[self setMask];
}
- (void) setImage: (UIImage *) _image
forState: (UIControlState) _state
{
[super setImage: _image
forState: _state];
[self setMask];
}
#pragma mark Set alphaMask
-(void) setMask
{
UIImage *btnImage = [self imageForState: UIControlStateNormal];
if (btnImage == nil)
btnImage = [self backgroundImageForState: UIControlStateNormal];
if (btnImage == nil)
{
self.alphaMask = nil;
return ;
}
[self.alphaMask feedImage: btnImage.CGImage];
}
#pragma mark Hit Test!
- (BOOL) pointInside : (CGPoint) p
withEvent : (UIEvent *) event
{
if (!CGRectContainsPoint(self.bounds, p))
return NO;
bool ret = [self.alphaMask hitTest: p];
return ret;
}
#pragma mark dealloc
- (void)dealloc
{
[self.alphaMask release];
[super dealloc];
}
@end
@interface AlphaMask : NSObject
{
@private
uint8_t alphaThreshold;
size_t imageWidth;
NSData* _bitArray;
}
- (id) initWithThreshold: (uint8_t) t;
- (void) feedImage: (CGImageRef) img;
- (bool) hitTest: (CGPoint) p;
@end
#import "AlphaMask.h"
@interface AlphaMask ()
@property (nonatomic, retain) NSData* bitArray;
+ (NSData *) calcHitGridFromCGImage: (CGImageRef) img
alphaThreshold: (uint8_t) alphaThreshold_ ;
@end
@implementation AlphaMask
@synthesize bitArray = _bitArray;
#pragma mark Init stuff
- (id) initWithThreshold: (uint8_t) alphaThreshold_
{
self = [super init];
if (!self)
return nil;
alphaThreshold = alphaThreshold_;
self.bitArray = nil;
imageWidth = 0;
return [self init];
}
- (void) feedImage: (CGImageRef) img
{
self.bitArray = [AlphaMask calcHitGridFromCGImage: img
alphaThreshold: alphaThreshold];
imageWidth = CGImageGetWidth(img);
}
#pragma mark Hit Test!
- (bool) hitTest: (CGPoint) p
{
const uint8_t c_0x01 = 0x01;
if (!self.bitArray)
return NO;
uint8_t * pBitArray = (uint8_t *) [self.bitArray bytes];
size_t N = p.y * imageWidth + p.x;
size_t n = N / (size_t) 8;
uint8_t thisPixel = *(pBitArray + n) ;
uint8_t mask = c_0x01 << (N % 8);
return (thisPixel & mask) ? YES : NO;
}
#pragma mark Extract alphaMask from image!
+ (NSData *) calcHitGridFromCGImage: (CGImageRef) img
alphaThreshold: (uint8_t) alphaThreshold_
{
CGContextRef alphaContext = NULL;
void * alphaGrid;
size_t w = CGImageGetWidth(img);
size_t h = CGImageGetHeight(img);
size_t bytesCount = w * h * sizeof(uint8_t);
alphaGrid = calloc (bytesCount, sizeof(uint8_t));
if (alphaGrid == NULL)
{
fprintf (stderr, "calloc failed!");
return nil;
}
alphaContext = CGBitmapContextCreate
(alphaGrid, w, h, 8, w, NULL, kCGImageAlphaOnly);
if (alphaContext == NULL)
{
free (alphaGrid);
fprintf (stderr, "Context not created!");
return nil;
}
CGRect rect = {{0,0},{w,h}};
CGContextDrawImage(alphaContext, rect, img);
void* _alphaData = CGBitmapContextGetData (alphaContext);
if (!_alphaData)
{
CGContextRelease(alphaContext);
free (alphaGrid);
return nil;
}
uint8_t *alphaData = (uint8_t *) _alphaData;
size_t srcBytes = bytesCount;
size_t destBytes = srcBytes / (size_t) 8;
if (srcBytes % 8)
destBytes++;
uint8_t* dest = malloc (destBytes);
if (!dest)
{
CGContextRelease(alphaContext);
free (alphaGrid);
fprintf (stderr, "malloc failed!");
return nil;
}
size_t iDestByte = 0;
uint8_t target = 0x00, iBit = 0, c_0x01 = 0x01;
for (size_t i=0; i < srcBytes; i++)
{
uint8_t src = *(alphaData++);
if (src > alphaThreshold_)
target |= (c_0x01 << iBit);
iBit++;
if (iBit > 7)
{
dest[iDestByte] = target;
target = 0x00;
iDestByte++;
iBit = 0;
}
}
NSData* ret = [NSData dataWithBytes: (const void *) dest
length: (NSUInteger) destBytes ];
CGContextRelease (alphaContext);
free (alphaGrid);
free (dest);
return ret;
}
@end