“Quartz 2D is an advanced, two-dimensional drawing engine available for iOS application development…“.
What this means is: you use it to draw stuff. Now don’t let the words “advanced” fool you, Quartz is actually pretty easy to work with, while at the same time very powerful.
In this tutorial I plan to show you how to start working with Quartz, I will provide sample code and explanations. I’ve written this tutorial in a way that benefits beginners by starting from the very beginning and going all the way to the end; However people who are only looking for good examples can just jump straight to Step 5 of the tutorial.
Our sample project will be on the iPhone and will use the iOS SDK, but the Quartz code is the same in MacOS, so even if you’re working on a MacOS project this tutorial can still be useful for you.
Our objective is to create a circular progress bar (view), we will be using the following Quartz features: path-based drawing, image clipping and radial gradients.
If you imagine the device screen as a paint canvas, you will understand how graphic libraries work. When using UIKit, items are “painted” onto the screen once. Whenever something changes, UIKit paints the screen again. UIKit uses Quartz to draw onto the screen. The programming equivalent of “paint canvas” in Quartz is called “CGContext“. We will use CGContext objects in our code…
Here is the final project that we will create at the end of this tutorial. If you are a beginner, I recommend you work through the tutorial steps instead of simply downloading the project and reading through it.
QuartzTest.zip
First we need to think about what we want (simple use-cases):
We need a circular shape showing percentage of something. So it takes a value between 0 and 1 and displays it.
We also want it to be easy to use and re-usable, or in other words a “component”.
We want the code to be easily understandable (because this is a tutorial after all), so we add many comments.
We want to be able to set it’s colour to whatever we want.
We want to be able to change it’s value and colour during our application execution. eg: we might want to use this component to display health in a game, so when the player has low health it will be red and when has full health it will be green.
-(void) setProgress:(CGFloat) newProgress; -(void) setColourR:(CGFloat) r G:(CGFloat) g B:(CGFloat) b A:(CGFloat) a;
The “setColourR:G:B:A:” method takes RGBA values as inputs (r: red, g: green, b: blue, a: alpha/transparency), the reason that we store colour using these values (rather than using a UIColor object will be revealed later) . We might also need to get the component’s progress value, so we add:
-(CGFloat) progress;
We will also need to store these values in our component, so we will add the following:
CGFloat _r; CGFloat _g; CGFloat _b; CGFloat _a; CGFloat _progress;
OK, now we have methods to set the component’s value and colour. Save the file. The “CircularProgressView.h” file should look like this:
#import <UIKit/UIKit.h> @interface CircularProgressView : UIView { CGFloat _r; CGFloat _g; CGFloat _b; CGFloat _a; CGFloat _progress; } -(void) setProgress:(CGFloat) newProgress; // set the component's value -(void) setColourR:(CGFloat) r G:(CGFloat) g B:(CGFloat) b A:(CGFloat) a; // set component colour, set using RGBA system, each value should be between 0 and 1 -(CGFloat) progress; // returns the component's value @end
// set the component's value -(void) setProgress:(CGFloat) newProgress { _progress = newProgress; [self setNeedsDisplay]; } // set component colour, set using RGBA system, each value should be between 0 and 1. -(void) setColourR:(CGFloat) r G:(CGFloat) g B:(CGFloat) b A:(CGFloat) a { _r = r; _g = g; _b = b; _a = a; [self setNeedsDisplay]; } // returns the component's value. -(CGFloat) progress { return _progress; }
In the above code we’ve set our class variables (the variables we defined in the “.h” file) with given values.
You may also notice that there is a call to [self setNeedsDisplay] in the “setProgress:” and “setColourR:G:B:A” methods, because we will write our own drawing code, we need to manually tell our component that it needs to re-draw itself. The method “setNeedsDisplay” does this for us, so whenever a value that affects the visual representation of our component changes we need to call [self setNeedsDisplay].
[dropcap]6[/dropcap]Navigate to the “initWithFrame:” method, it should look like this:- (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // Initialisation code. } return self; }
This method is used to create a UIView with the given rectangular frame.
We need to add a some initialisation code; such as:
self.backgroundColor = [UIColor clearColor]; self.opaque = NO; self.hidden = NO; self.alpha = 1.0;
This is because when you create a UIView it’s initial values might be something that you don’t want.
We will also need to initialise our class variables with their default values:
_r = 1.0; _g = 0.1; _b = 0.1; _a = 1.0; _progress = 0.6;
It really doesn’t matter what you set here, it’s just important that you set them (because they might initially contain undesirable and random values).
[dropcap]7[/dropcap]Since we want our component to be easy to use and re-usable, we need to think of the easies way to create it. UIViews are created using rectangles, so it would be easier to use if our component drew itself inside the given rectangle.First we need to find the radius of the largest circle that fits in the given rectangle:
// find the radius and position for the largest circle that fits in the UIView's frame. int radius, x, y; // in case the given frame is not square (oblong) we need to check and use the shortest side as our radius. if (frame.size.width > frame.size.height) { radius = frame.size.height; // we want our circle to be in the centre of the frame. int delta = frame.size.width - radius; x = delta/2; y = 0; }else { radius = frame.size.width; int delta = frame.size.height - radius; y = delta/2; x = 0; }
Now that we know the largest circle’s radius and position, we will store it in a CGRect structure. Since we will want to use this variable in other methods too, go ahead and add :
CGRect _outerCircleRect;
to “CircularProgressView.h”, and add the following to “CircularProgressView.m”:
// store the largest circle's position and radius in class variable. _outerCircleRect = CGRectMake(x, y, radius, radius);
Your init method should look like this:
- (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // Initialisation code. // set initial UIView state self.backgroundColor = [UIColor clearColor]; self.opaque = NO; self.hidden = NO; self.alpha = 1.0; // set class variables to default values _r = 1.0; _g = 0.1; _b = 0.1; _a = 1.0; _progress = 0.0; // find the radius and position for the largest circle that fits in the UIView's frame. int radius, x, y; // in case the given frame is not square (oblong) we need to check and use the shortest side as our radius. if (frame.size.width > frame.size.height) { radius = frame.size.height; // we want our circle to be in the center of the frame. int delta = frame.size.width - radius; x = delta/2; y = 0; }else { radius = frame.size.width; int delta = frame.size.height - radius; y = delta/2; x = 0; } // store the largest circle's position and radius in class variable. _outerCircleRect = CGRectMake(x, y, radius, radius); } return self; }
#import "CircularProgressView.h"
// Create an instance of the CircularProgressView class and add it to self.view so we can see how it looks during our testing. CircularProgressView* circularTest = [[CircularProgressView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)]; [self.view addSubview:circularTest];
Your “viewDidLoad” method should look like this:
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib. - (void)viewDidLoad { [super viewDidLoad]; // Create an instance of the CircularProgressView class and add it to self.view so we can see how it looks during our testing. CircularProgressView* circularTest = [[CircularProgressView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)]; [self.view addSubview:circularTest]; }
#import <QuartzCore/QuartzCore.h>
// get the drawing canvas (CGContext): CGContextRef context = UIGraphicsGetCurrentContext();
Because Quartz is not written in Objective-C, it’s methods (or functions) look different from regular methods, for example you don’t need to call them inside a “[ ]” block.
The CGContext object (here-forth called “context”) contains attributes related to our drawing canvas such as line width, drawing colour… because we will manipulate the context we will save it’s previous state before our drawing code and restore them after we’re done with it by adding:
// save the context's previous state:
CGContextSaveGState(context);
Our “drawRect:” method should look like this:
- (void)drawRect:(CGRect)rect { // Drawing code. // get the drawing canvas (CGContext): CGContextRef context = UIGraphicsGetCurrentContext(); // save the context's previous state: CGContextSaveGState(context); // our custom drawing code will go here: // restore the context's state when we are done with it: CGContextRestoreGState(context); }
// set line width CGContextSetLineWidth(context, 1); // set the colour when drawing lines R,G,B,A CGContextSetRGBStrokeColor(context, 1,0,0,1);
The above code sets the line width to 1 and the stroke colour (colour used when drawing lines) to red.
[dropcap]7[/dropcap]Quartz uses a path based system to draw items, so for example if you want to draw a line you give it the start and end points (which creates a path with 2 points) and then tell it to draw the path.You can set the starting position of a path using the “CGContextMoveToPoint” method (this method takes an X and Y as inputs).
After setting the initial position of the path you can add other shapes to the path (such as: lines, curves…). Information about the “path” is stored inside the context. These “Add” methods all begin with “CGContextAddXXXToPath” (replace XXX with shape name).
Just as an example we are going to draw a line :
CGContextMoveToPoint(context, 10, 10); CGContextAddLineToPoint(context, 100, 100); CGContextStrokePath(context);
You can try drawing other shapes using the other “Add” methods.
Quartz also has two ways to draw a path onto the screen: Stroking and Filling. “Stroke” draws the outline of a path, “Fill” fills any closed sections of the path (for a detailed look on how Quartz fills closed paths look at the “Quartz 2D Programming Guide”).
Every time that you use a drawing function the path stored inside a context is drawn and then cleared.
[dropcap]8[/dropcap]Quartz provides helper functions for drawing common shapes such as rectangles and ellipses (so you don’t have to give the points for these shapes). So replace the line drawing code with:// draw an ellipse in the provided rectangle
CGContextAddEllipseInRect(context, _outerCircleRect);
Your code should now look like this:
- (void)drawRect:(CGRect)rect { // Drawing code. // get the drawing canvas (CGContext): CGContextRef context = UIGraphicsGetCurrentContext(); // save the context's previous state: CGContextSaveGState(context); // our custom drawing code will go here: // set line width CGContextSetLineWidth(context, 1); // set the colour when drawing lines R,G,B,A CGContextSetRGBStrokeColor(context, 1,0,0,1); // draw an ellipse in the provided rectangle CGContextAddEllipseInRect(context, _outerCircleRect); CGContextStrokePath(context); // restore the context's state when we are done with it: CGContextRestoreGState(context); }
“A gradient is a fill that varies from one colour to another. An axial gradient (also called a linear gradient) varies along an axis between two defined end points. All points that lie on a line perpendicular to the axis have the same colour value.
A radial gradient is a fill that varies radially along an axis between two defined ends, which typically are both circles.”
As explained in the quote from “Quartz 2D Programming Guide”, a radial gradient is between two circles (usually), we already have an outer circle, what we need now is a smaller circle inside it.
So add:
CGRect _innerCircleRect;
to “CircularProgressView.h”, now go to “initWithFrame:” and add the following code after our creation of _outerCircleRect:
// store the inner circles rect, this inner circle will have a radius 10pixels smaller than the outer circle. // we want to the inner circle to be in the middle of the outer circle. _innerCircleRect = CGRectMake(x+10, y+10, radius-2*10 , radius-2*10 );
Now just to test our inner circle add the following code in the drawRect method (below the code that draws the outer circle):
CGContextAddEllipseInRect(context, _innerCircleRect); CGContextStrokePath(context);
// gradient properties: CGGradientRef myGradient; // You need tell Quartz your colour space (how you define colours), there are many colour spaces: RGBA, black&white... CGColorSpaceRef myColorspace; // the number of different colours size_t num_locations = 3; // the location of each colour change, these are between 0 and 1, zero is the first circle and 1 is the end circle, so 0.5 is in the middle. CGFloat locations[3] = { 0.0, 0.5 ,1.0 }; // this is the colour components array, because we are using an RGBA system each colour has four components (four numbers associated with it). CGFloat components[12] = {0.4, 0.4, 0.4, 0.9, // Start colour 0.9, 0.9, 0.9, 1.0, // middle colour 0.4, 0.4, 0.4, 0.9 }; // End colour myColorspace = CGColorSpaceCreateDeviceRGB(); // Create a CGGradient object. myGradient = CGGradientCreateWithColorComponents (myColorspace, components,locations, num_locations); // gradient start and end points CGPoint myStartPoint, myEndPoint; CGFloat myStartRadius, myEndRadius; myStartPoint.x = _innerCircleRect.origin.x + _innerCircleRect.size.width/2; myStartPoint.y = _innerCircleRect.origin.y + _innerCircleRect.size.width/2; myEndPoint.x = _innerCircleRect.origin.x + _innerCircleRect.size.width/2; myEndPoint.y = _innerCircleRect.origin.y + _innerCircleRect.size.width/2; myStartRadius = _innerCircleRect.size.width/2 ; myEndRadius = _outerCircleRect.size.width/2; // draw the gradient. CGContextDrawRadialGradient(context, myGradient, myStartPoint, myStartRadius, myEndPoint, myEndRadius, 0); CGGradientRelease(myGradient);
In order to better understand the above code you need to view the input variables for each method. By holding down the “option” (alt) key and clicking a function name you can bring up the quick help message related to the selected item (in Xcode… not here :)).
[image img=”http://www.turnedondigital.com/wp-content/uploads/2011/01/5.10.jpg” align=”center” border=”thin”](I’ve removed the code for the red circles to just see the gradient).
[dropcap]11[/dropcap]If you look at the above screenshot, you will see the gradient edges are jagged. Even if you turn on Anti-Aliasing (a mechanism for smoothing out the edges of drawn items), the gradient’s edges will not change. It would seem that anti-aliasing doesn’t work on gradients.To fix this problem we will draw the outer and inner circle on top of our gradient. So along with the circle drawing code, our drawRect method will look like this (please note the I’ve changed the circle colours from red to grey):
- (void)drawRect:(CGRect)rect { // Drawing code. // get the drawing canvas (CGContext): CGContextRef context = UIGraphicsGetCurrentContext(); // save the context's previous state: CGContextSaveGState(context); // our custom drawing code will go here: // Draw the gray background for our progress view: // gradient properties: CGGradientRef myGradient; // You need tell Quartz your colour space (how you define colours), there are many colour spaces: RGBA, black&white... CGColorSpaceRef myColorspace; // the number of different colours size_t num_locations = 3; // the location of each colour change, these are between 0 and 1, zero is the first circle and 1 is the end circle, so 0.5 is in the middle. CGFloat locations[3] = { 0.0, 0.5 ,1.0 }; // this is the colour components array, because we are using an RGBA system each colour has four components (four numbers associated with it). CGFloat components[12] = { 0.4, 0.4, 0.4, 0.9, // Start colour 0.9, 0.9, 0.9, 1.0, // middle colour 0.4, 0.4, 0.4, 0.9 }; // End colour myColorspace = CGColorSpaceCreateDeviceRGB(); myGradient = CGGradientCreateWithColorComponents(myColorspace, components,locations, num_locations); // gradient start and end points CGPoint myStartPoint, myEndPoint; CGFloat myStartRadius, myEndRadius; myStartPoint.x = _innerCircleRect.origin.x + _innerCircleRect.size.width/2; myStartPoint.y = _innerCircleRect.origin.y + _innerCircleRect.size.width/2; myEndPoint.x = _innerCircleRect.origin.x + _innerCircleRect.size.width/2; myEndPoint.y = _innerCircleRect.origin.y + _innerCircleRect.size.width/2; myStartRadius = _innerCircleRect.size.width/2 ; myEndRadius = _outerCircleRect.size.width/2; // draw the gradient. CGContextDrawRadialGradient(context, myGradient, myStartPoint, myStartRadius, myEndPoint, myEndRadius, 0); CGGradientRelease(myGradient); // draw outline so that the edges are smooth: // set line width CGContextSetLineWidth(context, 1); // set the colour when drawing lines R,G,B,A. (we will set it to the same colour we used as the start and end point of our gradient ) CGContextSetRGBStrokeColor(context, 0.4,0.4,0.4,0.9); // draw an ellipse in the provided rectangle CGContextAddEllipseInRect(context, _outerCircleRect); CGContextStrokePath(context); CGContextAddEllipseInRect(context, _innerCircleRect); CGContextStrokePath(context); // restore the context's state when we are done with it: CGContextRestoreGState(context); }
and have an output like this:
[image img=”http://www.turnedondigital.com/wp-content/uploads/2011/01/5.11.jpg” align=”center” border=”thin”]As you can see the edges are now smooth.
[dropcap]12[/dropcap]We would like to draw the filled up section (progress) of our component using gradients too (because we want that 3D effect), but as you know gradients draw circles and not part circles… To fix this we will use something called clipping. Clipping allows you to only draw parts that you want. In Quartz a clipping area is defined using paths. So just like before you move to a point and add shapes to a path, but instead of using “CGContextStrokePath” you use “CGContextClip”. When you use clipping, anything outside the clipping area is automatically not drawn.One important thing to note is this: CGContext can only add paths to the clipping area, in other words make the drawing area smaller, because later on you might want to draw outside your clipping area it’s always a good idea to save the context’s state before clipping.
The following code will create a path surrounding an area equivalent to the component’s progress:
// First clip the drawing area: // save the context before clipping CGContextSaveGState(context); CGContextMoveToPoint(context, _outerCircleRect.origin.x + _outerCircleRect.size.width/2, // move to the top center of the outer circle _outerCircleRect.origin.y +1); // the Y is one more because we want to draw inside the bigger circles. // add an arc relative to _progress CGContextAddArc(context, _outerCircleRect.origin.x + _outerCircleRect.size.width/2, _outerCircleRect.origin.y + _outerCircleRect.size.width/2, _outerCircleRect.size.width/2 - 1, -M_PI/2, (-M_PI/2 + _progress*2*M_PI), 0); // custom code for creating an arc shaped path CGContextAddArc(context, _outerCircleRect.origin.x + _outerCircleRect.size.width/2, _outerCircleRect.origin.y + _outerCircleRect.size.width/2, _outerCircleRect.size.width/2 - 9, (-M_PI/2 + _progress*2*M_PI), -M_PI/2, 1); // use close path to connect the last point in the path with the first point (to create a closed path) CGContextClosePath(context); // clip to the path stored in context CGContextClip(context); // Progress drawing code comes here: //restore the context and remove the clipping area. CGContextRestoreGState(context);
The above is some custom code to create the specific shape that we require for this project, so I will not explain it (as it is not in the scope of this tutorial).
Now that we’ve set the clipping area, we use the gradient drawing code (mentioned in part11 ) to draw our gradient, except this time instead of using grey as the colour, we use our own class variables:
// set the gradient colours based on class variables. CGFloat components2[12] = {_r, _g, _b, _a, // Start color ((_r + 0.5 < 1) ? 1 : (_r+0.5) ) , ((_g + 0.5 < 1) ? 1 : (_g+0.5) ), ((_b + 0.5 < 1) ? 1 : (_b+0.5) ), ((_a + 0.5 < 1) ? 1 : (_a+0.5) ), // middle color _r, _g, _b, _a }; // End color
The code for the middle colour might be a little confusing, but put simply it just creates a lighter colour shade by adding 0.5 to all colour values and if a colour value+0.5 exceeds 1.0 then replace it with 1.0. And just like before we draw two circles on the outside to smooth out the edges.
[image img=”http://www.turnedondigital.com/wp-content/uploads/2011/01/5.121.jpg” align=”center” border=”thin”](I’ve set the components progress to something other than zero to take this screen shot).
[dropcap]13[/dropcap]Before we finish up we need to look at the memory management of Quartz related objects. If you use methods that contain “Create” then you are responsible for releasing those objects, so for example:myColorspace = CGColorSpaceCreateDeviceRGB();
means that you must have a :
CGColorSpaceRelease(myColorspace);
later after you are done with it.
[dropcap]14[/dropcap] You can review the entire project code by downloading the zip file.And we are done… it was long but as you saw each step is not that complicated. You can now use the “CircularProgressView” class (by copying the .m & .h files) in your other projects.
Looking back at Step1, we can see that all of our requirements have been met (and based on the two lines of code that it took in “QuartzTestViewController” to use our component it is pretty easy to use).
[/toggle2]Hope this is all useful. If you have any questions feel free to drop me a line at soroush [at] turnedondigital.com
Posted By
Soroush
Categories
Blog
Tags
Drawing, Radial Gradient, Sample Code, Tutorial