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

     2023-02-15     40

关键词:

【中文标题】如何用 CAShapeLayer 和 UIBezierPath 画一个光滑的圆?【英文标题】:How to draw a smooth circle with CAShapeLayer and UIBezierPath? 【发布时间】:2014-08-10 14:06:43 【问题描述】:

我正在尝试通过使用 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: 实现,但使用绿色。请注意,您可以看到更多的不一致,下面的红色圆圈有更多的出血。它显然是以不同(更糟)的方式绘制的。

【问题讨论】:

不确定它是否与您的图像捕获有关,但上面的三个圆圈 - 至少在我看来 - 完全一样。 在这一行中:[UIBezierPath bezierPathWithOvalInRect:CGRectInset(self.bounds, 4, 4)] self.bound 保存点(不是像素),因此从技术上讲,您正在创建一个非视网膜大小的曲线。如果将该尺寸乘以 [UIScreen mainScreen].scale 值,您的椭圆在视网膜屏幕上将非常平滑。 @MaxMacLeod 检查我的编辑是否有差异。 @holex 这只是一个更小的圆圈。我的两个实现都使用相同的路径,一个看起来不错,另一个不好看。 在文档中,它说光栅化有利于速度而不是质量。可能是您看到了不精确插值/抗锯齿的伪影。 【参考方案1】:

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

TL;DR:如果您想使用CAShapeLayer 并且仍然获得平滑的圆圈,则需要谨慎使用shouldRasterizerasterizationScale

原创

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

按屏幕比例光栅化

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

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

请注意,即使在 Retina 设备上,rasterizationScale 也会默认为 1.0,这导致了默认 shouldRasterize 的模糊性。

圆现在稍微平滑了一点,但是坏位(diff 中最暗的像素)已经移动到顶部和底部边缘。不比不光栅化好多少!

以 2 倍屏幕比例光栅化

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

这会将路径光栅化为 2 倍屏幕比例,或在 Retina 设备上高达 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 的潜在问题。 我很确定 CAShapeLayer 已针对快速绘图和动画进行了优化。但是它还有很多其他优点,我更喜欢使用它而不是 drawRect:分辨率独立(等到 Apple 生产 4 倍视网膜显示器)、更少的内存、可变形而不失真、灵活的光栅化等。 回复:灵活的光栅化。您可以临时确定是否要栅格化,甚至可以在什么级别进行栅格化,例如在由多个CAShapeLayer 组成的层中。 我怀疑 Apple 对CAShapeLayer 再现使用了高平坦度。您有时可以在非视网膜显示器中看到这一点:再现看起来像一个多边形。那么解决办法就是自己做扁平化。参见例如ps.missouri.edu/ps2/support/tutorialfolder/flatness/index.html【参考方案2】:

啊,我前段时间遇到了同样的问题(当时还是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 的比例因子渲染,因为它是用于动画目的。

即仅当您对贝塞尔路径的形状的动画更改有特定需求时才使用 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 的以动画为中心的绘图优化是正确的。我将继续为我的非动画图层使用自定义绘图。【参考方案3】:

我知道这是一个较老的问题,但是对于那些尝试使用 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 Docs:https://developer.apple.com/reference/uikit/1623912-uigraphicsbeginimagecontextwitho?language=objc

【讨论】:

【参考方案4】:

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

【讨论】:

【参考方案5】:

使用此方法绘制 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 绘制的路径边缘的像素化问题。

IOS自定义进度条与CAShapeLayer

】IOS自定义进度条与CAShapeLayer【英文标题】:IOScustomprogressbarwithCAShapeLayer【发布时间】:2021-04-1411:19:22【问题描述】:如何用弧尾的文字创建像这张图片一样的自定义弓这是我当前的代码和当前结果letcenter=view.centerletcircularPath=UI... 查看详情

CAShapeLayer 和 SpriteKit

】CAShapeLayer和SpriteKit【英文标题】:CAShapeLayerandSpriteKit【发布时间】:2017-12-0306:54:38【问题描述】:我正在尝试利用CAShapeLayer中的路径动画功能,该功能在SpriteKit中不可用,因此必须将使用CAShapeLayer绘制的对象与同一视图中的Spr... 查看详情

[[CAShapeLayer alloc] init] 和 [CAShapeLayer layer] 的区别

】[[CAShapeLayeralloc]init]和[CAShapeLayerlayer]的区别【英文标题】:DifferenceBetween[[CAShapeLayeralloc]init]and[CAShapeLayerlayer]【发布时间】:2012-11-1014:54:30【问题描述】:我注意到大多数人在初始化CAShapeLayer时使用:CAShapeLayer*shapeLayer=[CAShapeLaye... 查看详情

“”和“”有啥区别,我如何用字符来测试前者?

】“”和“”有啥区别,我如何用字符来测试前者?【英文标题】:Whatisthedifferencebetween""and"",andhowdoItesttheformerintermsofachar?“”和“”有什么区别,我如何用字符来测试前者?【发布时间】:2012-08-1702:31:59【问题... 查看详情

如何用键盘和活动触发按钮

】如何用键盘和活动触发按钮【英文标题】:Howtotriggerbuttonwithkeyboardandactive【发布时间】:2021-12-1505:34:53【问题描述】:我想用键盘和鼠标触发一个按钮。它可以工作,但是如果我用键盘触发它,则:active不会显示。HTML<buttonid=... 查看详情

如何用组和匹配编写管道?

】如何用组和匹配编写管道?【英文标题】:Howtowritepipelinewithgroupalongwithmatch?【发布时间】:2019-08-2209:59:55【问题描述】:我使用group和match编写解析聚合管道,但它不起作用它在mongocompass中工作,但在解析服务器中被拒绝,请... 查看详情

如何用玩笑测试 Vue 3 和惯性

】如何用玩笑测试Vue3和惯性【英文标题】:HowtotestVue3andintertiawithjest【发布时间】:2021-12-2310:15:31【问题描述】:在使用LaravelMix设置的Laravel+Vue3+Inertia项目中,我们如何创建前端测试?特别是,我不知道如何处理Inertia的ShareData... 查看详情

如何用 Javascript 替换和追加

】如何用Javascript替换和追加【英文标题】:HowtoreplaceandappendwithJavascript【发布时间】:2012-11-2004:06:01【问题描述】:我有一个评论系统,我想在其中实现内联编辑(当有人知道一个好的插件或类似的东西时,请不要犹豫给我一个... 查看详情

cashapelayer和贝塞尔曲线配合使用

前言CAShapeLayer继承自CALayer,因此,可使用CALayer的所有属性。但是,CAShapeLayer需要和贝塞尔曲线配合使用才有意义。关于UIBezierPath,请阅读文章:iOSUIBezierPth精讲基本知识看看官方说明: 123456789 /*TheshapelayerdrawsacubicBezierspl... 查看详情

如何用边框(阴影)包装 UICollectionView 部分和项目?

】如何用边框(阴影)包装UICollectionView部分和项目?【英文标题】:HowtowrapUICollectionViewsectionanditemswithaborder(shadow)?【发布时间】:2018-01-1910:06:25【问题描述】:我想在我的uicollectionview的每对部分和项目周围制作边框/阴影。此外... 查看详情

如何用css和div写网页

先设计好框架,也就是容器,然后往里面塞东西,css是由外向里写,也就是先写外面大容器的css,在写内部小容器的css结构如下图:     css如下:   查看详情

如何用Python提取和识别车牌号?

】如何用Python提取和识别车牌号?【英文标题】:HowtoextractandrecognizethevehicleplatenumberwithPython?【发布时间】:2019-06-2211:10:03【问题描述】:我曾尝试使用pytesseract与PIL合作从车牌图像中识别车辆登记号。但我无法从这些图像中获... 查看详情

如何用 babel 和 node 14 构建?

】如何用babel和node14构建?【英文标题】:Howtobuildwithbabelandnode14?【发布时间】:2021-04-1920:49:26【问题描述】:我正在尝试使用babel和目标节点14.15.4构建我的项目我的.babelrc是这样的"presets":[["@babel/preset-env","targets":"node":true]]所以... 查看详情

如何用jQuery选择和替换整个页面

】如何用jQuery选择和替换整个页面【英文标题】:HowtoselectandreplacethewholepagewithjQuery【发布时间】:2010-10-2201:28:51【问题描述】:我的页面设计迫使我使用通过ajax加载的html刷新整个页面。$(\'html\').replaceWith(data);给我错误。有什么... 查看详情

如何用 AND 和 NOT 进行逻辑 OR?

】如何用AND和NOT进行逻辑OR?【英文标题】:HowtomakelogicalORwithAND,andNOT?【发布时间】:2012-01-1213:52:52【问题描述】:如何用逻辑AND和逻辑NOT创建逻辑OR?【问题讨论】:这是你的作业吗?是的,需要综合实体逻辑的控制通道,使... 查看详情

如何用rowspan和colspan解析表

】如何用rowspan和colspan解析表【英文标题】:Howtoparsetablewithrowspanandcolspan【发布时间】:2018-07-0118:33:18【问题描述】:首先,我阅读了Parsingatablewithrowspanandcolspan。我什至回答了这个问题。请在将其标记为重复之前阅读。<tablebor... 查看详情

如何用最小值和最大值拖动图像?

】如何用最小值和最大值拖动图像?【英文标题】:howdraggedimagewithaminandmaxvalue?【发布时间】:2015-07-1319:01:07【问题描述】:我有这个代码来增加图像的高度。-(void)touchesBegan:(NSSet*)toucheswithEvent:(UIEvent*)eventUITouch*touch=[[eventallTouches... 查看详情

如何用 jQuery 查找和替换类名的前缀?

】如何用jQuery查找和替换类名的前缀?【英文标题】:HowtofindandreplacetheprefixofaclassnamewithjQuery?【发布时间】:2020-07-1622:24:52【问题描述】:我有一堆类名,例如md:block和sm:flex我需要:查找所有以“md”或“sm”开头的类。在每个... 查看详情