2011年11月3日星期四

Creating a Graph With Quartz 2D: Part 4

Creating a Graph With Quartz 2D: Part 4:
In this series of articles I am discussing the creation of charts and graphs using nothing more than Quartz 2D, a graphics rendering API created by Apple, which is a part of Core Graphics. You might wish to get up to speed with Part 1, Part 2 and Part 3.
Our graphs look great, but there are a couple of things missing. First, it is common to have labels that show the scale of values, to number the data points, to provide some comments and so on. Second, with the fabulous touch screen of the iPhone, the users will probably expect to be able to interact with the graph, for example if they tap it, the graph might respond with an appropriate bit of additional information.
In this part of the series, let’s see how to enable this kind of interactivity. We’ll leave drawing text on the graph for the final part of the series.

Enabling Interactivity

First of all, let’s refactor the existing code somewhat. We are going to switch between bar graph and line graph. It might seem convenient to create two different projects for the two different graphs, and I guess some readers have already done that. For me, it is more convenient to keep all of the code in one project. I will simply move the code for drawing the bar graph into a separate method, placed right above the drawRect:
- (void)drawBarGraphWithContext:(CGContextRef)ctx
{
// Draw the bars
float maxBarHeight = kGraphHeight - kBarTop - kOffsetY;
for (int i = 0; i < sizeof(data); i++)
{
float barX = kOffsetX + kStepX + i * kStepX - kBarWidth / 2;
float barY = kBarTop + maxBarHeight - maxBarHeight * data[i];
float barHeight = maxBarHeight * data[i];
CGRect barRect = CGRectMake(barX, barY, kBarWidth, barHeight);
[self drawBar:barRect context:ctx];
}
}
Now replace in drawRect all the code that went into the new method with a single line. It should be right next to the line that invokes the method for drawing the line graph. By commenting out one of these lines, we’ll be able to switch easily between different types of graph.
[self drawBarGraphWithContext:context];
We are going to deal with the bar graph first. The idea is that whenever a bar is tapped, a message appears indicating the value of that bar, but if the user taps outside of any bar, nothing will happen.

Detecting the Bars Tapped

The approach is quite simple. When drawing bars, we create rectangles and fill them with gradients. If we manage to save those rectangles and keep them around, we should be able to test if the coordinates of a touch happen to be inside of one of them.
Fo simplicity, let’s suppose that we know the number of bars and can make it into a constant. Add the following definition to the already existing ones:
#define kNumberOfBars 12
Add the following line of code to GraphView.m outside of the methods:
CGRect touchAreas[kNumberOfBars];
Also modify the condition of the for loop in drawBarGraphWithContext:
for (int i = 0; i < kNumberOfBars; i++)
Finally, at the very end of this loop, save the rectangle for the bar into the new array:
touchAreas[i] = barRect;
Now, after the drawing of the graph is completed, we’ll have at our disposal an array of rectangles that were used for drawing the bars. The next step is to intercept the user’s touches and define their locations. For this, we need to add the following method:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
NSLog(@"Touch x:%f, y:%f", point.x, point.y);
}
Run the app, tap somewhere inside the graph, and you should see in the log the coordinates of the point that was tapped.
Finally, we need to figure out whether the point that was tapped belongs to any of the bars. Add in the end of the new method the following code snippet:
for (int i = 0; i < kNumberOfBars; i++)
{
if (CGRectContainsPoint(touchAreas[i], point))
{
NSLog(@"Tapped a bar with index %d, value %f", i, data[i]);
break;
}
}
Now if you tap inside one of the bars, you should see logging similar to the following, on the other hand, if you tap somewhere outside of the bars, you should only see the touch message:
MyGraph[1265:b303] Touch x:212.000000, y:126.000000
MyGraph[1265:b303] Tapped a bar with index 2, value 0.900000
Obviously, instead of logging the message that a bar was touched, you can display a view with a label, and set the text of that label to the value of the bar, or do something else that is appropriate for your application. Because that won’t be Quartz 2D specific, I am leaving the detailed implementation to you.
What about the line graph then, how should it react to a touch? One possible solution is to draw a vertical line through a data point that is closest to the location of the touch. I don’t think I really need to show how to do that, you know how to draw lines and you can figure out where exactly to draw the pointer. However, when working on such a pointer line, I’ve found an interesting solution, let me show it to you.

