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

我需要画我的绘制应用程序结束箭头线。 我不擅长三angular,所以不能解决这个问题。

用户将他的手指放在屏幕上并向任何方向画线。 所以,箭头应该出现在行结束。

UPDATE

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

原版的

这是一个有趣的小问题。 首先,有很多方法可以绘制箭头,曲线或直线。 让我们select一个非常简单的方法,并标出我们需要的测量值:

箭头部分

我们要写一个函数,它取得起点,终点,尾宽,头部宽度和头部长度,并返回一个概括箭头形状的path。 我们来创build一个名为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 

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

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

好的,简单的部分就完成了。 现在,我们如何find这七个点的坐标? 如果箭头沿着X轴alignment,find点要容易得多:

轴对齐的箭头点

在一个轴alignment的箭头上计算点坐标是相当容易的,但是我们需要箭头的整个长度来完成。 我们将使用标准库中的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]; 

但是我们需要改变这些观点,因为一般来说我们并不是要创build一个轴alignment的箭头。 幸运的是,Core Graphics支持一种称为仿射变换的变换 ,它可以让我们旋转和平移(滑动)点。 我们将调用另一个帮助器方法来创build将我们的轴alignment的箭头变成我们所要求的箭头的变换:

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

现在我们可以使用轴alignment的箭头和将其转换为我们想要的箭头的转换创build一个Core Graphicspath:

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

最后,我们可以围绕CGPath包装一个CGPath并返回它:

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

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

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

计算仿射变换比较复杂。 这就是三angular函数的来源。你可以使用atan2CGAffineTransformRotateCGAffineTransformTranslate函数来创build它,但是如果你记得足够的三angular函数,你可以直接创build它。 请参阅Quartz 2D Programming Guide中的“matrix背后的math”以获得更多关于我在这里所做的事情的信息:

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

我已经把所有的代码放在一个容易复制的地方 。

有了这个类别,你可以很容易地画出箭头:

样品箭头1样品箭头2

既然你只是生成一个path,你可以select不填充它,或者不要像这个例子中描述的那样:

不受伤的箭头示例

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

窄头样本头太长样品

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

这是我旧的Objective-C代码的一个Swift版本。 它应该在Swift 3.2或Swift 4.0中工作。

 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) 

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