以编程方式绘制iOS 7风格的圆角矩形。

31
我正在尝试使用核心图形以编程方式绘制iOS 7样式的图标“Squircle”形状,我不是在问如何绘制圆角矩形。 Squircle是一个超椭圆形:
它与常规的圆角矩形略有不同:
其确切公式可以轻松获得。 但是,我无法弄清楚如何使用CGPath之类的方法绘制它,更不用说填充它并能够相对轻松地调整其大小。而且还要完全符合公式。

不,我知道如何制作圆角矩形;我真的是一种超级省略号类型的 squircle,就像 iOS 7 上的 SpringBoard 图标所使用的那样。 - Remy Vanherweghem
2
@RemyVanherweghem 在iOS 7中,bezierPathWithRoundedRect方法被修改以绘制更平滑的角落。而且它似乎不是一个squircle(方圆形):http://blog.mikeswanson.com/post/62341902567/unleashing-genetic-algorithms-on-the-ios-7-icon - millimoose
话虽如此,UIBezierPath 方法并不能完美匹配图标模板,但比以前更接近了。 - millimoose
8个回答

26

5
嘭!这应该是被认可的答案。这就是我们在2020年的做事方式,朋友们。 - Trev14
无法将类型为“CALayerCornerCurve”的值分配给类型为“CGFloat”的变量。 - Ahmadreza
属性名为.cornerCurveyourLayer.cornerCurve = CALayerCornerCurve.continuous - AnthonyR

17

维基百科引用: 超椭圆

特别地,对于 n=1/2,每个弧都是由两个轴定义的二次贝塞尔曲线;因此,每个弧都是抛物线的一部分。

那么为什么不尝试使用贝塞尔曲线来逼近 Squircle? Bezier 和 Squircle 都由参数方程式定义。

UIBezierPath 类有一个方法:addCurveToPoint:controlPoint1:controlPoint2:

将立方贝塞尔曲线附加到接收器的路径上。

注意:使用 addQuadCurveToPoint:controlPoint: 方法会产生更差的结果 - 已测试。

我使用了这种方法,结果如下:

红线 - 圆角矩形,蓝线 - 由四条贝塞尔曲线构成的矩形

Rounded rectangle vs cubic Bezier curve

如果对这个结果感兴趣 - 下面是绘图代码。

注意:为了实现更精确的匹配,可能需要更改四个 角点 的坐标(现在它们对应于包含图形的矩形的角度)。

CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);

//set rect size for draw
float rectSize = 275.;
CGRect rectangle = CGRectMake(CGRectGetMidX(rect) - rectSize/2, CGRectGetMidY(rect) - rectSize/2, rectSize, rectSize);

//Rounded rectangle
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
UIBezierPath* roundedPath = [UIBezierPath bezierPathWithRoundedRect:rectangle cornerRadius:rectSize/4.7];
[roundedPath stroke];

//Rectangle from Fours Bezier Curves
CGContextSetStrokeColorWithColor(context, [UIColor blueColor].CGColor);
UIBezierPath *bezierCurvePath = [UIBezierPath bezierPath];

//set coner points
CGPoint topLPoint = CGPointMake(CGRectGetMinX(rectangle), CGRectGetMinY(rectangle));
CGPoint topRPoint = CGPointMake(CGRectGetMaxX(rectangle), CGRectGetMinY(rectangle));
CGPoint botLPoint = CGPointMake(CGRectGetMinX(rectangle), CGRectGetMaxY(rectangle));
CGPoint botRPoint = CGPointMake(CGRectGetMaxX(rectangle), CGRectGetMaxY(rectangle));

//set start-end points
CGPoint midRPoint = CGPointMake(CGRectGetMaxX(rectangle), CGRectGetMidY(rectangle));
CGPoint botMPoint = CGPointMake(CGRectGetMidX(rectangle), CGRectGetMaxY(rectangle));
CGPoint topMPoint = CGPointMake(CGRectGetMidX(rectangle), CGRectGetMinY(rectangle));
CGPoint midLPoint = CGPointMake(CGRectGetMinX(rectangle), CGRectGetMidY(rectangle));

