ios 如何使用 CAShapeLayer 和 UIBezierPath 绘制平滑圆?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/24316705/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-31 00:58:47  来源:igfitidea点击:

How to draw a smooth circle with CAShapeLayer and UIBezierPath?

iosuikitcore-graphicscalayercashapelayer

提问by bcherry

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 screen than using borderRadius or drawing the path in a CGContextRef directly.

我试图通过使用 CAShapeLayer 并在其上设置圆形路径来绘制描边圆。但是,与使用 borderRadius 或直接在 CGContextRef 中绘制路径相比,此方法在渲染到屏幕时始终不准确。

Here are the results of all three methods: enter image description here

以下是所有三种方法的结果: 在此处输入图片说明

Notice that the third is poorly rendered, especially inside the stroke on the top and bottom.

请注意,第三个渲染效果不佳,尤其是在顶部和底部的笔划内部。

I haveset the contentsScaleproperty to [UIScreen mainScreen].scale.

将该contentsScale属性设置为[UIScreen mainScreen].scale.

Here is my drawing code for these three circles. What's missing to make the CAShapeLayer draw smoothly?

这是我为这三个圆圈绘制的代码。使 CAShapeLayer 顺利绘制缺少什么?

@interface BCViewController ()

@end

@interface BCDrawingView : UIView

@end

@implementation BCDrawingView

- (id)initWithFrame:(CGRect)frame
{
    if ((self = [super initWithFrame:frame])) {
        self.backgroundColor = nil;
        self.opaque = YES;
    }

    return self;
}

- (void)drawRect:(CGRect)rect
{
    [super drawRect:rect];

    [[UIColor whiteColor] setFill];
    CGContextFillRect(UIGraphicsGetCurrentContext(), rect);

    CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), NULL);
    [[UIColor redColor] setStroke];
    CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 1);
    [[UIBezierPath bezierPathWithOvalInRect:CGRectInset(self.bounds, 4, 4)] stroke];
}

@end

@interface BCShapeView : UIView

@end

@implementation BCShapeView

+ (Class)layerClass
{
    return [CAShapeLayer class];
}

- (id)initWithFrame:(CGRect)frame
{
    if ((self = [super initWithFrame:frame])) {
        self.backgroundColor = nil;
        CAShapeLayer *layer = (id)self.layer;
        layer.lineWidth = 1;
        layer.fillColor = NULL;
        layer.path = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(self.bounds, 4, 4)].CGPath;
        layer.strokeColor = [UIColor redColor].CGColor;
        layer.contentsScale = [UIScreen mainScreen].scale;
        layer.shouldRasterize = NO;
    }

    return self;
}

@end


@implementation BCViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    UIView *borderView = [[UIView alloc] initWithFrame:CGRectMake(24, 104, 36, 36)];
    borderView.layer.borderColor = [UIColor redColor].CGColor;
    borderView.layer.borderWidth = 1;
    borderView.layer.cornerRadius = 18;
    [self.view addSubview:borderView];

    BCDrawingView *drawingView = [[BCDrawingView alloc] initWithFrame:CGRectMake(20, 40, 44, 44)];
    [self.view addSubview:drawingView];

    BCShapeView *shapeView = [[BCShapeView alloc] initWithFrame:CGRectMake(20, 160, 44, 44)];
    [self.view addSubview:shapeView];

    UILabel *borderLabel = [UILabel new];
    borderLabel.text = @"CALayer borderRadius";
    [borderLabel sizeToFit];
    borderLabel.center = CGPointMake(borderView.center.x + 26 + borderLabel.bounds.size.width/2.0, borderView.center.y);
    [self.view addSubview:borderLabel];

    UILabel *drawingLabel = [UILabel new];
    drawingLabel.text = @"drawRect: UIBezierPath";
    [drawingLabel sizeToFit];
    drawingLabel.center = CGPointMake(drawingView.center.x + 26 + drawingLabel.bounds.size.width/2.0, drawingView.center.y);
    [self.view addSubview:drawingLabel];

    UILabel *shapeLabel = [UILabel new];
    shapeLabel.text = @"CAShapeLayer UIBezierPath";
    [shapeLabel sizeToFit];
    shapeLabel.center = CGPointMake(shapeView.center.x + 26 + shapeLabel.bounds.size.width/2.0, shapeView.center.y);
    [self.view addSubview:shapeLabel];
}


@end

EDIT: For those who cannot see the difference, I've drawn circles on top of each other and zoomed in:

编辑:对于那些看不到差异的人,我在彼此之上绘制了圆圈并放大:

Here I've drawn a red circle with drawRect:, and then drawn an identical circle with drawRect:again in green on top of it. Note the limited bleed of red. Both of these circles are "smooth" (and identical to the cornerRadiusimplementation):

在这里,我用 绘制了一个红色圆圈drawRect:,然后drawRect:在其顶部绘制了一个相同的圆圈,并再次以绿色表示。注意红色的有限流血。这两个圆圈都是“平滑的”(与cornerRadius实现相同):

enter image description here

在此处输入图片说明

