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

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

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

【问题讨论】:

    标签: objective-c ios core-graphics quartz-2d


    【解决方案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) }