//Four Bezier Curve
[bezierCurvePath moveToPoint:midLPoint];
[bezierCurvePath addCurveToPoint:topMPoint controlPoint1:topLPoint controlPoint2:topLPoint];
[bezierCurvePath moveToPoint:midLPoint];
[bezierCurvePath addCurveToPoint:botMPoint controlPoint1:botLPoint controlPoint2:botLPoint];
[bezierCurvePath moveToPoint:midRPoint];
[bezierCurvePath addCurveToPoint:topMPoint controlPoint1:topRPoint controlPoint2:topRPoint];
[bezierCurvePath moveToPoint:midRPoint];
[bezierCurvePath addCurveToPoint:botMPoint controlPoint1:botRPoint controlPoint2:botRPoint];

[bezierCurvePath stroke];

CGContextRestoreGState(context);

7

下面是已经填写好的答案,同时也已经转换为Swift版本:

override func draw(_ rect: CGRect) {
    super.draw(rect)

    guard let context = UIGraphicsGetCurrentContext() else {
        return
    }
    context.saveGState()

    let rect = self.bounds
    let rectSize: CGFloat = rect.width
    let rectangle = CGRect(x: rect.midX - rectSize / 2, y: rect.midY - rectSize / 2, width: rectSize, height: rectSize)

    let topLPoint = CGPoint(x: rectangle.minX, y: rectangle.minY)
    let topRPoint = CGPoint(x: rectangle.maxX, y: rectangle.minY)
    let botLPoint = CGPoint(x: rectangle.minX, y: rectangle.maxY)
    let botRPoint = CGPoint(x: rectangle.maxX, y: rectangle.maxY)

    let midRPoint = CGPoint(x: rectangle.maxX, y: rectangle.midY)
    let botMPoint = CGPoint(x: rectangle.midX, y: rectangle.maxY)
    let topMPoint = CGPoint(x: rectangle.midX, y: rectangle.minY)
    let midLPoint = CGPoint(x: rectangle.minX, y: rectangle.midY)

    let bezierCurvePath = UIBezierPath()
    bezierCurvePath.move(to: midLPoint)
    bezierCurvePath.addCurve(to: topMPoint, controlPoint1: topLPoint, controlPoint2: topLPoint)
    bezierCurvePath.addCurve(to: midRPoint, controlPoint1: topRPoint, controlPoint2: topRPoint)
    bezierCurvePath.addCurve(to: botMPoint, controlPoint1: botRPoint, controlPoint2: botRPoint)
    bezierCurvePath.addCurve(to: midLPoint, controlPoint1: botLPoint, controlPoint2: botLPoint)

    context.setFillColor(UIColor.lightGray.cgColor)
    bezierCurvePath.fill()

    context.restoreGState()
}

非常适合在UIView子类中使用。


5
在Ruslan和Sunkas上面的答案基础上,我创建了一条路径,将超椭圆形的“角”与直线段连接起来;即一个超椭圆形的模拟常规圆角矩形(例如iPhone X模拟器边缘看到的掩模)。
extension UIBezierPath {

    static func superellipse(in rect: CGRect, cornerRadius: CGFloat) -> UIBezierPath {

        // (Corner radius can't exceed half of the shorter side; correct if
        // necessary:)
        let minSide = min(rect.width, rect.height)
        let radius = min(cornerRadius, minSide/2)

        let topLeft = CGPoint(x: rect.minX, y: rect.minY)
        let topRight = CGPoint(x: rect.maxX, y: rect.minY)
        let bottomLeft = CGPoint(x: rect.minX, y: rect.maxY)
        let bottomRight = CGPoint(x: rect.maxX, y: rect.maxY)

        // The two points of the segment along the top side (clockwise):
        let p0 = CGPoint(x: rect.minX + radius, y: rect.minY)
        let p1 = CGPoint(x: rect.maxX - radius, y: rect.minY)

        // The two points of the segment along the right side (clockwise):
        let p2 = CGPoint(x: rect.maxX, y: rect.minY + radius)
        let p3 = CGPoint(x: rect.maxX, y: rect.maxY - radius)

        // The two points of the segment along the bottom side (clockwise):
        let p4 = CGPoint(x: rect.maxX - radius, y: rect.maxY)
        let p5 = CGPoint(x: rect.minX + radius, y: rect.maxY)

        // The two points of the segment along the left side (clockwise):
        let p6 = CGPoint(x: rect.minX, y: rect.maxY - radius)
        let p7 = CGPoint(x: rect.minX, y: rect.minY + radius)

        let path = UIBezierPath()
        path.move(to: p0)
        path.addLine(to: p1)
        path.addCurve(to: p2, controlPoint1: topRight, controlPoint2: topRight)
        path.addLine(to: p3)
        path.addCurve(to: p4, controlPoint1: bottomRight, controlPoint2: bottomRight)
        path.addLine(to: p5)
        path.addCurve(to: p6, controlPoint1: bottomLeft, controlPoint2: bottomLeft)
        path.addLine(to: p7)
        path.addCurve(to: p0, controlPoint1: topLeft, controlPoint2: topLeft)

        return path
    }
}

