使用 ObjectiveC 的 Core Graphics,如何将箭头绕圆圈弯曲?
Using Core Graphics from ObjectiveC, how to you curve an arrow around a circle?
我们的设计师让我重新创建这个:
我继承了 UIView,并且我已经覆盖了 drawRect 命令,如下所示:
[super drawRect:frame];
CGFloat x = self.frame.origin.x;
CGFloat y = self.frame.origin.y;
CGFloat w = self.frame.size.width;
CGFloat h = self.frame.size.height;
CGFloat lineWidth = lineWidthRequested;
CGPoint centerPoint = CGPointMake(w/2, h/2);
CGFloat radius = radiusRequested;
CGFloat startAngle = 3 * M_PI / 2;
CGFloat endAngle = startAngle + percentage * 2 * M_PI;
CGMutablePathRef arc = CGPathCreateMutable();
CGPathAddArc(arc, NULL,
centerPoint.x, centerPoint.y,
radius,
startAngle,
endAngle,
NO);
CGPathRef strokedArc = CGPathCreateCopyByStrokingPath(arc, NULL,
lineWidth,
kCGLineCapButt,
kCGLineJoinMiter, // the default
10); // 10 is default miter limit
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextAddPath(c, strokedArc);
CGContextSetFillColorWithColor(c, [UIColor colorWithRed:239/255.0 green:101/255.0 blue:47/255.0 alpha:1.0].CGColor);
CGContextDrawPath(c, kCGPathFill);
我最终得到的是:
还得画箭头。会很容易,对吧?
在努力记住我的三角函数之后,我在这个页面上找到了围绕中心旋转的点:
Rotating a point around an origin in VB
但是当我尝试翻译成 objective C 来绘制箭头时,我得到了非常奇怪的结果。下面是 drawRect 中的代码:
CGFloat triangle[3][2] = {{centerPoint.x + 10, h - (centerPoint.y + radius)},
{centerPoint.x, h - (centerPoint.y + radius + lineWidth/2)},
{centerPoint.x, h - (centerPoint.y + radius - lineWidth/2)}};
for (int idx=0;idx < 3; idx++) {
// translate to origin
triangle[idx][0] -= centerPoint.x;
triangle[idx][1] -= centerPoint.y;
}
CGFloat angDistance = endAngle - startAngle;
CGFloat ct = cos(angDistance);
CGFloat st = sin(angDistance);
for (int idx=0;idx < 3; idx++) {
// rotate
triangle[idx][0] = ct * triangle[idx][0] - st * triangle[idx][1];
triangle[idx][1] = -st * triangle[idx][0] + ct * triangle[idx][1];
}
for (int idx=0;idx < 3; idx++) {
// translate back to position
triangle[idx][0] += centerPoint.x;
triangle[idx][1] += centerPoint.y;
}
NSLog(@"Rotating through %g, %06.1f,%06.1f , ct - %g, st - %g",angDistance, triangle[0][0],triangle[0][1],ct, st);
// XXX todo draw the filled triangle at end.
// draw a red triangle, the point of the arrow
CGContextSetFillColorWithColor(c, [[UIColor greenColor] CGColor]);
CGContextMoveToPoint(c, triangle[0][0], triangle[0][1]);
CGContextAddLineToPoint(c, triangle[1][0], triangle[1][1]);
CGContextAddLineToPoint(c, triangle[2][0], triangle[2][1]);
CGContextFillPath(c);
我本来以为我会做这些点,然后将它们平移到原点,旋转,然后再平移回来,我会笑的。
但是,实际情况并非如此...随着百分比从 0 增加到 2pi,箭头将自己绘制成一条模糊的三角形路线。当 angDistance 为零或 pi 时,箭头位于正确的位置。但是,当我朝向 pi/2 或 3pi/2 时,箭头朝向封闭矩形的下角。
我一定是在做一些明显愚蠢的事情,但我这辈子都看不到。
有什么想法吗?
谢谢,
-肯
我建议为所需形状的整个轮廓构建一条路径,然后 "fill" 该路径具有所需的颜色。这消除了任何间隙或任何不完全排列的风险。
因此,这条路径可能由箭头外侧的弧线、箭头头部的两条线、箭头内侧的弧线组成,然后关闭路径。这可能看起来像:
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, self.arrowColor.CGColor);
CGPoint center = CGPointMake(rect.size.width / 2.0, rect.size.height / 2.0);
CGContextMoveToPoint(context, center.x + cosf(self.startAngle) * (self.radius + self.lineWidth / 2.0),
center.y + sinf(self.startAngle) * (self.radius + self.lineWidth / 2.0));
CGContextAddArc(context, center.x, center.y, self.radius + self.lineWidth / 2.0, self.startAngle, self.endAngle, !self.clockwise);
CGFloat theta = asinf(self.lineWidth / self.radius / 2.0) * (self.clockwise ? 1.0 : -1.0);
CGFloat pointDistance = self.radius / cosf(theta);
CGContextAddLineToPoint(context, center.x + cosf(self.endAngle + theta) * pointDistance,
center.y + sinf(self.endAngle + theta) * pointDistance);
CGContextAddLineToPoint(context, center.x + cosf(self.endAngle) * (self.radius - self.lineWidth / 2.0),
center.y + sinf(self.endAngle) * (self.radius - self.lineWidth / 2.0));
CGContextAddArc(context, center.x, center.y, self.radius - self.lineWidth / 2.0, self.endAngle, self.startAngle, self.clockwise);
CGContextClosePath(context);
CGContextDrawPath(context, kCGPathFill);
}
这里唯一的技巧是为箭头的末端找到正确的点。我改进了选择以更好地处理较粗的箭头,但您应该随意使用您认为最适合您的应用程序的任何内容。
于是,代码如下:
self.arrowView.radius = 100;
self.arrowView.arrowColor = [UIColor blueColor];
self.arrowView.lineWidth = 40;
self.arrowView.startAngle = -M_PI_2;
self.arrowView.endAngle = M_PI;
self.arrowView.clockwise = TRUE;
会产生以下结果(我用 CADisplayLink
制作动画):
这使用零开始角表示“3 点钟”位置,但您显然可以根据需要调整它。但希望它能说明解决问题的一种方法。
顺便说一句,虽然我已经回答了如何使用 CoreGraphics 执行此操作的问题,但我不一定会建议这样做。例如,在 https://github.com/robertmryan/CircularArrowDemo 中,我没有实现 drawRect
,而是更新了一个 CAShapeLayer
。通过这样做,我不仅可以避免 drawRect
效率低下,而且理论上还可以改变您使用此 CAShapeLayer
的方式(例如,将其用作某些 UIView
的 mask
,在它后面揭示一些更有趣的颜色渐变(或其他图像)。
这是另一个解决方案(虽然可扩展性不是很好)。此解决方案假设这就像一个徽标,其中绘制的圆 angle/percent 不会改变。
- (void)drawRect:(CGRect)rect {
UIBezierPath *circleOutline = [UIBezierPath bezierPath];
[self.circleColor setStroke];
[circleOutline setLineWidth:self.bounds.size.width*0.15];
[circleOutline addArcWithCenter:CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2) radius:self.bounds.size.width/2-circleOutline.lineWidth/2 startAngle:3*M_PI/2 endAngle:3*M_PI/4 clockwise:YES];
[circleOutline stroke];
[self addArrowView:circleOutline];
}
- (void)addArrowView:(UIBezierPath *)path {
for (int x = 0; x < self.bounds.size.width/2; x++) {
for (int y = self.bounds.size.height/2; y < self.bounds.size.height; y++) {
if ([path containsPoint:CGPointMake(x, y)]) {
// Pythagorean Theorem - We want the diagonal length of the square to be lineWidth, so we need to calculate what size
// to make each side of the square to make the diagonal equal to lineWidth
double sideLength = sqrt((path.lineWidth*path.lineWidth)/2);
UIView *arrowView = [[UIView alloc] initWithFrame:CGRectMake(x-sideLength/2, y-sideLength/2, sideLength, sideLength)];
arrowView.backgroundColor = self.circleColor;
[self addSubview:arrowView];
return;
}
}
}
}
会产生:
我们的设计师让我重新创建这个:
我继承了 UIView,并且我已经覆盖了 drawRect 命令,如下所示:
[super drawRect:frame];
CGFloat x = self.frame.origin.x;
CGFloat y = self.frame.origin.y;
CGFloat w = self.frame.size.width;
CGFloat h = self.frame.size.height;
CGFloat lineWidth = lineWidthRequested;
CGPoint centerPoint = CGPointMake(w/2, h/2);
CGFloat radius = radiusRequested;
CGFloat startAngle = 3 * M_PI / 2;
CGFloat endAngle = startAngle + percentage * 2 * M_PI;
CGMutablePathRef arc = CGPathCreateMutable();
CGPathAddArc(arc, NULL,
centerPoint.x, centerPoint.y,
radius,
startAngle,
endAngle,
NO);
CGPathRef strokedArc = CGPathCreateCopyByStrokingPath(arc, NULL,
lineWidth,
kCGLineCapButt,
kCGLineJoinMiter, // the default
10); // 10 is default miter limit
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextAddPath(c, strokedArc);
CGContextSetFillColorWithColor(c, [UIColor colorWithRed:239/255.0 green:101/255.0 blue:47/255.0 alpha:1.0].CGColor);
CGContextDrawPath(c, kCGPathFill);
我最终得到的是:
还得画箭头。会很容易,对吧?
在努力记住我的三角函数之后,我在这个页面上找到了围绕中心旋转的点: Rotating a point around an origin in VB
但是当我尝试翻译成 objective C 来绘制箭头时,我得到了非常奇怪的结果。下面是 drawRect 中的代码:
CGFloat triangle[3][2] = {{centerPoint.x + 10, h - (centerPoint.y + radius)},
{centerPoint.x, h - (centerPoint.y + radius + lineWidth/2)},
{centerPoint.x, h - (centerPoint.y + radius - lineWidth/2)}};
for (int idx=0;idx < 3; idx++) {
// translate to origin
triangle[idx][0] -= centerPoint.x;
triangle[idx][1] -= centerPoint.y;
}
CGFloat angDistance = endAngle - startAngle;
CGFloat ct = cos(angDistance);
CGFloat st = sin(angDistance);
for (int idx=0;idx < 3; idx++) {
// rotate
triangle[idx][0] = ct * triangle[idx][0] - st * triangle[idx][1];
triangle[idx][1] = -st * triangle[idx][0] + ct * triangle[idx][1];
}
for (int idx=0;idx < 3; idx++) {
// translate back to position
triangle[idx][0] += centerPoint.x;
triangle[idx][1] += centerPoint.y;
}
NSLog(@"Rotating through %g, %06.1f,%06.1f , ct - %g, st - %g",angDistance, triangle[0][0],triangle[0][1],ct, st);
// XXX todo draw the filled triangle at end.
// draw a red triangle, the point of the arrow
CGContextSetFillColorWithColor(c, [[UIColor greenColor] CGColor]);
CGContextMoveToPoint(c, triangle[0][0], triangle[0][1]);
CGContextAddLineToPoint(c, triangle[1][0], triangle[1][1]);
CGContextAddLineToPoint(c, triangle[2][0], triangle[2][1]);
CGContextFillPath(c);
我本来以为我会做这些点,然后将它们平移到原点,旋转,然后再平移回来,我会笑的。
但是,实际情况并非如此...随着百分比从 0 增加到 2pi,箭头将自己绘制成一条模糊的三角形路线。当 angDistance 为零或 pi 时,箭头位于正确的位置。但是,当我朝向 pi/2 或 3pi/2 时,箭头朝向封闭矩形的下角。
我一定是在做一些明显愚蠢的事情,但我这辈子都看不到。
有什么想法吗?
谢谢,
-肯
我建议为所需形状的整个轮廓构建一条路径,然后 "fill" 该路径具有所需的颜色。这消除了任何间隙或任何不完全排列的风险。
因此,这条路径可能由箭头外侧的弧线、箭头头部的两条线、箭头内侧的弧线组成,然后关闭路径。这可能看起来像:
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, self.arrowColor.CGColor);
CGPoint center = CGPointMake(rect.size.width / 2.0, rect.size.height / 2.0);
CGContextMoveToPoint(context, center.x + cosf(self.startAngle) * (self.radius + self.lineWidth / 2.0),
center.y + sinf(self.startAngle) * (self.radius + self.lineWidth / 2.0));
CGContextAddArc(context, center.x, center.y, self.radius + self.lineWidth / 2.0, self.startAngle, self.endAngle, !self.clockwise);
CGFloat theta = asinf(self.lineWidth / self.radius / 2.0) * (self.clockwise ? 1.0 : -1.0);
CGFloat pointDistance = self.radius / cosf(theta);
CGContextAddLineToPoint(context, center.x + cosf(self.endAngle + theta) * pointDistance,
center.y + sinf(self.endAngle + theta) * pointDistance);
CGContextAddLineToPoint(context, center.x + cosf(self.endAngle) * (self.radius - self.lineWidth / 2.0),
center.y + sinf(self.endAngle) * (self.radius - self.lineWidth / 2.0));
CGContextAddArc(context, center.x, center.y, self.radius - self.lineWidth / 2.0, self.endAngle, self.startAngle, self.clockwise);
CGContextClosePath(context);
CGContextDrawPath(context, kCGPathFill);
}
这里唯一的技巧是为箭头的末端找到正确的点。我改进了选择以更好地处理较粗的箭头,但您应该随意使用您认为最适合您的应用程序的任何内容。
于是,代码如下:
self.arrowView.radius = 100;
self.arrowView.arrowColor = [UIColor blueColor];
self.arrowView.lineWidth = 40;
self.arrowView.startAngle = -M_PI_2;
self.arrowView.endAngle = M_PI;
self.arrowView.clockwise = TRUE;
会产生以下结果(我用 CADisplayLink
制作动画):
这使用零开始角表示“3 点钟”位置,但您显然可以根据需要调整它。但希望它能说明解决问题的一种方法。
顺便说一句,虽然我已经回答了如何使用 CoreGraphics 执行此操作的问题,但我不一定会建议这样做。例如,在 https://github.com/robertmryan/CircularArrowDemo 中,我没有实现 drawRect
,而是更新了一个 CAShapeLayer
。通过这样做,我不仅可以避免 drawRect
效率低下,而且理论上还可以改变您使用此 CAShapeLayer
的方式(例如,将其用作某些 UIView
的 mask
,在它后面揭示一些更有趣的颜色渐变(或其他图像)。
这是另一个解决方案(虽然可扩展性不是很好)。此解决方案假设这就像一个徽标,其中绘制的圆 angle/percent 不会改变。
- (void)drawRect:(CGRect)rect {
UIBezierPath *circleOutline = [UIBezierPath bezierPath];
[self.circleColor setStroke];
[circleOutline setLineWidth:self.bounds.size.width*0.15];
[circleOutline addArcWithCenter:CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2) radius:self.bounds.size.width/2-circleOutline.lineWidth/2 startAngle:3*M_PI/2 endAngle:3*M_PI/4 clockwise:YES];
[circleOutline stroke];
[self addArrowView:circleOutline];
}
- (void)addArrowView:(UIBezierPath *)path {
for (int x = 0; x < self.bounds.size.width/2; x++) {
for (int y = self.bounds.size.height/2; y < self.bounds.size.height; y++) {
if ([path containsPoint:CGPointMake(x, y)]) {
// Pythagorean Theorem - We want the diagonal length of the square to be lineWidth, so we need to calculate what size
// to make each side of the square to make the diagonal equal to lineWidth
double sideLength = sqrt((path.lineWidth*path.lineWidth)/2);
UIView *arrowView = [[UIView alloc] initWithFrame:CGRectMake(x-sideLength/2, y-sideLength/2, sideLength, sideLength)];
arrowView.backgroundColor = self.circleColor;
[self addSubview:arrowView];
return;
}
}
}
}
会产生: