Как нарисовать гладкий круг с CAShapeLayer и UIBezierPath?
Я пытаюсь нарисовать гладкий круг с помощью 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
EDIT: для тех, кто не видит разницы, я нарисовал круги друг над другом и увеличил масштаб:
здесь я нарисовал красный круг с drawRect:, а затем нарисовал идентичный круг с drawRect: снова в зеленом поверх него. Обратите внимание на ограниченное кровотечение красного цвета. Оба эти круга являются "гладкими" (и идентично cornerRadius реализация):

в этом втором примере вы увидите проблему. Я нарисовал один раз с помощью CAShapeLayer в красном, и снова сверху с drawRect: реализация того же пути, но в зеленом цвете. Обратите внимание, что вы можете увидеть гораздо больше несоответствий с кровью из под красного круга. Это явно рисуется по-другому (и хуже).

5 ответов:
кто знал, что есть так много способов, чтобы нарисовать круг?
TL; DR: если вы хотите использовать
CAShapeLayerи все равно получите гладкие круги, вам нужно будет использоватьshouldRasterizeиrasterizationScaleтщательно.Оригинал
вот твой оригинал
CAShapeLayerи отличия отdrawRectверсия. Я сделал скриншот с моего iPad Mini с дисплеем Retina, затем помассировал его в Photoshop и взорвал его до 200%. Как вы можете ясно видеть,CAShapeLayerверсия имеет видимые различия, особенно на левом и правом краях (самые темные пиксели в diff).растеризация в масштабе экрана
давайте растеризуем в масштабе экрана, который должен быть
2.0на устройствах retina. Добавьте этот код:layer.rasterizationScale = [UIScreen mainScreen].scale; layer.shouldRasterize = YES;отметим, что
rasterizationScaleпо умолчанию1.0даже на устройствах retina, что объясняет нечеткость по умолчаниюshouldRasterize.круг Теперь немного более гладкий, но плохие биты (самые темные пиксели в diff) переместились к верхнему и нижнему краям. Не заметно лучше, чем без растеризации!
растеризация в масштабе экрана 2x
layer.rasterizationScale = 2.0 * [UIScreen mainScreen].scale; layer.shouldRasterize = YES;это растеризует путь в масштабе экрана 2x, или до
4.0на устройствах retina.круг теперь заметно более гладкий, различия намного легче и распространяются равномерно.
Я также запустил это в Instruments: Core Animation и не увидел никаких существенных различий в параметрах отладки Core Animation. Однако это может быть медленнее, так как это масштабирование не просто размывает закадровое растровое изображение на экране. Возможно, Вам также потребуется временно установить
shouldRasterize = NOво время анимации.не работает
Set
shouldRasterize = YESсам по себе. на устройствах retina это выглядит нечетко, потому чтоrasterizationScale != screenScale.Set
contentScale = screenScale. СCAShapeLayerне втягивается вcontents, независимо от того, является ли это растеризацией, это не влияет на исполнение.кредит Джей Голливуд из Humaan, острый графический дизайнер, который впервые указал мне на это.
Ах, я столкнулся с той же проблемой некоторое время назад (это было еще 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 всегда будет отображаться со scalefactor 1.0, независимо от scalefactor слоя, потому что это означает для целей анимации.
т. е. вы используете 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; }Я не пробовал, но было бы интересно узнать результат...
Я знаю, что это более старый вопрос, но для тех, кто пытается работать в методе drawRect и все еще испытывает проблемы, одна небольшая настройка, которая очень помогла мне, использовала правильный метод для извлечения UIGraphicsContext. Использование по умолчанию:
let newSize = CGSize(width: 50, height: 50) UIGraphicsBeginImageContext(newSize) let context = UIGraphicsGetCurrentContext()приведет к размытым кругам независимо от того, какое предложение я следовал из других ответов. Что, наконец, сделало это для меня, так это осознание того, что метод по умолчанию для получения ImageContext устанавливает масштабирование на non-retina. К получить ImageContext для дисплея retina вам нужно использовать этот метод:
let newSize = CGSize(width: 50, height: 50) UIGraphicsBeginImageContextWithOptions(newSize, false, 0) let context = UIGraphicsGetCurrentContext()оттуда с помощью обычных методов рисования работал нормально. Установка последнего параметра в
0скажет системе использовать коэффициент масштабирования главного экрана устройства. Средний вариантfalseиспользуется для указания графического контекста, будете ли вы рисовать непрозрачное изображение (trueозначает, что изображение будет непрозрачным) или тот, который нуждается в альфа-канале, включенном для прозрачных пленок. Вот такие соответствующие документы Apple для большего контекста: https://developer.apple.com/reference/uikit/1623912-uigraphicsbeginimagecontextwitho?language=objc
Я думаю
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];



Comments