Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / iPhone

Irregularly Shaped Buttons

4.33/5 (5 votes)
10 Sep 2010CPOL2 min read 42.8K   492  
Irregularly Shaped Buttons
IrregButtonBig.png

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.

C++
//
//  TestViewController.h
//  Test
//
//  Pi

@interface TestViewController : UIViewController 
{ }

- (IBAction) buttonClick : (id) sender;

@end

//
//  TestViewController.m
//  Test
//
//  Pi

#import "TestViewController.h"

@implementation TestViewController

- (IBAction) buttonClick : (id) sender
{
    NSLog(@"Clicked:%@", sender);
}

- (void) dealloc 
{
    [super dealloc];
}

@end

//
//  ClickThruButton.h
//  Test
//
//  Pi

@class AlphaMask;

@interface clickThruButton : UIButton 
{
    @private AlphaMask* _alphaMask;
}

@end

//
//  ClickThruButton.m
//  Test
//
//  Pi

#import "clickThruButton.h"
#import "AlphaMask.h"

@interface clickThruButton ()

@property (nonatomic, retain) AlphaMask* alphaMask;

- (void) myInit;
- (void) setMask;

@end

@implementation clickThruButton

@synthesize alphaMask = _alphaMask;

/*
 To make this object versatile, we should allow for the possibility 
 that it is being used from IB, or directly from code. 
 By overriding both these functions, we can ensure that 
 however it is created, our custom initializer gets called.
 */
#pragma mark init
// if irregButtons created from NIB
- (void)awakeFromNib
{
    [super awakeFromNib];
    [self myInit];    
}

// if irregButtons created or modified from code...
- (id) initWithFrame: (CGRect) aRect
{
    self = [super initWithFrame: aRect];
    if (self) 
        [self myInit];    
    return self;    
}