In this second example, you'll see the issue. I've drawn once using a CAShapeLayerin red, and again on top with a drawRect:implementation of the same path, but in green. Note that you can see a lot more inconsistency with more bleed from the red circle underneath. It's clearly being drawn in a different (and worse) fashion.

在第二个示例中,您将看到问题。我已经使用CAShapeLayer红色绘制了一次,并再次使用drawRect:相同路径的实现在顶部绘制,但使用绿色。请注意,您可以看到更多的不一致性,下方红色圆圈中的出血更多。它显然是以不同(甚至更糟)的方式绘制的。

enter image description here

在此处输入图片说明

采纳答案by Glen Low

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

谁知道画圆有这么多方法?

TL;DR:If you want to use CAShapeLayerand still get smooth circles, you'll need to use shouldRasterizeand rasterizationScalecarefully.

TL; DR:如果你想使用CAShapeLayer,仍然可以得到流畅的圈子里,你需要使用shouldRasterizerasterizationScale谨慎。

Original

原来的

enter image description here

在此处输入图片说明

Here's your original CAShapeLayerand a diff from the drawRectversion. 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 CAShapeLayerversion has visible differences, especially on the left and right edges (darkest pixels in the diff).

这是您的原始版本CAShapeLayer和与drawRect版本的差异。我用 Retina Display 从我的 iPad Mini 上截取了一个屏幕截图,然后在 Photoshop 中对其进行了按摩,并将其放大到 200%。如您所见,CAShapeLayer版本具有明显的差异,尤其是在左右边缘(差异中最暗的像素)。

Rasterize at screen scale

按屏幕比例光栅化

enter image description here

在此处输入图片说明

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

让我们在屏幕规模上进行光栅化,这应该2.0在视网膜设备上。添加此代码:

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

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

请注意,在视网膜设备上rasterizationScale默认为1.0even,这说明了 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

以 2 倍屏幕比例光栅化

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.0on retina devices.

这会以 2 倍的屏幕比例光栅化路径,或者4.0在视网膜设备上。

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 = NOwhile animating.

我也在 Instruments: Core Animation 中运行了这个,但在 Core Animation Debug Options 中没有看到任何重大差异。然而,它可能会更慢,因为它不仅仅是将屏幕外位图传输到屏幕上进行缩小。您可能还需要shouldRasterize = NO在制作动画时临时设置。

What doesn't work

什么不起作用

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

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

  • shouldRasterize = YES自行设置。在视网膜设备上,这看起来很模糊,因为rasterizationScale != screenScale.

  • 设置contentScale = screenScale由于CAShapeLayer不绘制到contents,无论它是否进行光栅化,这都不会影响再现。

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

感谢 Humaan 的Jay Hollywood ,一位敏锐的平面设计师,他首先向我指出了这一点。

回答by 32Beat

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:

啊,我前段时间遇到了同样的问题(当时还是iOS 5然后是iirc),我在代码中写了以下注释:

/*
    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.

圆形下方的实心圆形会使其填充色流血。根据颜色的不同,这将非常明显。在用户交互期间,形状会渲染得更糟,这让我得出结论,无论图层比例因子如何,shapelayer 总是以 1.0 的比例因子进行渲染,因为它用于动画目的。

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

即,如果您对贝塞尔路径的形状有特定的动画更改需要,则仅使用CAShapeLayer,而不是通过通常的图层属性设置动画的任何其他属性。

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:

我最终决定编写一个简单的 ShapeLayer 来缓存它自己的结果,但你可以尝试实现 displayLayer: 或 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...

我还没有尝试过,但知道结果会很有趣......

回答by JiuJitsuCoder

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:

我知道这是一个较旧的问题,但是对于那些尝试使用 drawRect 方法但仍然遇到问题的人来说,一个对我有很大帮助的小调整是使用正确的方法来获取 UIGraphicsContext。使用默认值:

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:

无论我从其他答案中遵循哪个建议,都会导致模糊的圆圈。最终为我做的是意识到获取 ImageContext 的默认方法将缩放设置为非视网膜。要获取视网膜显示的 ImageContext,您需要使用此方法:

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 0will tell the system to use the scaling factor of the device's main screen. The middle option falseis used to tell the graphics context whether or not you'll be drawing an opaque image (truemeans 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将告诉系统使用设备主屏幕的缩放因子。中间选项false用于告诉图形上下文您是要绘制不透明图像(true意味着图像将是不透明的)还是需要包含透明度的 Alpha 通道的图像。以下是有关更多上下文的相应 Apple 文档:https: //developer.apple.com/reference/uikit/1623912-uigraphicsbeginimagecontextwitho?language=objc

回答by hfossli

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

我想CAShapeLayer是由一种更高效的渲染形状的方式支持的,并采取了一些捷径。无论如何CAShapeLayer,在主线程上可能会有点慢。除非您需要在不同路径之间设置动画,否则我建议异步渲染到UIImage后台线程上的 a。

回答by Sandeep Singh

Use this method to draw UIBezierPath

使用此方法绘制 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];