如何使用CAShapeLayer和UIBezierPath绘制一个平滑的圆?


78

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

这是这三种方法的结果: 在此处输入图片说明

请注意,第三个渲染效果很差,尤其是在顶部和底部的笔触内部。

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

这是我为这三个圆圈绘制的代码。使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

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

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

在此处输入图片说明

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

在此处输入图片说明


1
不确定是否与您的图像捕获有关,但是上面的那三个圆圈-至少在我看来-完全相同。
Max MacLeod 2014年

在这条线:[UIBezierPath bezierPathWithOvalInRect:CGRectInset(self.bounds, 4, 4)]self.bound保持点(而不是像素),所以在技术上要创建一个非视网膜大小的曲线。如果将该尺寸乘以该[UIScreen mainScreen].scale值,则椭圆形将在视网膜屏幕上完全平滑。
Holex 2014年

@MaxMacLeod检查我的编辑是否有区别。
bcherry

@holex只会使圆圈变小。我的两个实现都使用相同的路径,一个看起来不错,而另一个则没有。
樱桃

在文档中,它说光栅化有利于速度而不是质量。可能是您看到的是不精确的插值/抗锯齿效果。
Leo Natan 2014年

Answers:


70

谁知道有这么多种绘制圆圈的方法?

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

原版的

在此处输入图片说明

这是您的原始版本,CAShapeLayer与该drawRect版本有所不同。我用配备Retina显示屏的iPad Mini截取了屏幕截图,然后在Photoshop中对其进行了按摩,然后将其放大至200%。您可以清楚地看到,该CAShapeLayer版本具有明显的差异,尤其是在左右边缘(差异中最暗的像素)上。

以屏幕比例栅格化

在此处输入图片说明

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

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

请注意,即使是在视网膜设备上,rasterizationScale默认值1.0也是如此,这说明了default的模糊性shouldRasterize

现在,圆变得更平滑了,但是不良位(差异中最暗的像素)已移至顶部和底部边缘。没有比没有栅格化要好得多!

以2倍的屏幕比例光栅化

在此处输入图片说明

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

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

圆现在明显更平滑,差异更轻并且均匀分布。

我也在《 Instruments:Core Animation》中运行了此程序,并且在Core Animation Debug Options中没有看到任何主要差异。但是,它可能会变慢,因为它的缩放比例不只是将屏幕外的位图拖到屏幕上。设置shouldRasterize = NO动画时,您可能还需要临时设置。

什么不起作用

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

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

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


谢谢你的回答!在某些情况下,这显然可以提供一种不错的解决方法。您是否知道@ 32Beat是正确的,由于动画的优化,CAShapeLayer会渲染低保真度的副本?好像直接绘图设法以2倍的比例绘制正确的东西,因此增大比例可以解决该问题,但最终不能解决CAShapeLayer的根本问题。
bcherry 2014年

2
我很确定CAShapeLayer为快速绘制和动画进行了优化。但是它仍然有这么等优势,我更喜欢用它了drawRect:分辨率独立(只是等待,直到苹果公司产生了4倍视网膜显示),内存少,变形不变形,有弹性光栅化等
格伦低

回复:灵活的栅格化。您可以临时确定是否进行栅格化,甚至确定在什么级别上进行栅格化,例如在由多个图层组成的图层中CAShapeLayer
Glen Low

我怀疑苹果使用高平整度进行CAShapeLayer演绎。有时,您可以在非视网膜显示中看到这一点:再现看起来像一个多边形。解决的办法是自己弄平。参见例如ps.missouri.edu/ps2/support/tutorialfolder/flatness/index.html
Glen Low,

9

啊,前段时间我遇到了同样的问题(当时还是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. 
*/

圆形下面的实心圆会渗出其填充颜色。根据颜色,这将非常明显。并且在用户交互期间,形状将呈现更差的效果,这让我得出结论,无论图层缩放因子如何,shapelayer始终将使用1.0的缩放因子进行渲染,因为它是出于动画目的。

也就是说,仅当您有特定需要对bezierpath的形状进行动画更改,而不是通过常规图层属性可以动画化的其他任何属性时,才使用CAShapeLayer 。

我最终决定编写一个简单的ShapeLayer来缓存其自身的结果,但是您可以尝试实现displayLayer:或drawLayer:inContext:

就像是:

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

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


这很有道理。我注意到,将contentsScale设置为什么都没有关系,看起来还是一样。我尝试了您的displayLayer实现。不幸的是,这并没有改变。我怀疑您对CAShapeLayer具有针对动画的图形优化是正确的。我将继续对非动画图层使用自定义绘图。
樱桃

4

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

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

无论我从其他答案中得出哪个建议,都会导致圈子模糊。最终对我来说,是意识到获取ImageContext的默认方法将缩放比例设置为非视网膜。要获取用于视网膜显示的ImageContext,您需要使用以下方法:

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

从那里使用正常的绘制方法效果很好。将最后一个选项设置为0将告诉系统使用设备主屏幕的缩放比例。中间选项false用于告诉图形上下文是要绘制不透明图像(true意味着图像将是不透明的)还是需要透明胶片包含alpha通道的图像。以下是适当的Apple文档以获取更多上下文:https : //developer.apple.com/reference/uikit/1623912-uigraphicsbeginimagecontextwitho?language=objc


1

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


-1

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

像这样画

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

我尝试过这种制作圆以及使用圆角矩形路径的方法。两者都不能解决沿CAShapeLayer绘制的路径边缘进行像素化的问题。
樱桃
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.