How to draw a smooth circle with CAShapeLayer and UIBezierPath?

前端 未结 5 1490
闹比i
闹比i 2020-12-07 07:14

I am attempting to draw a stroked circle by using a CAShapeLayer and setting a circular path on it. However, this method is consistently less accurate when rendered to the

相关标签:
5条回答
  • 2020-12-07 07:53

    I know this is an older question, but for those who are trying to work in the drawRect method and still having trouble, one small tweak that helped me immensely was using the correct method to fetch the UIGraphicsContext. Using the default:

    let newSize = CGSize(width: 50, height: 50)
    UIGraphicsBeginImageContext(newSize)
    let context = UIGraphicsGetCurrentContext()
    

    would result in blurry circles no matter which suggestion I followed from the other answers. What finally did it for me was realizing that the default method for getting an ImageContext sets the scaling to non-retina. To get an ImageContext for a retina display you need to use this method:

    let newSize = CGSize(width: 50, height: 50)
    UIGraphicsBeginImageContextWithOptions(newSize, false, 0)
    let context = UIGraphicsGetCurrentContext()
    

    from there using the normal drawing methods worked fine. Setting the last option to 0 will tell the system to use the scaling factor of the device’s main screen. The middle option false is used to tell the graphics context whether or not you'll be drawing an opaque image (true means the image will be opaque) or one that needs an alpha channel included for transparencies. Here are the appropriate Apple Docs for more context: https://developer.apple.com/reference/uikit/1623912-uigraphicsbeginimagecontextwitho?language=objc

    0 讨论(0)
  • 2020-12-07 08:01

    Who knew there are so many ways to draw a circle?

    TL;DR: If you want to use CAShapeLayer and still get smooth circles, you'll need to use shouldRasterize and rasterizationScale carefully.

    Original

    enter image description here

    Here's your original CAShapeLayer and a diff from the drawRect version. I made a screenshot off my iPad Mini with Retina Display, then massaged it in Photoshop, and blew it up to 200%. As you can clearly see, the CAShapeLayer version has visible differences, especially on the left and right edges (darkest pixels in the diff).

    Rasterize at screen scale

    enter image description here

    Let's rasterize at screen scale, which should be 2.0 on retina devices. Add this code:

    layer.rasterizationScale = [UIScreen mainScreen].scale;
    layer.shouldRasterize = YES;
    

    Note that rasterizationScale defaults to 1.0 even on retina devices, which accounts for the fuzziness of default shouldRasterize.

    The circle is now a little smoother, but the bad bits (darkest pixels in the diff) have moved to the top and bottom edges. Not appreciably better than no rasterizing!

    Rasterize at 2x screen scale

    enter image description here

    layer.rasterizationScale = 2.0 * [UIScreen mainScreen].scale;
    layer.shouldRasterize = YES;
    

    This rasterizes the path at 2x screen scale, or up to 4.0 on retina devices.

    The circle is now visibly smoother, the diffs are much lighter and spread out evenly.

    I also ran this in Instruments: Core Animation and didn't see any major differences in the Core Animation Debug Options. However it may be slower since it's downscaling not just blitting an offscreen bitmap to the screen. You may also need to temporarily set shouldRasterize = NO while animating.

    What doesn't work

    • Set shouldRasterize = YES by itself. On retina devices, this looks fuzzy because rasterizationScale != screenScale.

    • Set contentScale = screenScale. Since CAShapeLayer doesn't draw into contents, whether or not it is rasterizing, this doesn't affect the rendition.

    Credit to Jay Hollywood of Humaan, a sharp graphic designer who first pointed it out to me.

    0 讨论(0)
  • 2020-12-07 08:06

    I guess CAShapeLayer is backed by a more performant way of rendering its shapes and takes some shortcuts. Anyway CAShapeLayer can be a little bit slow on the main thread. Unless you need to animate between different paths I would suggest render asynchronously to a UIImage on a background thread.

    0 讨论(0)
  • 2020-12-07 08:06

    Use this method to draw UIBezierPath

    /*********draw circle where double tapped******/
    - (UIBezierPath *)makeCircleAtLocation:(CGPoint)location radius:(CGFloat)radius
    {
        self.circleCenter = location;
        self.circleRadius = radius;
    
        UIBezierPath *path = [UIBezierPath bezierPath];
        [path addArcWithCenter:self.circleCenter
                        radius:self.circleRadius
                    startAngle:0.0
                      endAngle:M_PI * 2.0
                     clockwise:YES];
    
        return path;
    }
    

    And draw like this

    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
        shapeLayer.path = [[self makeCircleAtLocation:location radius:50.0] CGPath];
        shapeLayer.strokeColor = [[UIColor whiteColor] CGColor];
        shapeLayer.fillColor = nil;
        shapeLayer.lineWidth = 3.0;
    
        // Add CAShapeLayer to our view
    
        [gesture.view.layer addSublayer:shapeLayer];
    
    0 讨论(0)
  • 2020-12-07 08:08

    Ah, i ran into the same problem some time ago (it was still iOS 5 then iirc), and I wrote the following comment in the code:

    /*
        ShapeLayer
        ----------
        Fixed equivalent of CAShapeLayer. 
        CAShapeLayer is meant for animatable bezierpath 
        and also doesn't cache properly for retina display. 
        ShapeLayer converts its path into a pixelimage, 
        honoring any displayscaling required for retina. 
    */
    

    A filled circle underneath a circleshape would bleed its fillcolor. Depending on the colors this would be very noticeable. And during userinteraction the shape would render even worse, which let me to conclude that the shapelayer would always render with a scalefactor of 1.0, regardless of the layer scalefactor, because it is meant for animation purposes.

    i.e. you only use a CAShapeLayer if you have a specific need for animatable changes to the shape of the bezierpath, not to any of the other properties that are animatable through the usual layer properties.

    I eventually decided to write a simple ShapeLayer that would cache its own result, but you might try implementing the displayLayer: or the drawLayer:inContext:

    Something like:

    - (void)displayLayer:(CALayer *)layer
    {
        UIImage *image = nil;
    
        CGContextRef context = UIImageContextBegin(layer.bounds.size, NO, 0.0);
        if (context != nil)
        {
            [layer renderInContext:context];
            image = UIImageContextEnd();
        }
    
        layer.contents = image;
    }
    

    I haven't tried that, but would be interesting to know the result...

    0 讨论(0)
提交回复
热议问题