How can I draw an arrow using Core Graphics?

后端 未结 4 1264
梦毁少年i
梦毁少年i 2020-12-02 04:58

I need to draw line with arrow on its end in my Draw app. I\'m not good in trigonometry, so can\'t solve this problem.

The user put his finger on the screen and d

相关标签:
4条回答
  • 2020-12-02 05:36

    Here's a Swift version of my old Objective-C code. It should work in Swift 3.2 and later versions.

    extension UIBezierPath {
    
        static func arrow(from start: CGPoint, to end: CGPoint, tailWidth: CGFloat, headWidth: CGFloat, headLength: CGFloat) -> UIBezierPath {
            let length = hypot(end.x - start.x, end.y - start.y)
            let tailLength = length - headLength
    
            func p(_ x: CGFloat, _ y: CGFloat) -> CGPoint { return CGPoint(x: x, y: y) }
            let points: [CGPoint] = [
                p(0, tailWidth / 2),
                p(tailLength, tailWidth / 2),
                p(tailLength, headWidth / 2),
                p(length, 0),
                p(tailLength, -headWidth / 2),
                p(tailLength, -tailWidth / 2),
                p(0, -tailWidth / 2)
            ]
    
            let cosine = (end.x - start.x) / length
            let sine = (end.y - start.y) / length
            let transform = CGAffineTransform(a: cosine, b: sine, c: -sine, d: cosine, tx: start.x, ty: start.y)
    
            let path = CGMutablePath()
            path.addLines(between: points, transform: transform)
            path.closeSubpath()
    
            return self.init(cgPath: path)
        }
    
    }
    

    Here's an example of how you'd call it:

    let arrow = UIBezierPath.arrow(from: CGPoint(x: 50, y: 100), to: CGPoint(x: 200, y: 50),
            tailWidth: 10, headWidth: 25, headLength: 40)
    
    0 讨论(0)
  • 2020-12-02 05:40

    In Swift 3.0 you can achieve this with

    extension UIBezierPath {
    
    class func arrow(from start: CGPoint, to end: CGPoint, tailWidth: CGFloat, headWidth: CGFloat, headLength: CGFloat) -> Self {
        let length = hypot(end.x - start.x, end.y - start.y)
        let tailLength = length - headLength
    
        func p(_ x: CGFloat, _ y: CGFloat) -> CGPoint { return CGPoint(x: x, y: y) }
        var points: [CGPoint] = [
            p(0, tailWidth / 2),
            p(tailLength, tailWidth / 2),
            p(tailLength, headWidth / 2),
            p(length, 0),
            p(tailLength, -headWidth / 2),
            p(tailLength, -tailWidth / 2),
            p(0, -tailWidth / 2)
        ]
    
        let cosine = (end.x - start.x) / length
        let sine = (end.y - start.y) / length
        var transform = CGAffineTransform(a: cosine, b: sine, c: -sine, d: cosine, tx: start.x, ty: start.y)        
        let path = CGMutablePath()
        path.addLines(between: points, transform: transform)
        path.closeSubpath()
        return self.init(cgPath: path)
    }
    
    }
    
    0 讨论(0)
  • 2020-12-02 05:48

    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:

    arrow parts

    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:

    axis-aligned arrow points

    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:

    sample arrow 1 sample arrow 2

    Since you're just generating a path, you can choose not to fill it, or not to stroke it as in this example:

    unstroked arrow sample

    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:

    narrow head sample head too long sample

    0 讨论(0)
  • 2020-12-02 05:52
    //This is the integration into the view of the previous exemple
    //Attach the following class to your view in the xib file
    
    #import <UIKit/UIKit.h>
    
    @interface Arrow : UIView
    
    @end
    
    #import "Arrow.h"
    #import "UIBezierPath+dqd_arrowhead.h"
    
    @implementation Arrow
    {
        CGPoint startPoint;
        CGPoint endPoint;
        CGFloat tailWidth;
        CGFloat headWidth;
        CGFloat headLength;
        UIBezierPath *path;
    
    }
    
    
    - (id)initWithCoder:(NSCoder *)aDecoder
    {
        if (self = [super initWithCoder:aDecoder])
        {
            [self setMultipleTouchEnabled:NO];
            [self setBackgroundColor:[UIColor whiteColor]];
    
        }
        return self;
    }
    
    - (void)drawRect:(CGRect)rect {
    
        [[UIColor redColor] setStroke];
        tailWidth = 4;
        headWidth = 8;
        headLength = 8;
        path = [UIBezierPath dqd_bezierPathWithArrowFromPoint:(CGPoint)startPoint
                                                      toPoint:(CGPoint)endPoint
                                                    tailWidth:(CGFloat)tailWidth
                                                    headWidth:(CGFloat)headWidth
                                                   headLength:(CGFloat)headLength];
        [path setLineWidth:2.0];
    
        [path stroke];
    
    }
    - (void) touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
    {
        UITouch* touchPoint = [touches anyObject];
        startPoint = [touchPoint locationInView:self];
        endPoint = [touchPoint locationInView:self];
    
    
        [self setNeedsDisplay];
    }
    
    -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
    {
        UITouch* touch = [touches anyObject];
        endPoint=[touch locationInView:self];
        [self setNeedsDisplay];
    }
    
    -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
    {
        UITouch* touch = [touches anyObject];
        endPoint = [touch locationInView:self];
        [self setNeedsDisplay];
    }
    
    
    @end
    
    0 讨论(0)
提交回复
热议问题