[英]UIBezierPath Triangle with rounded edges
我设计了这段代码来生成一个 Bezier 路径,该路径用作 CAShapeLayer 的路径来屏蔽 UIView(视图的高度和宽度是可变的)
这段代码生成了一个边缘锐利的三角形,但我想让它变成圆角! 我花了 2 个小时尝试使用addArcWithCenter...
、 lineCapStyle
和lineJoinStyle
等,但似乎没有任何效果对我lineJoinStyle
。
UIBezierPath *bezierPath = [UIBezierPath bezierPath];
CGPoint center = CGPointMake(rect.size.width / 2, 0);
CGPoint bottomLeft = CGPointMake(10, rect.size.height - 0);
CGPoint bottomRight = CGPointMake(rect.size.width - 0, rect.size.height - 0);
[bezierPath moveToPoint:center];
[bezierPath addLineToPoint:bottomLeft];
[bezierPath addLineToPoint:bottomRight];
[bezierPath closePath];
所以我的问题是,我将如何在 UIBezierPath 中舍入三角形的所有边缘(我是否需要子层、多条路径等)?
注意,我没有绘制这个 BezierPath,所以drawRect
所有CGContext...
函数都无法帮助我:(
谢谢!
FWIW:这个答案通过解释CGPathAddArcToPoint(...)
为您做什么来CGPathAddArcToPoint(...)
其教育目的。 我强烈建议您通读它,因为它将帮助您理解和欣赏 CGPath API。 然后你应该继续使用它,如an0's answer 所示,而不是当你在你的应用程序中圆边时使用这个代码。 如果您想尝试并了解这样的几何计算,则此代码应仅用作参考。
因为我觉得这样的问题很有趣,所以我不得不回答它:)
这是一个很长的答案。 没有简短的版本:D
注意:为了我自己的简单起见,我的解决方案是对用于形成三角形的点进行一些假设,例如:
如果您愿意,您可以使用相同的技术对任何多边形进行圆角处理,只要它是严格凸的(即不是尖星)。 我不会解释如何做到这一点,但它遵循相同的原则。
这一切都从一个三角形开始,你想用一些半径r把它的角倒圆:
圆角三角形应包含在尖角三角形中,因此第一步是找到尽可能靠近角的位置,在那里您可以拟合半径为 r 的圆。
一个简单的方法是创建 3 条平行于三角形 3 条边的新线,并将每条距离r向内移动,与原始边的边正交。
为此,您计算每条线的斜率/角度以及应用于两个新点的偏移量:
CGFloat angle = atan2f(end.y - start.y,
end.x - start.x);
CGVector offset = CGVectorMake(-sinf(angle)*radius,
cosf(angle)*radius);
注意:为清楚起见,我使用的是 CGVector 类型(在 iOS 7 中可用),但您也可以使用点或大小来处理以前的操作系统版本。
然后将偏移量添加到每条线的起点和终点:
CGPoint offsetStart = CGPointMake(start.x + offset.dx,
start.y + offset.dy);
CGPoint offsetEnd = CGPointMake(end.x + offset.dx,
end.y + offset.dy);
当你这样做时,你会看到这三条线在三个地方相互交叉:
每个交点恰好是与两条边的距离r (假设三角形足够大,如上所述) 。
您可以计算两条线的交点:
// (x1⋅y2-y1⋅x2)(x3-x4) - (x1-x2)(x3⋅y4-y3⋅x4)
// px = –––––––––––––––––––––––––––––––––––––––––––
// (x1-x2)(y3-y4) - (y1-y2)(x3-x4)
// (x1⋅y2-y1⋅x2)(y3-y4) - (y1-y2)(x3⋅y4-y3⋅x4)
// py = –––––––––––––––––––––––––––––––––––––––––––
// (x1-x2)(y3-y4) - (y1-y2)(x3-x4)
CGFloat intersectionX = ((x1*y2-y1*x2)*(x3-x4) - (x1-x2)*(x3*y4-y3*x4)) / ((x1-x2)*(y3-y4) - (y1-y2)*(x3-x4));
CGFloat intersectionY = ((x1*y2-y1*x2)*(y3-y4) - (y1-y2)*(x3*y4-y3*x4)) / ((x1-x2)*(y3-y4) - (y1-y2)*(x3-x4));
CGPoint intersection = CGPointMake(intersectionX, intersectionY);
其中 (x1, y1) 到 (x2, y2) 是第一行,(x3, y3) 到 (x4, y4) 是第二行。
如果然后在每个交点上放置一个半径为r的圆,您可以看到它确实适用于圆角三角形(忽略三角形和圆的不同线宽):
现在要创建圆角三角形,您要创建一条路径,该路径在原始三角形与交点正交的点上从直线变为圆弧再变为直线(等等)。 这也是圆与原始三角形相切的点。
知道三角形中所有 3 条边的斜率、圆角半径和圆心(交点),每个圆角的起始角和终止角就是该边的斜率 - 90 度。 为了将这些东西组合在一起,我在我的代码中创建了一个结构体,但如果您不想,则不必这样做:
typedef struct {
CGPoint centerPoint;
CGFloat startAngle;
CGFloat endAngle;
} CornerPoint;
为了减少代码重复,我为自己创建了一个方法,该方法计算一个点的交点和角度,给定一条线,从一个点,通过另一个点,到最后一点(它不是封闭的,所以它不是三角形):
代码如下(它实际上只是我上面展示的代码,放在一起):
- (CornerPoint)roundedCornerWithLinesFrom:(CGPoint)from
via:(CGPoint)via
to:(CGPoint)to
withRadius:(CGFloat)radius
{
CGFloat fromAngle = atan2f(via.y - from.y,
via.x - from.x);
CGFloat toAngle = atan2f(to.y - via.y,
to.x - via.x);
CGVector fromOffset = CGVectorMake(-sinf(fromAngle)*radius,
cosf(fromAngle)*radius);
CGVector toOffset = CGVectorMake(-sinf(toAngle)*radius,
cosf(toAngle)*radius);
CGFloat x1 = from.x +fromOffset.dx;
CGFloat y1 = from.y +fromOffset.dy;
CGFloat x2 = via.x +fromOffset.dx;
CGFloat y2 = via.y +fromOffset.dy;
CGFloat x3 = via.x +toOffset.dx;
CGFloat y3 = via.y +toOffset.dy;
CGFloat x4 = to.x +toOffset.dx;
CGFloat y4 = to.y +toOffset.dy;
CGFloat intersectionX = ((x1*y2-y1*x2)*(x3-x4) - (x1-x2)*(x3*y4-y3*x4)) / ((x1-x2)*(y3-y4) - (y1-y2)*(x3-x4));
CGFloat intersectionY = ((x1*y2-y1*x2)*(y3-y4) - (y1-y2)*(x3*y4-y3*x4)) / ((x1-x2)*(y3-y4) - (y1-y2)*(x3-x4));
CGPoint intersection = CGPointMake(intersectionX, intersectionY);
CornerPoint corner;
corner.centerPoint = intersection;
corner.startAngle = fromAngle - M_PI_2;
corner.endAngle = toAngle - M_PI_2;
return corner;
}
然后我使用该代码 3 次来计算 3 个角:
CornerPoint leftCorner = [self roundedCornerWithLinesFrom:right
via:left
to:top
withRadius:radius];
CornerPoint topCorner = [self roundedCornerWithLinesFrom:left
via:top
to:right
withRadius:radius];
CornerPoint rightCorner = [self roundedCornerWithLinesFrom:top
via:right
to:left
withRadius:radius];
现在,有了所有必要的数据,我们开始创建实际路径的部分。 我将依赖于这样一个事实,即 CGPathAddArc 将从当前点到起点添加一条直线,而不必自己绘制这些线(这是记录在案的行为)。
我手动计算的唯一点是路径的起点。 我选择右下角开始(没有具体原因)。 从那里,您只需添加一个圆弧,中心位于起点和终点角度的交点处:
CGMutablePathRef roundedTrianglePath = CGPathCreateMutable();
// manually calculated start point
CGPathMoveToPoint(roundedTrianglePath, NULL,
leftCorner.centerPoint.x + radius*cosf(leftCorner.startAngle),
leftCorner.centerPoint.y + radius*sinf(leftCorner.startAngle));
// add 3 arcs in the 3 corners
CGPathAddArc(roundedTrianglePath, NULL,
leftCorner.centerPoint.x, leftCorner.centerPoint.y,
radius,
leftCorner.startAngle, leftCorner.endAngle,
NO);
CGPathAddArc(roundedTrianglePath, NULL,
topCorner.centerPoint.x, topCorner.centerPoint.y,
radius,
topCorner.startAngle, topCorner.endAngle,
NO);
CGPathAddArc(roundedTrianglePath, NULL,
rightCorner.centerPoint.x, rightCorner.centerPoint.y,
radius,
rightCorner.startAngle, rightCorner.endAngle,
NO);
// close the path
CGPathCloseSubpath(roundedTrianglePath);
看起来像这样:
没有所有支持线的最终结果如下所示:
@David 的几何学很酷而且很有教育意义。 但是您真的不需要以这种方式遍历整个几何体。 我将提供一个更简单的代码:
CGFloat radius = 20;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, (center.x + bottomLeft.x) / 2, (center.y + bottomLeft.y) / 2);
CGPathAddArcToPoint(path, NULL, bottomLeft.x, bottomLeft.y, bottomRight.x, bottomRight.y, radius);
CGPathAddArcToPoint(path, NULL, bottomRight.x, bottomRight.y, center.x, center.y, radius);
CGPathAddArcToPoint(path, NULL, center.x, center.y, bottomLeft.x, bottomLeft.y, radius);
CGPathCloseSubpath(path);
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithCGPath:path];
CGPathRelease(path);
bezierPath
正是您所需要的。 关键是CGPathAddArcToPoint
为你做几何。
基于@an0 以上回答的游乐场。
import UIKit
import PlaygroundSupport
import CoreGraphics
var view = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
view.backgroundColor = .black
PlaygroundPage.current.liveView = view
let triangle = CAShapeLayer()
triangle.fillColor = UIColor.systemGreen.cgColor
triangle.path = createRoundedTriangle(width: 220, height: 200, radius: 15)
triangle.position = CGPoint(x: 140, y: 130)
view.layer.addSublayer(triangle)
func createRoundedTriangle(width: CGFloat, height: CGFloat, radius: CGFloat) -> CGPath {
// Draw the triangle path with its origin at the center.
let point1 = CGPoint(x: -width / 2, y: height / 2)
let point2 = CGPoint(x: 0, y: -height / 2)
let point3 = CGPoint(x: width / 2, y: height / 2)
let path = CGMutablePath()
path.move(to: CGPoint(x: 0, y: height / 2))
path.addArc(tangent1End: point1, tangent2End: point2, radius: radius)
path.addArc(tangent1End: point2, tangent2End: point3, radius: radius)
path.addArc(tangent1End: point3, tangent2End: point1, radius: radius)
path.closeSubpath()
return path
}
快速圆角三角形
基于@invisible squirrel 的回答,我写了一个 UIView 扩展。
class TriangleView: UIView {
override func draw(_ rect: CGRect) {
let widthHeight = self.layer.frame.size.height
let triangle = CAShapeLayer()
triangle.fillColor = UIColor.black.cgColor
triangle.path = roundedTriangle(widthHeight: widthHeight)
triangle.position = CGPoint(x: 0, y: 0)
self.layer.addSublayer(triangle)
}
func roundedTriangle(widthHeight: CGFloat) -> CGPath {
let point1 = CGPoint(x: widthHeight/2, y:0)
let point2 = CGPoint(x: widthHeight , y: widthHeight)
let point3 = CGPoint(x: 0, y: widthHeight)
let path = CGMutablePath()
path.move(to: CGPoint(x: 0, y: widthHeight))
path.addArc(tangent1End: point1, tangent2End: point2, radius: 5)
path.addArc(tangent1End: point2, tangent2End: point3, radius: 5)
path.addArc(tangent1End: point3, tangent2End: point1, radius: 5)
path.closeSubpath()
return path
}
}
@an0 - 谢谢! 我是全新的,所以我没有标记为有用或评论的声誉,但你今天为我节省了大量时间。
我知道这超出了问题的范围,但相同的逻辑适用于CGContextBeginPath()
、 CGContextMoveToPoint()
、 CGContextAddArcToPoint()
和CGContextClosePath()
以防有人需要使用它。
如果有人感兴趣,这是我的等边三角形指向右侧的代码。 triangleFrame
CGRect
是预定义的CGRect
, radius
是预定义的CGFloat
。
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextBeginPath(context);
CGContextMoveToPoint(context, CGRectGetMinX(triangleFrame), CGRectGetMidY(triangleFrame)); CGRectGetMidY(triangleFrame));
CGContextAddArcToPoint(context, CGRectGetMinX(triangleFrame), CGRectGetMidY(triangleFrame), CGRectGetMinX(triangleFrame), CGRectGetMinY(triangleFrame), radius);
CGContextAddArcToPoint(context, CGRectGetMinX(triangleFrame), CGRectGetMinY(triangleFrame), CGRectGetMaxX(triangleFrame), CGRectGetMidY(triangleFrame), radius);
CGContextAddArcToPoint(context, CGRectGetMaxX(triangleFrame), CGRectGetMidY(triangleFrame), CGRectGetMinX(triangleFrame), CGRectGetMaxY(triangleFrame), radius);
CGContextAddArcToPoint(context, CGRectGetMinX(triangleFrame), CGRectGetMaxY(triangleFrame), CGRectGetMinX(triangleFrame), CGRectGetMidY(triangleFrame), radius);
当然,对于不规则三角形,可以更具体地定义每个点。 然后,您可以根据需要使用CGContextFillPath()
或CGContextStrokePath()
。
根据UIBezierPath
的回答,我编写了一个UIBezierPath
扩展,它添加了与 CGPath 相同的功能:
extension UIBezierPath {
// Based on https://stackoverflow.com/a/20646446/634185
public func addArc(from startPoint: CGPoint,
to endPoint: CGPoint,
centerPoint: CGPoint,
radius: CGFloat,
clockwise: Bool) {
let fromAngle = atan2(centerPoint.y - startPoint.y,
centerPoint.x - startPoint.x)
let toAngle = atan2(endPoint.y - centerPoint.y,
endPoint.x - centerPoint.x)
let fromOffset = CGVector(dx: -sin(fromAngle) * radius,
dy: cos(fromAngle) * radius)
let toOffset = CGVector(dx: -sin(toAngle) * radius,
dy: cos(toAngle) * radius)
let x1 = startPoint.x + fromOffset.dx
let y1 = startPoint.y + fromOffset.dy
let x2 = centerPoint.x + fromOffset.dx
let y2 = centerPoint.y + fromOffset.dy
let x3 = centerPoint.x + toOffset.dx
let y3 = centerPoint.y + toOffset.dy
let x4 = endPoint.x + toOffset.dx
let y4 = endPoint.y + toOffset.dy
let intersectionX =
((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4))
/ ((x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4))
let intersectionY =
((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4))
/ ((x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4))
let intersection = CGPoint(x: intersectionX, y: intersectionY)
let startAngle = fromAngle - .pi / 2.0
let endAngle = toAngle - .pi / 2.0
addArc(withCenter: intersection,
radius: radius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: clockwise)
}
}
所以有了这个,代码应该是这样的:
let path = UIBezierPath()
let center = CGPoint(x: rect.size.width / 2, y: 0)
let bottomLeft = CGPoint(x: 10, y: rect.size.height - 0)
let bottomRight = CGPoint(x: rect.size.width - 0, y: rect.size.height - 0)
path.move(to: center);
path.addArc(from: center,
to: bottomRight,
centerPoint: bottomLeft,
radius: radius,
clockwise: false)
path.addLine(to: bottomRight)
path.close()
只需添加两行代码即可,要简单得多:
UIBezierPath *bezierPath = [UIBezierPath bezierPath];
bezierPath.lineCapStyle = kCGLineCapRound;
bezierPath.lineJoinStyle = kCGLineJoinRound;
CGPoint center = CGPointMake(rect.size.width / 2, 0);
...
编辑:
要查看圆角,您需要使三角形比矩形边界小一点:
CGPoint center = CGPointMake(rect.size.width / 2, 10);
CGPoint bottomLeft = CGPointMake(10, rect.size.height - 10);
CGPoint bottomRight = CGPointMake(rect.size.width - 10, rect.size.height - 10);
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.