I'm trying to find a way to draw a iOS 7-style icon 'squircle' shape programmatically, using core graphics. I'm not asking how to draw a rounded rectangle. A squircle is a superellipse:

Squircle

which is slightly different than a regular rounded rectangle: Rounded rectangle vs squircle

Music icon in squircle

It's exact formula is readily available. However, I can't figure out how to draw this using, for example, a CGPath, let alone fill it, and be able to resize it rather easily. All this while being entirely exact with the formula.

有帮助吗?

解决方案

in iOS 13/ Xcode 11 you can now use CALayerCornerCurve

Example

yourLayer.cornerCurve = CALayerCornerCurve.continuous

source: https://developer.apple.com/documentation/quartzcore/calayercornercurve

其他提示

Quote from Wikipedia: Superellipse

For n = 1/2, in particular, each of the four arcs is a Quadratic Bézier curve defined by the two axes; as a result, each arc is a segment of a parabola.

So why not try to approximate Squircle using Bezier curves? Both curves (Bezier and Squircle) are defined by the parametric equations.

UIBezierPath Class have method: addCurveToPoint:controlPoint1:controlPoint2:

Appends a cubic Bézier curve to the receiver’s path.

NOTE: Use of the addQuadCurveToPoint:controlPoint: method gives worse results - tested.

I used this method and that's what happened as a result:

red line - rounded rectangle, blue line - rectangle from fours Bezier curves

Rounded rectangle vs cubic Bezier curve

If this result is interested - drawing code below.

NOTE: To achieve a more exact match Bezier curve can be required to change the coordinates of the four corner points (now they correspond to the angles of the rectangle in which is inscribed the figure).

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

A filled version of the accepted answer, also ported to 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()
}

perfect for use in a UIView subclass.

Building on top of Ruslan's and Sunkas' answers above, I created a path that joins superelliptic "corners" with straight line segments; i.e. a superelliptic analog of the regular, rounded rectangle (like the mask seen around the edges of the iPhone X Simulator):

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

The points p0 through p7 in the code can be visualized in the following diagram:

enter image description here

If you pass a rectangle that is actually a square, and the corner radius is equal to or greater than half the side length, the straight line segments collapse (p0 "merges" with p1, p2 with p3, etc.) and you get the standard superellipse.

This isn't a great answer since it doesn't get to the heart of what you're asking which is how to draw a superellipse** programmatically, but you can:

  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];  
    

** It's maybe worth noting that the shape might not be an exact superellipse anyway: http://i.imgur.com/l0ljVRo.png

The other answers here don't look like the real thing. If you want something that actually matches the shape of an iOS icon then there's a detailed method here with a lot of very magic numbers. Kudos to PaintCode for working it out.

This would be pretty easy to do in OpenGL ES with a shader. Just draw a quad and pass in the x and y as vertex attributes. In the fragment shader, plug x and y into the equation. If the result <= 1, then the fragment is inside the shape. If I can get some free time I might try this and post it here.

If you want to use CGPath, I think the key is to parameterize x and y in terms of t, which goes from 0 to 2π. Then evaluate x and y at regular intervals. I'll try to figure this out in my free time, too, but my math is kind of rusty.

BTW, I'm certain that Apple is not using this formula. See the link that @millimoose posted: http://blog.mikeswanson.com/post/62341902567/unleashing-genetic-algorithms-on-the-ios-7-icon

Thank you Mark for the link and to PaintCode for figuring it out. It does indeed produce the same result as CALayerCornerCurve.continuous added in iOS 13. Here's Swift version of PaintCode's objc extension:

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
    }
}
许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top