ios 如何使用 Core Graphics 绘制箭头?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/13528898/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-30 21:18:25  来源:igfitidea点击:

How can I draw an arrow using Core Graphics?

objective-cioscore-graphicsquartz-2d

提问by Sasha Prent

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.

我需要在我的 Draw 应用程序中绘制带有箭头的线。我不擅长三角学,所以无法解决这个问题。

The user put his finger on the screen and draw the line in any direction. So, the arrow should appear on the line end.

用户将手指放在屏幕上并沿任何方向绘制线条。所以,箭头应该出现在行尾。

回答by rob mayoff

UPDATE

更新

I've posted a Swift version of this answer separately.

我已经单独发布了这个答案的 Swift 版本。

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_arrowheadto add this method to UIBezierPath:

我们想编写一个函数,它接受起点、终点、尾部宽度、头部宽度和头部长度,并返回一个概述箭头形状的路径。让我们创建一个名为的类别以dqd_arrowhead将此方法添加到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:

好了,简单的部分就完成了。现在,我们如何找到路径上这七个点的坐标?如果箭头沿 X 轴对齐,则更容易找到点:

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 hypotffunction from the standard library:

计算轴对齐箭头上的点坐标非常容易,但我们需要箭头的总长度来完成。我们将使用hypotf标准库中的函数:

    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 nottrying 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:

但是我们需要转换这些点,因为通常我们不会尝试创建轴对齐的箭头。幸运的是,Core Graphics 支持一种称为仿射变换的变换,它可以让我们旋转和平移(滑动)点。我们将调用另一个辅助方法来创建将轴对齐箭头变成我们要求的箭头的变换:

    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:

现在我们可以使用轴对齐箭头的点和将其转换为我们想要的箭头的变换来创建 Core Graphics 路径:

    CGMutablePathRef cgPath = CGPathCreateMutable();
    CGPathAddLines(cgPath, &transform, points, sizeof points / sizeof *points);
    CGPathCloseSubpath(cgPath);

Finally, we can wrap a UIBezierPatharound the CGPathand return it:

最后,我们可以将 a 包裹UIBezierPath起来CGPath并返回它:

    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 atan2and the CGAffineTransformRotateand CGAffineTransformTranslatefunctions 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 Guidefor more information about what I'm doing here:

计算仿射变换更为复杂。这就是三角学的用武之地。您可以使用atan2CGAffineTransformRotateCGAffineTransformTranslate函数来创建它,但如果您记得足够多的三角学,则可以直接创建它。请参阅Quartz 2D 编程指南中的“矩阵背后的数学”以获取有关我在这里所做工作的更多信息:

+ (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.

我已将所有代码放在一个要点中,以便于 copy'n'paste

With this category, you can easily draw arrows:

使用此类别,您可以轻松绘制箭头:

sample arrow 1sample arrow 2

示例箭头 1示例箭头 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 samplehead too long sample

窄头样品样品头太长

回答by rob mayoff

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

这是我旧的 Objective-C 代码的 Swift 版本。它应该适用于 Swift 3.2 及更高版本。

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)

回答by copter244

//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

回答by Sujatha Girijala

In Swift 3.0 you can achieve this with

在 Swift 3.0 中,您可以使用

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)
}

}