I have often tried to create a UIButton subclass to do something special, like draw a shadow, but couldn’t make it work. You can’t just override UIView’s drawRect: method and get the results you expect. My usual response to that is: Fine, I’ll make my button a subclass of UIControl, then.
That leaves the problem of how to highlight the button’s image when it is pressed. UIButton does a great job of this, drawing a black mask on top of the button’s image, ignoring any transparent pixels. I’ve been trying to figure out how to reproduce that effect for probably a year, I suspect, without success.
CoreGraphics is powerful. There’s pretty much nothing it can’t do. But it’s not exactly easy to work with. I have finally read enough of other people’s blog posts so that I can cobble together a solution. I’ve included a function below that inputs a UIImage object and outputs a new UIImage with a black mask drawn over it, exactly the same effect that UIButton uses.
I’ve included a screen-shot from the simulator to show what the effect looks like. On the left, you’ll see an image that is similar to the icon for the Calendar program on the iPhone. Next is that same image, highlighted by UIButton. The third image is highlighted with the function shown below. The two highlighted versions are almost exactly the same. You can tell the difference with a color-dropper tool, but I doubt you could tell them apart with the naked eye.
I know I pretty much never use code from other people’s blog posts without modifying it to suit my own uses. Assuming you’re like that as well, here’s some notes that will help you understand the code better.
The alpha value for the black mask, 0.46, was chosen because it produces results nearly identical to what UIButton does. You can make that number bigger or smaller for a lighter or darker mask.
UIGraphicsBeginImageContextWithOptions() is the best function to use to begin an image context. It will get you high-resolution graphics on the iPhone 4’s “retina display.” But that function does not exist on older versions of iOS. Therefore, we test for its existence before calling it, by checking to see if its function pointer is NULL. If so, we fall back to the older method that’s been available since the earliest iOS versions. If you don’t plan to support versions of iOS earlier than 4.0, then you can omit the availability test.
There are a couple of lines of code that transform the coordinates normally used by CoreGraphics functions to those normally used by UIKit objects. Without those two lines, the black mask would be drawn upside-down. I don’t understand this business very well. I just know the function fails without them.
The secret sauce is CGContextClipToMask(). It was easy to figure out how to draw a colored mask over an existing image, but you need this extra step to prevent the mask from being drawn over transparent pixels.
UIImage* WBHighlightImage(UIImage* image) { const CGSize size = image.size; const CGRect bnds = CGRectMake(0.0, 0.0, size.width, size.height); UIColor* colr = nil; UIImage* copy = nil; CGContextRef ctxt = NULL; // this is the mask color colr = [[[UIColor alloc] initWithWhite:0 alpha:0.46] autorelease]; // begin image context if (UIGraphicsBeginImageContextWithOptions == NULL) { UIGraphicsBeginImageContext(bnds.size); } else { UIGraphicsBeginImageContextWithOptions(bnds.size, FALSE, 0.0); } ctxt = UIGraphicsGetCurrentContext(); // transform CG* coords to UI* coords CGContextTranslateCTM(ctxt, 0.0, bnds.size.height); CGContextScaleCTM(ctxt, 1.0, -1.0); // draw original image CGContextDrawImage(ctxt, bnds, image.CGImage); // draw highlight overlay CGContextClipToMask(ctxt, bnds, image.CGImage); CGContextSetFillColorWithColor(ctxt, colr.CGColor); CGContextFillRect(ctxt, bnds); // finish image context copy = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return copy; }