Drawing a Pointer Line

The first approach that I tried was simply adding code for drawing a vertical line to the end of the drawRect method, and running it after the graph was touched. To make the line visible, I had to request a redraw of the whole graph after each touch. This is where I noticed that the graph became a bit sluggish, redrawing the lines, gradients and labels again and again. Clearly, this wasn’t an acceptable solution.
An alternative approach would be to redraw not the whole graph, but only a limited area of it. However, I felt too lazy to do this, and finally found a solution that I believe is simple and nice. The idea is to put another, transparent view on top of the GraphView, let’s call it PointerView, and handle touches and draw the pointer in that view only, leaving the GraphView as it is. Let’s dive into how I did it.
Select MyGraphViewController.xib, then drag a View from the Library and drop it on top of the GraphView. You may need to make some adjustments here. First, make sure that the new View is at the same level of the object hierarchy as the GraphView. To achieve this, drag and drop the new view on top of the Scroll View right in the object tree. Second, set the View’s x and y coordinates to 0. The following screenshot demonstrates what should be the end result of your manipulations.
Quartz 2D Part 4 Figure 1
Figure 1
Next, add to your project a new Objective-C class and make it a subclass of UIView. I named it PointerView. Back to the MyGraphViewController.xib, select the newly added View and change its class to PointerView. You might also want to change its name in the object tree to Pointer View.
While we are here, let’s change the background color of the PointerView to transparent. For this, click on the control for selecting a background color in PointerView’s properties and drag the Opacity slider to the left as far as it can go:
Quartz 2D Part 4 Figure 2
Figure 2
If you run the app now, and don’t forget to switch to line graph mode by commenting/uncommenting the appropriate lines of code, it should work and look exactly as it did before. In PointerView.m, uncomment the drawRect method and add the method for handling touches:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
}
Your homework, is the logic for figuring out the closest datapoint and drawing the pointer line through it. Here, we’ll simply draw a vertical line at the point where the graph was touched. Declare a couple of variables in PointerView.h:
#import <UIKit/UIKit.h>
@interface PointerView : UIView
{
float pointerX;
BOOL drawPointer;
}
@end
The first one will store the x coordinate of the latest touch while the second is a flag that will tell it’s all right to draw the pointer line. We can now complete the touchesBegan method by adding to it the following lines:
pointerX = point.x;
drawPointer = YES;
[self setNeedsDisplay];
The last line marks PointerView for redrawing. We now have all the information we need for drawing the pointer, so here is the drawing code:
- (void)drawRect:(CGRect)rect
{
if (drawPointer)
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect frame = self.frame;
CGContextSetLineWidth(context, 2.0);
CGContextSetStrokeColorWithColor(context, [[UIColor colorWithRed:0.4 green:0.8 blue:0.4 alpha:1.0] CGColor]);
CGContextMoveToPoint(context, pointerX, 0);
CGContextAddLineToPoint(context, pointerX, frame.size.height);
CGContextStrokePath(context);
}
}
Here is how the graph should look after a touch:
Quartz 2D Part 4 Figure 3
Figure 3
Note that performance won’t suffer, because after a touch we’ll only redraw the pointer line. The only important element that is missing from our graphs now is some textual information. We are going to learn how to draw that text using Quartz 2D in the next, and final part of this series. Stay tuned.

Quartz 2D Index

Alexander Kolesnikov’s series on Creating a Graph using Quartz 2D was split into 5 parts. You can refer to the series using the Quartz 2D Tag and access the individual articles using the links below.

没有评论:

发表评论