Friday, June 10, 2011

iPhone Glossy Buttons

I've recently finished as small "calculator" like application, for which I wanted very much the look of the in-built iPhone calculator app, with its matte black plastic surface and inset buttons with a raised glossy look.




The two points I was after was the "rounded" gloss on the top, and the neat highlights at the top and bottom to give the button the appearence of being inset in in the surface and rising above it.

So I looked everywhere on the net for code for such a button, without success. Plenty of blogs (this one and this one and rays awesome blog) describe nice buttons much like the "delete" button, or the slide button, which have a transverse shading only.








Anyway, I had to come out with my own code, and this meant understanding how Core Graphics does clipping - not a small thing! You can do two types of clipping with Core Graphics, using either CGContextClip or CGContextEOClip. If you look a the Apple Developer notes, they are confusing:


CGContextEOClip:
The function uses the even-odd rule to calculate the intersection of the current path with the current clipping path. Quartz then uses the path resulting from the intersection as the new current clipping path for subsequent painting operations.

CGContextClip:
The function uses the nonzero winding number rule to calculate the intersection of the current path with the current clipping path. Quartz then uses the path resulting from the intersection as the new current clipping path for subsequent painting operations.

Confusing huh? I'll try and explain MY understanding, and then we'll go through what I did to get my outcome.


Firstly, CGCOntextEOClip takes each path that has been added to the current context, and clips the paths in an even odd fashion. So the first path is drawn. Then the region covered by the second path is removed. Then the region covered by the next path is added back... and so on.


Pretty simple. But only relatively useful. For the glossy button, we want our fill to be the intersection of TWO paths - the button shape, and the glossy curved area. Even-Odd won't work for this. So We have to use a winding count clip, which is much harder to understand. I've copied these explanations which kind of work for me:



Winding count is the default way that CoreGraphics determines if a pixel is inside or outside a path. It works like this:
  1. CoreGraphics draws every horizontal row within the path's bounding rectangle from left-to-right
  2. At the start of each row, CoreGraphics sets the winding count for the shape to zero.
  3. If CoreGraphics crosses a line in the shape at any point during the row, it notes if the line was going upwards or downwards at the point where CoreGraphics crossed it.
  4. An upward line increases the winding count of the shape by 1.
  5. A downward line decreases the winding count of the shape by 1.
  6. If the winding count for the shape is ever non-zero (positive or negative) then pixels are filled according to the color of the shape.
Technically this isn't what we want either. We want the intersection of (i) the buttons shape and (ii) the circular section that makes the gloss shape:

So instead we draw either shape, and then clip. Then draw then next shape, and clip again. In fact, the direction of the curves doesnt matter when we do it this way, as it does when you add two paths together and then clip.

void drawCurvedGloss(CGContextRef context, CGRect rect, CGFloat radius) {

CGColorRef glossStart = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.6].CGColor;
CGColorRef glossEnd = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.1].CGColor;

CGMutablePathRef glossPath = CGPathCreateMutable();

CGContextSaveGState(context);
    CGPathMoveToPoint(glossPath, NULL, CGRectGetMidX(rect), CGRectGetMinY(rect)-radius+rect.size.height/2);
CGPathAddArc(glossPath, NULL, CGRectGetMidX(rect), CGRectGetMinY(rect)-radius+rect.size.height/2, radius, 0.75f*M_PI, 0.25f*M_PI, YES);
CGPathCloseSubpath(glossPath);
CGContextAddPath(context, glossPath);
CGContextClip(context);
CGContextAddPath(context, createRoundedRectForRect(rect, 6.0f));
CGContextClip(context);

CGRect half = CGRectMake(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height/2);    


drawLinearGradient(context, half, glossStart, glossEnd);
CGContextRestoreGState(context);
    
}

Here is our result:




 Neat huh?


Code is available at github

17 comments:

ryan said...

Could you put the actual code up on github, rather than a zip file with the code?

Noodle said...

Hi Ryan. Sure - I'm curious as to why it might be better that why. I suppose its just the way git is supposed to work? I can't say I like git much...

C├ędric Luthi said...

As ryan said, you missed the point of GitHub. By the way, that's not just the way git is supposed to work, that's how any version control system works: you check in the source files, not a (zip) archive of your source files. This allows for nice things, like seeing a diff between two commits.

This is impossible with a zip file.

Noodle said...

Actually I haven't missed the point of GitHub - just the way you think I should use it here. I've used it for version control with developments I've done(as well as SVN adn CVS), not sure why I would want version control here. I just wanted to provide users with the files, and the iOS dev community seems to use git.

Bird said...

It doesn't matter where the file is hosted if it's just a zip. There is no point in using source control at all. There are far easier ways to host a zip file than pushing it to a github repo.

The advantage of github is that people can watch it, see how many other people are watching it, see how it changes and how it is forked. They can fork it themselves and make the modifications they like and send you a pull request. The code can grow and expand to adapt new versions of the sdk. There are so many reasons to source control the code.

Thanks so much for sharing the code though. The buttons look great!!

Victor said...

All this talk about git and so few comments on how cool your code is. People...appreciate! I see this code is just for the gloss effect. How did you create the background for the buttons, images?

Noodle said...

Thanks Victor. Im not sure what you mean by how did I create the background? Its a piece of black plastic to simulate that good ol' fx-991 calculator look (im still using mine 25 years later!). The code for the buttons creates all the rest of it - the black line around the buttons, the sligh highlight at the button, etc.

Noodle said...
This comment has been removed by the author.
Swoop said...

Thanks for sharing! This is really helpful and just what I've been looking for! :)

Brian said...

Just out of curiosity, why create these programaticly with Core Graphics and not create a glossy button template in Photoshop or something and save it as a PNG? Is there an efficiency reason?

Joe said...

Awesome code. Is it possible to use UIControlEventTouchUpInside to activate the selector? I couldn't get '[button addTarget:self action:@selector(myFunction:) forControlEvents:UIControlEventTouchUpInside];' to work. Thanks!

Anonymous said...

Great post. I agree with Brian though in that the iPhone has to work harder to draw these complex shapes every time when it could just use a .png file.

Anonymous said...

Hi - I cannot find the display label in your code .. i.e, where the number would appear when pressed

Brennan Stehling said...

I've used your version to derive one for myself. You may find the way I handle the target action to be useful on your end.

https://github.com/brennanMKE/GlossyButtons

Anonymous said...

Very useful, but I highly recommend removing the shadow CALayer if you plan to do any animations, including orientation rotation. It is very sluggish with the presence of those minuscule shadows and looks fine without them.

CodePadawan said...

@Brennan. Really liked yours. Kudos. In response to comments suggesting using a png - really that's not a great way to do anything like this. Anytime you want to change the buttons (eg color) you have to make new pngs. This is much more flexible.

Anonymous said...

hi i like your graphics well i was trying to implement glossy buttons in android app using titanium alloy studio but i failed to do please help me.

Post a Comment