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

bch*_*rry 74 core-graphics calayer uikit cashapelayer ios

我试图通过使用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
Run Code Online (Sandbox Code Playgroud)

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

在这里,我绘制了一个红色圆圈drawRect:,然后drawRect:在它上面再次以绿色绘制了一个相同的圆圈.请注意红色的有限流血.这两个圈子都是"平滑的"(和cornerRadius实现相同):

在此输入图像描述

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

在此输入图像描述

Gle*_*Low 68

谁知道有很多方法画圆圈?

TL; DR:如果你想使用CAShapeLayer,仍然可以得到流畅的圈子里,你将需要使用shouldRasterizerasterizationScale认真.

原版的

在此输入图像描述

这是你的原版CAShapeLayerdrawRect版本的差异.我使用Retina Display在我的iPad Mini上制作了一个屏幕截图,然后在Photoshop中对其进行了按摩,并将其吹得高达200%.你可以清楚地看到,CAShapeLayer版本有明显的差异,特别是在左右边缘(差异中最暗的像素).

在屏幕范围内进行栅格化

在此输入图像描述

让我们在屏幕尺度上进行栅格化,这应该2.0在视网膜设备上.添加此代码:

layer.rasterizationScale = [UIScreen mainScreen].scale;
layer.shouldRasterize = YES;
Run Code Online (Sandbox Code Playgroud)

请注意,甚至视网膜设备上的rasterizationScale默认设置1.0也会导致默认模糊shouldRasterize.

圆现在有点平滑,但坏位(差异中最暗的像素)已经移动到顶部和底部边缘.没有比没有光栅化好多了!

以2倍的屏幕比例进行栅格化

在此输入图像描述

layer.rasterizationScale = 2.0 * [UIScreen mainScreen].scale;
layer.shouldRasterize = YES;
Run Code Online (Sandbox Code Playgroud)

这样可以在2倍屏幕范围内光栅化路径,或者4.0在视网膜设备上光栅化.

圆形现在明显更平滑,差异更轻,均匀分布.

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

什么行不通

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

  • 设置contentScale = screenScale.既然CAShapeLayer没有画入contents,无论是否是光栅化,这都不会影响演绎.

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

  • 我很确定`CAShapeLayer`针对快速绘图和动画进行了优化.然而它仍然具有其它优点,我更喜欢使用它而不是'drawRect`:分辨率独立(只等到Apple产生4倍视网膜显示),更少的内存,可变形而不失真,灵活的光栅化等. (2认同)

小智 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. 
*/
Run Code Online (Sandbox Code Playgroud)

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

即,如果您特别需要对bezier路径的形状进行动画更改,而不是通过常用图层属性可设置的任何其他属性,则只使用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;
}
Run Code Online (Sandbox Code Playgroud)

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