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

我试图绘制一个画圆通过使用 CAShapeLayer 和设置一个圆形路径上。但是,与使用 borderRadius 或直接在 CGContextRef 中绘制路径相比,此方法在呈现到屏幕时始终不太准确。

以下是这三种方法的结果:enter image description here

请注意,第三个渲染得很差,特别是在顶部和底部的笔划内部。

I 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实现相同) :

enter image description here

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

enter image description here

38477 次浏览

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

圆形下面的一个填充圆会流出它的填充色。根据颜色的不同,这将是非常明显的。而且在用户交互过程中,形状的渲染效果会更差,这让我得出结论: 形状图层总是使用1.0的比例因子来渲染,而不管层的比例因子是什么,因为它是用于动画目的的。

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

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

我还没试过,不过想知道结果会很有趣。

我猜想 CAShapeLayer是由一个更高效的方式来渲染它的形状,并采取一些捷径。无论如何,CAShapeLayer在主线程上可能有点慢。除非你需要动画之间的不同路径,我会建议异步渲染到一个背景线程的 UIImage

使用此方法绘制 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并且仍然得到光滑的圆圈,你需要仔细地使用 shouldRasterizerasterizationScale

原创的

enter image description here

这是您的原始 CAShapeLayer和一个不同的 drawRect版本。我用 Retina Display 为我的 iPad Mini 做了一个屏幕截图,然后在 Photoshop 中进行了按摩,并将其放大到200% 。正如您可以清楚地看到,CAShapeLayer版本有可见的差异,特别是在左边缘和右边缘(最暗的像素在差异)。

在屏幕尺寸上栅格化

enter image description here

让我们在屏幕尺度上进行光栅化,它应该是视网膜设备上的 2.0。添加以下代码:

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

请注意,即使在视网膜设备上,rasterizationScale也默认为 1.0,这解释了默认 shouldRasterize的模糊性。

圆圈现在变得平滑了一些,但是坏的部分(差值中最暗的像素)已经移动到了顶部和底部的边缘。没有明显好过没有光栅!

栅格化在2倍屏幕规模

enter image description here

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

这栅格化的路径在2倍屏幕规模,或高达 4.0的视网膜设备。

圆圈现在看起来更光滑了,差异更小了,而且分布也更均匀了。

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

什么行不通

  • 在视网膜设备上,这看起来很模糊,因为 rasterizationScale != screenScale

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

这要归功于美国广播公司(人类)的杰伊•好莱坞(Jay Hollywood) ,他是一位尖锐的平面设计师,最先向我指出了这一点。

我知道这是一个老问题了,但是对于那些试图使用 draRect 方法但仍然遇到麻烦的人来说,一个小小的调整对我非常有帮助,那就是使用正确的方法来获取 UIGraphicsContext。使用默认值:

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

不管我从其他答案中采纳了哪个建议,都会导致模糊的圈子。最后,我意识到获取 ImageContext 的默认方法将缩放设置为 non-retina。要获得视网膜显示器的 ImageContext,你需要使用以下方法:

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

从那里使用正常的绘图方法工作良好。将最后一个选项设置为 0将告诉系统使用设备主屏幕的缩放因子。中间的选项 false用于告诉图形上下文是否您将绘制一个不透明的图像(true意味着图像将是不透明的)或者一个需要包含 alpha 通道的透明图像。以下是适当的苹果文档以了解更多上下文: https://developer.apple.com/reference/uikit/1623912-uigraphicsbeginimagecontextwitho?language=objc