UPDATE
I've posted a Swift version of this answer separately.
ORIGINAL
This is a fun little problem. First of all, there are lots of ways to draw arrows, with curved or straight sides. Let's pick a very simple way and label the measurements we'll need:
We want to write a function that takes the start point, the end point, the tail width, the head width, and the head length, and returns a path outlining the arrow shape. Let's create a category named dqd_arrowhead
to add this method to UIBezierPath
:
// UIBezierPath+dqd_arrowhead.h
@interface UIBezierPath (dqd_arrowhead)
+ (UIBezierPath *)dqd_bezierPathWithArrowFromPoint:(CGPoint)startPoint
toPoint:(CGPoint)endPoint
tailWidth:(CGFloat)tailWidth
headWidth:(CGFloat)headWidth
headLength:(CGFloat)headLength;
@end
Since there are seven corners on the path of the arrow, let's start our implementation by naming that constant:
// UIBezierPath+dqd_arrowhead.m
#import "UIBezierPath+dqd_arrowhead.h"
#define kArrowPointCount 7
@implementation UIBezierPath (dqd_arrowhead)
+ (UIBezierPath *)dqd_bezierPathWithArrowFromPoint:(CGPoint)startPoint
toPoint:(CGPoint)endPoint
tailWidth:(CGFloat)tailWidth
headWidth:(CGFloat)headWidth
headLength:(CGFloat)headLength {
OK, the easy part is done. Now, how do we find the coordinates of those seven points on the path? It is much easier to find the points if the arrow is aligned along the X axis:
It's pretty easy to compute the point coordinates on an axis-aligned arrow, but we'll need the overall length of the arrow to do it. We'll use the hypotf
function from the standard library:
CGFloat length = hypotf(endPoint.x - startPoint.x, endPoint.y - startPoint.y);
We'll call on a helper method to actually compute the seven points:
CGPoint points[kArrowPointCount];
[self dqd_getAxisAlignedArrowPoints:points
forLength:length
tailWidth:tailWidth
headWidth:headWidth
headLength:headLength];
But we need to transform those points, because in general we're not trying to create an axis-aligned arrow. Fortunately, Core Graphics supports a kind of transformation called an affine transformation, which lets us rotate and translate (slide) points. We'll call another helper method to create the transform that turns our axis-aligned arrow into the arrow we were asked for:
CGAffineTransform transform = [self dqd_transformForStartPoint:startPoint
endPoint:endPoint
length:length];
Now we can create a Core Graphics path using the points of the axis-aligned arrow and the transform that turns it into the arrow we want:
CGMutablePathRef cgPath = CGPathCreateMutable();
CGPathAddLines(cgPath, &transform, points, sizeof points / sizeof *points);
CGPathCloseSubpath(cgPath);
Finally, we can wrap a UIBezierPath
around the CGPath
and return it:
UIBezierPath *uiPath = [UIBezierPath bezierPathWithCGPath:cgPath];
CGPathRelease(cgPath);
return uiPath;
}
Here's the helper method that computes the point coordinates. It's quite simple. Refer back to the diagram of the axis-aligned arrow if you need to.
+ (void)dqd_getAxisAlignedArrowPoints:(CGPoint[kArrowPointCount])points
forLength:(CGFloat)length
tailWidth:(CGFloat)tailWidth
headWidth:(CGFloat)headWidth
headLength:(CGFloat)headLength {
CGFloat tailLength = length - headLength;
points[0] = CGPointMake(0, tailWidth / 2);
points[1] = CGPointMake(tailLength, tailWidth / 2);
points[2] = CGPointMake(tailLength, headWidth / 2);
points[3] = CGPointMake(length, 0);
points[4] = CGPointMake(tailLength, -headWidth / 2);
points[5] = CGPointMake(tailLength, -tailWidth / 2);
points[6] = CGPointMake(0, -tailWidth / 2);
}
Computing the affine transform is more complicated. This is where the trigonometry comes in. You could use atan2
and the CGAffineTransformRotate
and CGAffineTransformTranslate
functions to create it, but if you remember enough trigonometry, you can create it directly. Consult “The Math Behind the Matrices” in the Quartz 2D Programming Guide for more information about what I'm doing here:
+ (CGAffineTransform)dqd_transformForStartPoint:(CGPoint)startPoint
endPoint:(CGPoint)endPoint
length:(CGFloat)length {
CGFloat cosine = (endPoint.x - startPoint.x) / length;
CGFloat sine = (endPoint.y - startPoint.y) / length;
return (CGAffineTransform){ cosine, sine, -sine, cosine, startPoint.x, startPoint.y };
}
@end
I have put all of the code in a gist for easy copy'n'paste.
With this category, you can easily draw arrows:
Since you're just generating a path, you can choose not to fill it, or not to stroke it as in this example:
You have to be careful, though. This code doesn't prevent you from getting funky results if you make the head width less than the tail width, or if you make the head length larger than the total arrow length:
Best Answer
Sometimes it is really useful to spend some time reinventing the wheel. As you might have already noticed there are a lot of frameworks, but it is not that hard to implement a simple, but yet useful solution without introducing all that complexity. (Please don't get me wrong, for any serious purpose it is better to use some mature and proven to be stable framework).
I will present my results first and then explain the simple and straightforward idea behind them.
You'll see in my implementation there is no need to analyze every single point and do complex computations. The idea is to spot some valuable meta information. I will use tangent as an example:
Let's identify a simple and straightforward pattern, typical for the selected shape:
So it is not that hard to implement a circle detection mechanism based on that idea. See working demo below (Sorry, I'm using Java as the fastest way to provide this fast and a bit dirty example):
It should not be a problem to implement similar behavior on iOS, since you just need several events and coordinates. Something like the following (see example):
There are several enhancements possible.
Start at any point
Current requirement is to start drawing a circle from the top middle point due to the following simplification:
Please notice the default value of
index
is used. A simple search through the available "parts" of the shape will remove that limitation. Please note you'll need to use a circular buffer in order to detect a full shape:Clockwise and counterclockwise
In order to support both modes you will need to use the circular buffer from the previous enhancement and search in both directions:
Draw an ellipse
You have everything you need already in the
bounds
array.Simply use that data:
Other gestures (optional)
Finally, you just need to properly handle a situation when
dx
(ordy
) is equal to zero in order to support other gestures:Update
This small PoC got quite a high attention, so I did update the code a bit in order to make it work smoothly and provide some drawing hints, highlight supporting points, etc:
Here is the code: