Search code examples
c#iosxamarinpolygoncgpath

Xamarin iOS CGPath draw polygon with curved/rounded corners


I'm trying to create a class that draws a polygon in a custom UIView subclass. I have it working, but now I want to smooth the corners by rounding them off and I'm not sure how to do that. Here is what I have so far:

public class MyView : UIView
{
    private List<CGPoint> points;

    public MyView(CGRect frame, List<CGPoint> points) : base (frame)
    {
        this.points = points;
    }

    public override Draw (CGRect rect)
    {
        CGContext context = UIGraphics.GetCurrentContext();

        context.SetLineWidth(2);
        UIColor.Black.SetStroke();
        UIColor.Green.SetFill();

        CGPath path = new CGPath();
        path.AddLines(points.ToArray());
        path.CloseSubpath();

        context.AddPath(path);
        context.Clip();

        using (CGColorSpace rgb = CGColorSpace.CreateDeviceRGB())
        {
            CGGradient gradient = new CGGradient (rgb, new CGColor[]
            {
                new CGColor(0, 1, 0),
                new CGColor(0, 0.5f, 0),
                new CGColor(0, 1, 0),
                new CGColor(0, 0.5f, 0)
            });

            context.DrawLinearGradient(gradient,
                new CGPoint (path.BoundingBox.Left, path.BoundingBox.Top), 
                new CGPoint (path.BoundingBox.Right, path.BoundingBox.Bottom), 
                CGGradientDrawingOptions.DrawsBeforeStartLocation);
        }
    }
}

With the points I'm throwing in I get this:

enter image description here

I've tried using path.AddCurveToPoint and path.AddArc but I can't seem to get them to work the way I want. Any help would be appreciated.

Edit

I played around with path.AddCurveToPoint and now it looks like this:

enter image description here

It starts off correctly, but then just completely goes nuts. Not sure what I'm doing wrong here. Here is my updated Draw override:

public override Draw (CGRect rect)
{
    CGContext context = UIGraphics.GetCurrentContext();

    context.SetLineWidth(2);
    UIColor.Black.SetStroke();
    UIColor.Green.SetFill();

    CGPath path = new CGPath();
    path.AddLines(points.ToArray());
    path.CloseSubpath();
    // updated section
    int count = points.Count;

    for (int x = 1; x < count; x++)
    {
        var p = points[x];
        if (x != 0)
        {
            var prev = points[x - 1];
            if (prev.Y != p.Y)
            {
                if (prev.Y > p.Y)
                {
                    path.AddCurveToPoint(prev, new CGPoint(p.X - prev.X, p.Y), p);
                }
                else
                {
                    //???
                }
            }
        }
    }
    // end updated section
    context.AddPath(path);
    context.Clip();

    using (CGColorSpace rgb = CGColorSpace.CreateDeviceRGB())
    {
        CGGradient gradient = new CGGradient (rgb, new CGColor[]
        {
            new CGColor(0, 1, 0),
            new CGColor(0, 0.5f, 0),
            new CGColor(0, 1, 0),
            new CGColor(0, 0.5f, 0)
        });

        context.DrawLinearGradient(gradient,
            new CGPoint (path.BoundingBox.Left, path.BoundingBox.Top), 
            new CGPoint (path.BoundingBox.Right, path.BoundingBox.Bottom), 
            CGGradientDrawingOptions.DrawsBeforeStartLocation);
    }
}