代码中的点p0p7可以在以下图表中可视化:

enter image description here

如果您传递的矩形实际上是一个正方形,并且圆角半径等于或大于边长的一半,则直线线段会折叠(p0“合并”为p1p2p3等),从而得到标准超椭圆。

3

这里其他的答案看起来并不像真正的东西。如果你想要一个实际匹配 iOS 图标形状的东西,那么有一个详细的方法在这里,其中包含许多非常神奇的数字。向 PaintCode 致敬。


2

这不是一个很好的答案,因为它没有深入回答你所问的问题,即如何以编程方式绘制超椭圆形**,但你可以:

  1. Download the SVG for the iOS7 icon shape here: http://dribbble.com/shots/1127699-iOS-7-icon-shape-PSD
  2. Import it to your Xcode project
  3. Add PocketSVG to your project: https://github.com/arielelkin/PocketSVG
  4. Load the SVG, and convert it to a UIBezierPath, from there you can scale and transform however you like:

    PocketSVG *myVectorDrawing = [[PocketSVG alloc] initFromSVGFileNamed:@"iOS_7_icon_shape"];
    
    UIBezierPath *myBezierPath = myVectorDrawing.bezier;
    
    // Apply your transforms here:
    [myBezierPath applyTransform:CGAffineTransformMakeScale(2.5, 2.5)];
    [myBezierPath applyTransform:CGAffineTransformMakeTranslation(10, 50)];
    
    CAShapeLayer *myShapeLayer = [CAShapeLayer layer];
    myShapeLayer.path = myBezierPath.CGPath;
    myShapeLayer.strokeColor = [[UIColor redColor] CGColor];
    myShapeLayer.lineWidth = 2;
    myShapeLayer.fillColor = [[UIColor clearColor] CGColor];
    
    [self.view.layer addSublayer:myShapeLayer];  
    

值得注意的是,这个形状可能并不是一个精确的超椭圆形: http://i.imgur.com/l0ljVRo.png