- (void) myInit
{
    // Set so that any alpha > 0x00 (transparent) sinks the click
    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
/*
 Note that we get redirected here from both our custom initializer 
 and the image setter methods which we have overridden.
 
 We can't just override the setters -- if the object is loading from a 
 NIB these methods don't fire. Clearly it must set the iVars directly.
 
 This method should get invoked every time the buttons image changes.
 Because it needs to extract, process and compress the Alpha data, 
 in a way that our hit tester can access quickly.
 */
-(void) setMask
{
    UIImage *btnImage = [self imageForState: UIControlStateNormal];
    
    // If no image found, try for background image
    if (btnImage == nil) 
        btnImage = [self backgroundImageForState: UIControlStateNormal];
    
    if (btnImage == nil)  
    {
        self.alphaMask = nil;
        return ;
    }
    
    [self.alphaMask  feedImage: btnImage.CGImage];
}

#pragma mark Hit Test!
/* override pointInside:withEvent:
 Notice that we don't directly override hitTest. If you look at the 
 documentation you will see that this button's PARENT's hit tester 
 will check the pointInside methods of one of its children.
 */
- (BOOL) pointInside : (CGPoint) p  
           withEvent : (UIEvent *) event
{
    // Optimization check -- bounding box
    if (!CGRectContainsPoint(self.bounds, p))
        return NO;
    
    // Checks the point against alphaMask's precalculated bit array, 
    // to determine whether this point is allowed to register a hit
    bool ret = [self.alphaMask  hitTest: p];
    
    // If yes, send ' yes ' back to the parents hit tester, 
    // which will be one level up the call stack.  
    // So in this example, the parent will be the view, 
    // and it will check through all of its children until 
    // it finds one that responds with ' yes '
    return ret;
}

#pragma mark dealloc
- (void)dealloc
{
    [self.alphaMask release];
    [super dealloc];
}
@end

//
//  imageHelper.h
//  test
//
//  Pi

@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;

// Private methods and properties defined in the .m

@end

//
//  imageHelper.m
//  test
//
//  Pi

#import "AlphaMask.h"

// Private methods discussion here:
// http://stackoverflow.com/questions/172598/
//best-way-to-define-private-methods-for-a-class-in-objective-c
//Objective-C doesn't directly support private methods. Using an 
// empty category is an acceptably hacky way to achieve this effect.
@interface  AlphaMask () // <-- empty category

// each bit represents 1 pixel: will hold Yes/No for click-thru, 1=hit 0=click-thru
@property (nonatomic, retain) NSData* bitArray;

// note + means STATIC method
+ (NSData *) calcHitGridFromCGImage: (CGImageRef) img
           alphaThreshold: (uint8_t) alphaThreshold_ ;
@end

@implementation AlphaMask

@synthesize bitArray = _bitArray;

#pragma mark Init stuff
/*
 See below for a more detailed discussion on alphaThreshold.  
 Basically if you set it to 0, the hit tester will only 
 pass through pixels that are 100% transparent
 
 Setting it to 64 would pass through all pixels 
 that are less than 25% transparent
 
 255 is the maximum. Setting to this, the image cannot 
 take a hit -- everything passes through.
 */
- (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!
/*
 Ascertains, through looking up the relevant bit in our bit array 
 that pertains to this pixel, whether the pixel should take the hit
 (bit set to 1) or allow the click to pass through (bit set to 0).
 In order to minimize overhead, I am playing with C pointers directly.
 
 Note: for some reason, iOS seems to be hit testing each object
 three times -- which is bizarre, and another good reason for 
 spending as little time as possible inside this function.
 */
- (bool) hitTest: (CGPoint) p
{
    const uint8_t c_0x01 = 0x01; 
    
    if (!self.bitArray)
        return NO;
    
    // location of first byte
    uint8_t * pBitArray = (uint8_t *) [self.bitArray bytes];
    
    // the N'th pixel will lie in the n'th byte (one byte covers 8 pixels)
    size_t N = p.y * imageWidth + p.x;
    size_t n = N / (size_t) 8;
    uint8_t thisPixel = *(pBitArray + n) ;
    
    // mask with the bit we want
    uint8_t mask = c_0x01 << (N % 8);
    
    // nonzero => Yes absorb HIT, zero => No - click-thru
    return (thisPixel & mask) ? YES : NO;
}

#pragma mark Extract alphaMask from image!
// Constructs a compressed bitmap (one bit per pixel) that stores for each pixel
//     whether that pixel should accept the hit, or pass it through.
// If the pixels alpha value is zero, the pixel is transparent
// if the pixels alpha value > alphaThreshold, the corresponding bit is set to 1, 
//     indicating that this pixel is to receive a hit
//Note that setting alphaThreshold to 0 means that any pixel that is not 
//     100% transparent will receive a hit
+ (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);
    
    // allocate AND ZERO (so can't use malloc) memory for alpha-only context
    alphaGrid = calloc (bytesCount, sizeof(uint8_t));
    if (alphaGrid == NULL) 
    {
        fprintf (stderr, "calloc failed!");
        return nil;
    }
    
    // create alpha-only context
    alphaContext = CGBitmapContextCreate 
    	(alphaGrid, w, h, 8,   w, NULL, kCGImageAlphaOnly);
    if (alphaContext == NULL)
    {
        free (alphaGrid);
        fprintf (stderr, "Context not created!");
        return nil;
    } 
    
    // blat image onto alpha-only context
    CGRect rect = {{0,0},{w,h}}; 
    CGContextDrawImage(alphaContext, rect, img); 
    
    // grab alpha-only image-data
    void* _alphaData = CGBitmapContextGetData (alphaContext);
    if (!_alphaData)
    {
        CGContextRelease(alphaContext); 
        free (alphaGrid);
        return nil;
    }
    uint8_t *alphaData = (uint8_t *) _alphaData;
    
    // ---------------------------
    // compress to 1 bit per pixel
    // ---------------------------
        
    size_t srcBytes = bytesCount;
    size_t destBytes = srcBytes / (size_t) 8;
    if (srcBytes % 8)
        destBytes++;
    
    // malloc ok here, as we zero each target byte
    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++);
        
        // set bit to 1 for 'takes hit', leave on 0 for 'click-thru'
        // alpha 0x00 is transparent
        // comparison fails famously if not using UNSIGNED data type
        if (src > alphaThreshold_)
            target |= (c_0x01 << iBit);
        
        iBit++;
        if (iBit > 7) 
        {
            dest[iDestByte] = target;
            target = 0x00;
            
            iDestByte++;
            iBit = 0;
        }
    }
    
    // COPIES buffer
    // is AUTORELEASED!
    // http://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/
    // MemoryMgmt/Articles/mmRules.html#//apple_ref/doc/uid/20000994-BAJHFBGH
    NSData* ret = [NSData dataWithBytes: (const void *) dest 
                                 length: (NSUInteger) destBytes ];
    
    CGContextRelease (alphaContext);
    free (alphaGrid);
    free (dest);
    
    return ret;
}

@end
IrregButton.png

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)