Solution

  • Update:

    With your update, I see your issue now. You are trying to use your point list as both the Bézier control points and the draw points for the polygon corners, that just will not work.

    The "easiest" way it to use your existing point list as the control points and create mid-way knots between each control point and add a quad curve to each segment.

    Note: The harder way is to use your list of points as the knots and you create the control points for each bezier curve segment. I will not go into it here, but a number of charting systems use this article as their reference and the return values from getControlPoints would be used as the control points for the method AddCurveToPoint.

    Easy way example:

    enter image description here

    var color = UIColor.Red;
    var color2 = UIColor.White;
    
    var points = new List<CGPoint>() { 
        new CGPoint(136.49f, 134.6f),
        new CGPoint(197.04f, 20.0f),
        new CGPoint(257.59f, 20.0f),
        new CGPoint(303.0f, 134.6f),
        new CGPoint(303.0f, 252.0f),
        new CGPoint(28.0f, 252.0f),
        new CGPoint(28.0f, 134.6f),
        new CGPoint(136.49f, 134.6f)
    };
    
    UIBezierPath polygonPath = new UIBezierPath();
    polygonPath.MoveTo(points[0]);
    polygonPath.AddLineTo(points[1]);
    polygonPath.AddLineTo(points[2]);
    polygonPath.AddLineTo(points[3]);
    polygonPath.AddLineTo(points[4]);
    polygonPath.AddLineTo(points[5]);
    polygonPath.AddLineTo(points[6]);
    polygonPath.ClosePath();
    polygonPath.Fill();
    color2.SetStroke();
    polygonPath.LineWidth = 10.0f;
    polygonPath.Stroke();
    
    UIBezierPath smoothedPath = new UIBezierPath();
    var m0 = MidPoint(points[0], points[1]);
    smoothedPath.MoveTo(m0);
    smoothedPath.AddQuadCurveToPoint(m0, points[0]);
    var m1 = MidPoint(points[1], points[2]);
    smoothedPath.AddQuadCurveToPoint(m1, points[1]);
    var m2 = MidPoint(points[2], points[3]);
    smoothedPath.AddQuadCurveToPoint(m2, points[2]);
    var m3 = MidPoint(points[3], points[4]);
    smoothedPath.AddQuadCurveToPoint(m3, points[3]);
    var m4 = MidPoint(points[4], points[5]);
    smoothedPath.AddQuadCurveToPoint(m4, points[4]);
    var m5 = MidPoint(points[5], points[6]);
    smoothedPath.AddQuadCurveToPoint(m5, points[5]);
    var m6 = MidPoint(points[6], points[0]);
    smoothedPath.AddQuadCurveToPoint(m6, points[6]);
    
    smoothedPath.AddQuadCurveToPoint(m0, points[0]); 
    smoothedPath.ClosePath();
    
    color.SetStroke();
    smoothedPath.LineWidth = 5.0f;
    smoothedPath.Stroke();
    

    Original:

    1) I am unsure of how much joint smoothing you are looking, but CGLineJoin.Round for the LineJoinStyle on a UIBezierPath will create a subtle look.

    2) If you need heavy corner smoothing, you can manually calculate start/end of each line and add an ArcWithCenter to join each line segment.

    3) Or without the manual corner arc calculations you can set a really heavy line width while using CGLineJoin.Round and size your drawing slightly smaller to make up for the increased line width.

    Example:

    var color = UIColor.FromRGBA(0.129f, 0.467f, 0.874f, 1.000f);
    
    UIBezierPath polygonPath = new UIBezierPath();
    polygonPath.MoveTo(new CGPoint(136.49f, 134.6f));
    polygonPath.AddLineTo(new CGPoint(197.04f, 20.0f));
    polygonPath.AddLineTo(new CGPoint(257.59f, 20.0f));
    polygonPath.AddLineTo(new CGPoint(303.0f, 134.6f));
    polygonPath.AddLineTo(new CGPoint(303.0f, 252.0f));
    polygonPath.AddLineTo(new CGPoint(28.0f, 252.0f));
    polygonPath.AddLineTo(new CGPoint(28.0f, 134.6f));
    polygonPath.AddLineTo(new CGPoint(136.49f, 134.6f));
    polygonPath.ClosePath();
    polygonPath.LineJoinStyle = CGLineJoin.Round;
    
    color.SetFill();
    polygonPath.Fill();
    color.SetStroke();
    polygonPath.LineWidth = 30.0f;
    polygonPath.Stroke();
    

    enter image description here

    : MidPoint Method

    protected CGPoint MidPoint(CGPoint first, CGPoint second)
    {
        var x = (first.X + second.X) / 2;
        var y = (first.Y + second.Y) / 2;
        return new CGPoint(x: x, y: y);
    }