2
感谢马克提供的链接和PaintCode的解决方案。它确实产生了与iOS 13中添加的CALayerCornerCurve.continuous相同的结果。这是PaintCode的objc扩展的Swift版本:
extension UIBezierPath {
    /// Source: [PaintCode](https://www.paintcodeapp.com/news/code-for-ios-7-rounded-rectangles)
    static func iOS7RoundedRect(in rect: CGRect, cornerRadius radius: CGFloat) -> UIBezierPath {
        let limit: CGFloat = min(rect.size.width, rect.size.height) / 2 / 1.52866483
        let limitedRadius: CGFloat = min(radius, limit)
        
        func topLeft(_ x: CGFloat, _ y: CGFloat) -> CGPoint {
            CGPoint(x: rect.origin.x + x * limitedRadius, y: rect.origin.y + y * limitedRadius)
        }

        func topRight(_ x: CGFloat, _ y: CGFloat) -> CGPoint {
            CGPoint(x: rect.origin.x + rect.size.width - x * limitedRadius, y: rect.origin.y + y * limitedRadius)
        }
            
        func bottomRight(_ x: CGFloat, _ y: CGFloat) -> CGPoint {
            CGPoint(x: rect.origin.x + rect.size.width - x * limitedRadius, y: rect.origin.y + rect.size.height - y * limitedRadius)
        }
            
        func bottomLeft(_ x: CGFloat, _ y: CGFloat) -> CGPoint {
            CGPoint(x: rect.origin.x + x * limitedRadius, y: rect.origin.y + rect.size.height - y * limitedRadius)
        }
        
        let path = UIBezierPath()
        
        path.move(to: topLeft(1.52866483, 0.00000000))
        path.addLine(to: topRight(1.52866471, 0.00000000))
        path.addCurve(to: topRight(0.66993427, 0.06549600),
                      controlPoint1: topRight(1.08849323, 0.00000000),
                      controlPoint2: topRight(0.86840689, 0.00000000))
        path.addLine(to: topRight(0.63149399, 0.07491100))
        path.addCurve(to: topRight(0.07491176, 0.63149399),
                      controlPoint1: topRight(0.37282392, 0.16905899),
                      controlPoint2: topRight(0.16906013, 0.37282401))
        path.addCurve(to: topRight(0.00000000, 1.52866483),
                      controlPoint1: topRight(0.00000000, 0.86840701),
                      controlPoint2: topRight(0.00000000, 1.08849299))
        path.addLine(to: bottomRight(0.00000000, 1.52866471))
        path.addCurve(to: bottomRight(0.06549569, 0.66993493),
                      controlPoint1: bottomRight(0.00000000, 1.08849323),
                      controlPoint2: bottomRight(0.00000000, 0.86840689))
        path.addLine(to: bottomRight(0.07491111, 0.63149399))
        path.addCurve(to: bottomRight(0.63149399, 0.07491111),
                      controlPoint1: bottomRight(0.16905883, 0.37282392),
                      controlPoint2: bottomRight(0.37282392, 0.16905883))
        path.addCurve(to: bottomRight(1.52866471, 0.00000000),
                      controlPoint1: bottomRight(0.86840689, 0.00000000),
                      controlPoint2: bottomRight(1.08849323, 0.00000000))
        path.addLine(to: bottomLeft(1.52866483, 0.00000000))
        path.addCurve(to: bottomLeft(0.66993397, 0.06549569),
                      controlPoint1: bottomLeft(1.08849299, 0.00000000),
                      controlPoint2: bottomLeft(0.86840701, 0.00000000))
        path.addLine(to: bottomLeft(0.63149399, 0.07491111))
        path.addCurve(to: bottomLeft(0.07491100, 0.63149399),
                      controlPoint1: bottomLeft(0.37282401, 0.16905883),
                      controlPoint2: bottomLeft(0.16906001, 0.37282392))
        path.addCurve(to: bottomLeft(0.00000000, 1.52866471),
                      controlPoint1: bottomLeft(0.00000000, 0.86840689),
                      controlPoint2: bottomLeft(0.00000000, 1.08849323))
        path.addLine(to: topLeft(0.00000000, 1.52866483))
        path.addCurve(to: topLeft(0.06549600, 0.66993397),
                      controlPoint1: topLeft(0.00000000, 1.08849299),
                      controlPoint2: topLeft(0.00000000, 0.86840701))
        path.addLine(to: topLeft(0.07491100, 0.63149399))
        path.addCurve(to: topLeft(0.63149399, 0.07491100),
                      controlPoint1: topLeft(0.16906001, 0.37282401),
                      controlPoint2: topLeft(0.37282401, 0.16906001))
        path.addCurve(to: topLeft(1.52866483, 0.00000000),
                      controlPoint1: topLeft(0.86840701, 0.00000000),
                      controlPoint2: topLeft(1.08849299, 0.00000000))
        
        path.close()
        
        return path
    }
}

当我将iOS 15上生成的UIBezierPath路径与此代码生成的路径进行比较时,我注意到参数略有过时。但是差异非常小。 - Honghao Z

1

如果使用OpenGL ES Shader,这个应该很容易实现。只需绘制一个四边形并将x和y作为顶点属性传递。在片元着色器中,将x和y插入到方程式中。如果结果小于等于1,则片元在图形内部。如果我有空闲时间,我可能会尝试一下并在此处发布。

如果要使用CGPath,我认为关键是将x和y参数化为t的函数,t从0到2π。然后以规则间隔评估x和y。我也会在空闲时间里尝试弄清楚这一点,但我的数学有些生疏。

顺便说一句,我确信苹果公司没有使用这个公式。请参见@millimoose发布的链接:http://blog.mikeswanson.com/post/62341902567/unleashing-genetic-algorithms-on-the-ios-7-icon


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接