Touch-based interfaces offer easy-to-implement flexibility and power. Part 1 of this two-part series explored ways to help you build applications that work directly with user gestures and go far beyond the capabilities of prebuilt controls for direct manipulation interfaces. In this final part, I examine touch-based painting, smoothing drawing paths, and building in the capability for Multi-Touch interfaces, circle detection, custom-gesture recognition, and other features that will expand your application's ability to work with user gestures and touches.
Drawing Touches Onscreen
UIView
hosts the realm of direct onscreen drawing. Its drawRect
method offers a low-level way to draw content directly, letting you create and display arbitrary elements using Quartz 2D calls. Touch plus drawing join together to build concrete interfaces that can easily be manipulated in a variety of ways.
Recipe 1 combines gestures with drawRect
to introduce touch-based painting. As a user touches the screen, the TouchTrackerView
class builds a Bezier path that follows the user's finger. To paint the progress as the touch proceeds, the touchesMoved:withEvent:
method calls setNeedsDisplay
. This, in turn, triggers a call to drawRect:
, where the view strokes the accumulated Bezier path. Figure 1 shows the interface with a path created in this way.
Figure 1: A simple painting tool for iOS requires little more than collecting touches along a path and painting that path with UIKit/Quartz 2D calls.
Although you could adapt this recipe to use gesture recognizers, there's really no need. The touches are essentially meaningless, and are only provided to create a pleasing tracing. The basic responder methods (namely, touches began, moved, and so on) are perfectly capable of handling path creation and management tasks.
This example is meant for creating continuous traces. It does not respond to any touch event without a move; if you want to expand this recipe to add a simple dot or mark, you'll have to add that behavior yourself.
Recipe 1: Touch-Based Painting in a UIView.
@interface TouchTrackerView : UIView { UIBezierPath *path; } @end @implementation TouchTrackerView - (void) touchesBegan:(NSSet *) touches withEvent:(UIEvent *) event { // Initialize a new path for the user gesture self.path = [UIBezierPath bezierPath]; path.lineWidth = 4.0f; UITouch *touch = [touches anyObject]; [path moveToPoint:[touch locationInView:self]]; } - (void) touchesMoved:(NSSet *) touches withEvent:(UIEvent *) event { // Add new points to the path UITouch *touch = [touches anyObject]; [self.path addLineToPoint:[touch locationInView:self]]; [self setNeedsDisplay]; } - (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; [path addLineToPoint:[touch locationInView:self]]; [self setNeedsDisplay]; } - (void) touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [self touchesEnded:touches withEvent:event]; } - (void) drawRect:(CGRect)rect { // Draw the path [path stroke]; } - (id) initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) self.multipleTouchEnabled = NO; return self; } @end
The full sample project for Recipe 1 and all other recipes in this article are available online (navigate to the first folder to find them.)
Smoothing Drawings
Depending on the device in use and the amount of simultaneous processing involved, capturing user gestures may produce results that are rougher than desired. Touch events are often limited by CPU demands as well as by shaking hands. A smoothing algorithm can offset those limitations by interpolating between points. Figure 2 demonstrates the kind of angularity that derives from granular input and the smoothing that can be applied instead.
Figure 2: Catmull-Rom smoothing can be applied in real time to improve arcs between touch events. The images shown here are based on an identical gesture input, with and without smoothing applied.
Catmull-Rom splines create continuous curves between key points. This algorithm ensures that each initial point you provide remains part of the final curve. The resulting path retains the original path's shape. You choose the number of interpolation points between each pair of reference points. The trade-off lies between processing power and greater smoothing. The more points you add, the more CPU resources you'll consume. As you'll see when using the sample code that accompanies this article, a little smoothing goes a long way, even on newer devices; the latest iPad is so responsive that it's hard to draw a particularly jaggy line in the first place.
Recipe 2 demonstrates how to extract points from an existing Bezier path and then apply splining to create a smoothed result. Catmull-Rom uses four points at a time to calculate intermediate values between the second and third points, using a granularity you specify between those points.
Recipe 2 provides an example of just one kind of real-time geometric processing you might add to your applications. Many other algorithms out there in the world of computational geometry can be applied in a similar manner.
Recipe 2:Creating Smoothed Bezier Paths Using Catmull-Rom Splining.
#define VALUE(_INDEX_) [NSValue valueWithCGPoint:points[_INDEX_]] @implementation UIBezierPath (Points) void getPointsFromBezier(void *info, const CGPathElement *element) { NSMutableArray *bezierPoints = (__bridge NSMutableArray *)info; // Retrieve the path element type and its points CGPathElementType type = element->type; CGPoint *points = element->points; // Add the points if they're available (per type) if (type != kCGPathElementCloseSubpath) { [bezierPoints addObject:VALUE(0)]; if ((type != kCGPathElementAddLineToPoint) && (type != kCGPathElementMoveToPoint)) [bezierPoints addObject:VALUE(1)]; } if (type == kCGPathElementAddCurveToPoint) [bezierPoints addObject:VALUE(2)]; } - (NSArray *)points { NSMutableArray *points = [NSMutableArray array]; CGPathApply(self.CGPath, (__bridge void *)points, getPointsFromBezier); return points; } @end #define POINT(_INDEX_) \ [(NSValue *)[points objectAtIndex:_INDEX_] CGPointValue] @implementation UIBezierPath (Smoothing) - (UIBezierPath *) smoothedPath: (int) granularity { NSMutableArray *points = [self.points mutableCopy]; if (points.count < 4) return [self copy]; // Add control points to make the math make sense // Via Josh Weinberg [points insertObject:[points objectAtIndex:0] atIndex:0]; [points addObject:[points lastObject]]; UIBezierPath *smoothedPath = [UIBezierPath bezierPath]; // Copy traits smoothedPath.lineWidth = self.lineWidth; // Draw out the first 3 points (0..2) [smoothedPath moveToPoint:POINT(0)]; for (int index = 1; index < 3; index++) [smoothedPath addLineToPoint:POINT(index)]; for (int index = 4; index < points.count; index++) { CGPoint p0 = POINT(index - 3); CGPoint p1 = POINT(index - 2); CGPoint p2 = POINT(index - 1); CGPoint p3 = POINT(index); // now add n points starting at p1 + dx/dy up // until p2 using Catmull-Rom splines for (int i = 1; i < granularity; i++) { float t = (float) i * (1.0f / (float) granularity); float tt = t * t; float ttt = tt * t; CGPoint pi; // intermediate point pi.x = 0.5 * (2*p1.x+(p2.x-p0.x)*t + (2*p0.x-5*p1.x+4*p2.x-p3.x)*tt + (3*p1.x-p0.x-3*p2.x+p3.x)*ttt); pi.y = 0.5 * (2*p1.y+(p2.y-p0.y)*t + (2*p0.y-5*p1.y+4*p2.y-p3.y)*tt + (3*p1.y-p0.y-3*p2.y+p3.y)*ttt); [smoothedPath addLineToPoint:pi]; } // Now add p2 [smoothedPath addLineToPoint:p2]; } // finish by adding the last point [smoothedPath addLineToPoint:POINT(points.count - 1)]; return smoothedPath; } @end // Example usage: // Replace the path with a smoothed version after drawing completes - (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; [path addLineToPoint:[touch locationInView:self]]; path = [path smoothedPath:4]; [self setNeedsDisplay]; }
Using Multi-Touch Interaction
Enabling Multi-Touch interaction in UIView
instances lets iOS recover and respond to more than one finger touch at a time. Set the UIView
property multipleTouchEnabled
to YES
or override isMultipleTouchEnabled
for your view. When enabled, each touch callback returns an entire set of touches. When that set's count exceeds 1, you know you're dealing with Multi-Touch.
In theory, iOS supports an arbitrary number of touches. You can explore that limit by running the following recipe on an iPad, using as many fingers as possible at once. The practical upper limit has changed over time; this recipe modestly demurs from offering a specific number.
When Multi-Touch was first explored on the iPhone, developers did not dream of the freedom and flexibility that Multi-Touch combined with multiple users offered. Adding Multi-Touch to your games and other applications opens up not just expanded gestures, but also new ways of creating profoundly exciting multiuser experiences, especially on larger screens like the iPad. I encourage you to include Multi-Touch support in your applications wherever it is practical.