如何使用 Core Graphics 绘制箭头?

Posted

技术标签:

【中文标题】如何使用 Core Graphics 绘制箭头?【英文标题】:How can I draw an arrow using Core Graphics? 【发布时间】:2012-11-23 12:10:27 【问题描述】:

我需要在我的 Draw 应用程序中用箭头画线。我三角学不好,所以解决不了这个问题。

用户将手指放在屏幕上并向任意方向画线。 所以,箭头应该出现在行尾。

【问题讨论】:

【参考方案1】:

更新

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

原创

这是一个有趣的小问题。首先,有很多方法来绘制箭头,有弯曲的或直的。让我们选择一种非常简单的方法并标记我们需要的测量值:

我们想写一个函数,它接受起点、终点、尾部宽度、头部宽度和头部长度,并返回一个勾勒出箭头形状的路径。让我们创建一个名为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

由于箭头的路径上有七个角,让我们通过命名该常量来开始我们的实现:

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

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

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

    CGFloat length = hypotf(endPoint.x - startPoint.x, endPoint.y - startPoint.y);

我们将调用一个辅助方法来实际计算这七个点:

    CGPoint points[kArrowPointCount];
    [self dqd_getAxisAlignedArrowPoints:points
                              forLength:length
                              tailWidth:tailWidth
                              headWidth:headWidth
                             headLength:headLength];

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

    CGAffineTransform transform = [self dqd_transformForStartPoint:startPoint
                                                          endPoint:endPoint
                                                            length:length];

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

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

最后,我们可以在CGPath 周围包裹一个UIBezierPath 并返回它:

    UIBezierPath *uiPath = [UIBezierPath bezierPathWithCGPath:cgPath];
    CGPathRelease(cgPath);
    return uiPath;

这是计算点坐标的辅助方法。这很简单。如果需要,请参考轴对齐箭头的图表。

+ (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);

计算仿射变换更复杂。这就是三角函数的用武之地。您可以使用atan2CGAffineTransformRotateCGAffineTransformTranslate 函数来创建它,但是如果您记住足够多的三角函数,您可以直接创建它。请咨询“The Math Behind the Matrices” in the Quartz 2D Programming Guide,了解更多关于我在这里做什么的信息:

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

我已将所有代码放入a gist for easy copy'n'paste。

有了这个分类,你就可以轻松画箭头了:

由于您只是生成一条路径,因此您可以选择不填充它,或者像本例中那样不描边:

不过,你必须小心。如果您使头部宽度小于尾部宽度,或者头部长度大于总箭头长度,则此代码不会阻止您获得时髦的结果:

【讨论】:

@SashaPrent 如果我已经回答了您的问题,请单击我的答案(顶部)旁边的复选标记。谢谢。 真的很强大,Rob ..,我必须说你是单射 c 函数和数学的大师 出色的解决方案。该过程比几何更容易,更清晰 我不是在寻找这个问题的答案,但这个答案所以很好,我还是投了赞成票。真的很棒。【参考方案2】:

这是我的旧 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)
    


以下是您如何称呼它的示例:

let arrow = UIBezierPath.arrow(from: CGPoint(x: 50, y: 100), to: CGPoint(x: 200, y: 50),
        tailWidth: 10, headWidth: 25, headLength: 40)

【讨论】:

【参考方案3】:
//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

【讨论】:

使用CAShapeLayer 可能比实现drawRect: 更有效。 多么精美、详尽、优雅的解释。整个事物散发出真正的关怀。谢谢。【参考方案4】:

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



【讨论】:

您能否解释一下如何这解决了 OP 的问题?请参阅How to Answer。 覆盖 func draw(_ rect: CGRect) 让箭头 = UIBezierPath.arrow(from: CGPoint(x:10, y:10), to: CGPoint(x:200, y:10) ,tailWidth: 10, headWidth: 25, headLength: 40) 让 shapeLayer = CAShapeLayer() shapeLayer.path = arrow.cgPath self.layer.addSublayer(shapeLayer)

以上是关于如何使用 Core Graphics 绘制箭头?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Core Graphics 绘制点?

在 iOS 中使用 Core Graphics 绘制 VU 表

使用 Core Graphics 绘制带有减去文本的路径

如何在 Core Graphics 中绘制任何 PNG 格式

如何在 Core Graphics / Quartz 2D 中绘制圆角矩形?

使用 Core Graphics 绘制 UIImage?