Как нарисовать гладкий круг с CAShapeLayer и UIBezierPath?



Я пытаюсь нарисовать гладкий круг с помощью CAShapeLayer и установить круговой путь на нем. Тем не менее, этот метод является менее точным, когда последовательно отображены на экране, чем при использовании borderRadius или рисуя путь в CGContextRef напрямую.



вот результаты всех трех методов: enter image description here



обратите внимание, что третий плохо визуализируется, особенно внутри штриха сверху и снизу.



Я есть установить 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 реализация):



enter image description here



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



enter image description here

819   5  

5 ответов:

кто знал, что есть так много способов, чтобы нарисовать круг?

TL; DR: если вы хотите использовать CAShapeLayer и все равно получите гладкие круги, вам нужно будет использовать shouldRasterize и rasterizationScale тщательно.

Оригинал

enter image description here

вот твой оригинал CAShapeLayer и отличия от drawRect версия. Я сделал скриншот с моего iPad Mini с дисплеем Retina, затем помассировал его в Photoshop и взорвал его до 200%. Как вы можете ясно видеть,CAShapeLayer версия имеет видимые различия, особенно на левом и правом краях (самые темные пиксели в diff).

растеризация в масштабе экрана

enter image description here

давайте растеризуем в масштабе экрана, который должен быть 2.0 на устройствах retina. Добавьте этот код:

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

отметим, что rasterizationScale по умолчанию 1.0 даже на устройствах retina, что объясняет нечеткость по умолчанию shouldRasterize.

круг Теперь немного более гладкий, но плохие биты (самые темные пиксели в diff) переместились к верхнему и нижнему краям. Не заметно лучше, чем без растеризации!

растеризация в масштабе экрана 2x

enter image description here

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

    Ничего не найдено.