如何用CAShapeLayer和UIBezierPath绘制一个平滑的圆?
我试图通过使用CAShapeLayer并在其上设置循环path来绘制一个描边圈。 但是,这种方法在渲染到屏幕上的准确性比使用borderRadius时要less得多,或者直接在CGContextRef中绘制path。
以下是所有三种方法的结果:
请注意,第三个渲染效果不好,特别是在顶部和底部的中风。
我已经将contentsScale
属性设置为[UIScreen mainScreen].scale
。
这是我的这三个圈子的绘图代码。 缺less什么使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:
绘制了一个相同的圆圈drawRect:
再次用绿色表示它。 注意红色的有限stream血。 这两个圆都是“光滑的”(和cornerRadius
实现相同):
在第二个例子中,你会看到这个问题。 我用红色绘制了一个CAShapeLayer
,再次用drawRect:
实现了相同的path,但是采用绿色。 请注意,您可以看到更多的不一致,更多的stream血从下面的红色圆圈。 这显然是以不同的方式画出来的。
谁知道有这么多的方法来画一个圈子?
TL; DR:如果您想使用
CAShapeLayer
并且仍然可以获得平滑的圆圈,则需要仔细使用shouldRasterize
和rasterizationScale
。
原版的
这是您的原始CAShapeLayer
和drawRect
版本的差异。 我使用Retina Display在iPad Mini上截取了一张屏幕截图,然后在Photoshop中对其进行了按摩,并将其吹到了200%。 正如您可以清楚地看到的那样, CAShapeLayer
版本具有明显的差异,特别是在左侧和右侧边缘(diff中最暗的像素)。
在屏幕上进行栅格化
让我们在屏幕上进行光栅化处理,在视网膜设备上应该是2.0
。 添加以下代码:
layer.rasterizationScale = [UIScreen mainScreen].scale; layer.shouldRasterize = YES;
请注意,即使在视网膜设备上, rasterizationScale
默认值也是1.0
,这说明了默认值shouldRasterize
的模糊性。
圆现在有点平滑了,但坏点(diff中最暗的像素)已经移动到顶部和底部边缘。 没有明显好于没有栅格化!
以2倍的屏幕比例栅格化
layer.rasterizationScale = 2.0 * [UIScreen mainScreen].scale; layer.shouldRasterize = YES;
这可以在2倍的屏幕范围内光栅化path,或在视网膜设备上高达4.0
。
圆现在明显更平滑,差异更轻,分散均匀。
我也运行在仪器:核心animation这一点,没有看到核心animationdebugging选项的任何重大差异。 然而,它可能会比较慢,因为它的缩小不仅仅是将屏幕外的位图传送到屏幕上。 animation时,您可能还需要临时设置shouldRasterize = NO
。
什么都行不通
-
设置
shouldRasterize = YES
本身。 在视网膜设备上,这看起来很模糊,因为rasterizationScale != screenScale
。 -
设置
contentScale = screenScale
。 由于CAShapeLayer
没有绘制contents
,不pipe是否光栅化,这都不影响绘制。
感谢Humaan的杰伊•好莱坞(Jay Hollywood), 他是一位锐利的平面devise师,他首先向我指出了这一点。
嗯,我前段时间遇到同样的问题(它仍然是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. */
圆形下方的实心圆会泄漏其fillcolor。 根据颜色,这将是非常明显的。 在用户交互过程中,形状会变得更加糟糕,这让我得出结论:shapelayer将总是以1.0的比例因子进行渲染,而不pipe图层比例因子如何,因为这是为了animation的目的。
即,如果您对bezierpath 的形状进行animation更改的具体需求,而不是通过常用图层属性进行animation处理的任何其他属性,则只能使用CAShapeLayer。
我最终决定写一个简单的ShapeLayer来caching自己的结果,但是你可以尝试实现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; }
我没有尝试过,但知道结果会很有趣…
我知道这是一个较老的问题,但对于那些正在尝试使用drawRect方法并仍然有困难的人来说,一个很大的帮助就是使用正确的方法来获取UIGraphicsContext。 使用默认值:
let newSize = CGSize(width: 50, height: 50) UIGraphicsBeginImageContext(newSize) let context = UIGraphicsGetCurrentContext()
会导致模糊的圈子,无论我从其他答案跟随哪个build议。 最后我做的是意识到获取ImageContext的默认方法将缩放设置为非视网膜。 要获得视网膜显示的ImageContext,您需要使用此方法:
let newSize = CGSize(width: 50, height: 50) UIGraphicsBeginImageContextWithOptions(newSize, false, 0) let context = UIGraphicsGetCurrentContext()
从那里使用正常的绘图方法工作正常。 将最后一个选项设置为0
将告诉系统使用设备主屏幕的比例因子。 中间选项false
用于告诉graphics上下文是否要绘制不透明的图像( true
表示图像将是不透明的),或者需要包含透明胶片的Alpha通道。 以下是适合更多上下文的Apple Docs: https : //developer.apple.com/reference/uikit/1623912-uigraphicsbeginimagecontextwitho?language=objc
我想CAShapeLayer
是由一个更高性能的方式来渲染其形状,并采取一些捷径。 无论如何, CAShapeLayer
在主线程上可能有点慢。 除非你需要在不同的path之间animation,否则我会build议asynchronous渲染到后台线程上的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];