首页 文章

UIBezierPath三角形,圆边

提问于
浏览
19

我设计了这个代码来生成一个Bezier路径,用作CAShapeLayer掩盖UIView的路径(视图的高度和宽度是可变的)

这段代码生成一个边缘锐利的三角形,但我想让它成为一个圆角!我花了2个小时尝试使用 addArcWithCenter...lineCapStylelineJoinStyle 等,但似乎没有什么对我有用 .

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中舍入三角形的所有边缘(我需要子层,多路径等)吗?

N.B我不是在绘制这个BezierPath所以 drawRect 中的所有 CGContext... 函数都无法帮助我:(

谢谢!

6 回答

  • 0

    更复杂的是,只需添加两行代码:

    UIBezierPath *bezierPath = [UIBezierPath bezierPath];
    bezierPath.lineCapStyle = kCGLineCapRound;
    bezierPath.lineJoinStyle = kCGLineJoinRound;
    
    CGPoint center = CGPointMake(rect.size.width / 2, 0);
    ...
    

    EDIT:

    要看到角是圆角的,你需要使三角形比直线边界小一点:

    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);
    
  • 13

    基于@ David的answer我写了一个小的 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()
    
  • 103

    编辑

    FWIW: 这个答案通过解释 CGPathAddArcToPoint(...) 为您做什么来达到其教育目的 . 我强烈建议您仔细阅读,因为它将帮助您理解和欣赏CGPath API . 那么你应该继续使用它,如an0's answer中所示,而不是在您的应用程序中舍入边缘时使用此代码 . 如果您想要使用此代码并且了解这样的几何计算,则此代码应仅用作参考 .


    原始答案

    因为我发现这样的问题很有趣,我不得不回答:)

    这是一个很长的答案 . 没有简短的版本:D


    注意:为了我自己的简单,我的解决方案是对用于形成三角形的点做出一些假设,例如:

    • 三角形的面积足够大以适合圆角(例如,三角形的高度大于角落中圆圈的直径 . 我没有检查或试图阻止任何可能的奇怪结果否则发生 .

    • 角以逆时针顺序列出 . 您可以使它适用于任何订单,但为了简单起见,它感觉像是一个公平的约束 .

    如果你愿意,你可以使用相同的技术来对任何多边形进行舍入,只要它尽可能地解释如何做,但它遵循相同的原则 .


    这一切都以一个三角形开始,你想要用一些半径圆角,r:

    enter image description here

    圆角三角形应该包含在尖三角形中,所以第一步是找到尽可能靠近角落的位置,在这里您可以使用半径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中可用),但您也可以使用点或大小来使用以前的OS版本 .

    然后将偏移量添加到每行的起点和终点:

    CGPoint offsetStart = CGPointMake(start.x + offset.dx,
                                      start.y + offset.dy);
    
    CGPoint offsetEnd   = CGPointMake(end.x + offset.dx,
                                      end.y + offset.dy);
    

    当你这样做时,你会看到三条线在三个地方相互交叉:

    enter image description here

    每个交叉点恰好是两个边的距离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的圆,则可以看到它确实是圆角三角形(忽略三角形和圆形的不同线宽):

    enter image description here

    现在要创建一个圆角三角形,您要创建一条路径,该路径从一条线变为一条弧,再变换为原始三角形与交叉点正交的点上的线(等) . 这也是圆与原始三角形相切的点 .

    知道三角形中所有3个边的斜率,圆角半径和圆的中心(交点),每个圆角的起始和终止角度是该边的斜率--90度 . 为了将这些东西组合在一起,我在我的代码中创建了一个结构,但如果你不想这样做,则不必:

    typedef struct {
        CGPoint centerPoint;
        CGFloat startAngle;
        CGFloat endAngle;
    } CornerPoint;
    

    为了减少代码重复,我为自己创建了一个方法,计算一个点的交点和角度给定从一个点到另一个点的线到最终点(它没有关闭,所以它不是三角形):

    enter image description here

    代码如下(它实际上只是我上面显示的代码,放在一起):

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

    看起来像这样:

    enter image description here

    没有所有支持线的最终结果如下所示:

    enter image description here

  • -2

    @ 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 为你做几何 .

  • 1

    Swift 4中的圆角三角形

    enter image description here

    基于@ an0的answer以上:

    override func viewDidLoad() {
        let triangle = CAShapeLayer()
        triangle.fillColor = UIColor.lightGray.cgColor
        triangle.path = createRoundedTriangle(width: 200, height: 200, radius: 8)
        triangle.position = CGPoint(x: 200, y: 300)
    
        view.layer.addSublayer(triangle)
    }
    
    func createRoundedTriangle(width: CGFloat, height: CGFloat, radius: CGFloat) -> CGPath {
        // Points of the triangle
        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
    }
    

    将三角形创建为UIBezierPath

    let cgPath = createRoundedTriangle(width: 200, height: 200, radius: 8)
    let path = UIBezierPath(cgPath: cgPath)
    
  • 33

    @ an0-谢谢!我是全新的,所以我没有声称有用或评论的声誉,但你今天节省了我很多时间 .

    我知道这超出了问题的范围,但是相同的逻辑适用于 CGContextBeginPath()CGContextMoveToPoint()CGContextAddArcToPoint()CGContextClosePath() ,以防任何人需要使用它 .

    如果有人有兴趣,这是我的等边三角形指向右边的代码 . triangleFrame 是预定义的 CGRectradius 是预定义的 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() .

相关问题