Подтвердить что ты не робот

Как нарисовать гладкий круг с 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

EDIT: для тех, кто не видит разницы, я нарисовал круги друг над другом и увеличил масштаб:

Здесь я нарисовал красный круг с drawRect:, а затем снова нарисовал идентичный круг с drawRect: зеленым над ним. Обратите внимание на ограниченное кровотечение красного цвета. Оба эти круга являются "гладкими" (и идентичны реализации cornerRadius):

enter image description here

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

enter image description here

4b9b3361

Ответ 1

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

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

Оригинал

enter image description here

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

Растрировать в масштабе экрана

enter image description here

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

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

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

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

Растрировать на шкале масштаба 2

enter image description here

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

Это растеризует путь с 2x-масштабной шкалой или до 4.0 на сетчатых устройствах.

Теперь круг становится более гладким, различия намного легче и равномерно распределяются.

Я также использовал это в "Инструменты: Core Animation" и не видел серьезных отличий в параметрах отладки Core Animation. Однако это может быть медленнее, так как оно уменьшает масштаб, не просто смещая экранное растровое изображение на экран. Вам также может потребоваться временно установить shouldRasterize = NO во время анимации.

Что не работает

  • Установите shouldRasterize = YES самостоятельно. На устройствах сетчатки это выглядит нечетким, потому что rasterizationScale != screenScale.

  • Установите contentScale = screenScale. Поскольку CAShapeLayer не обращается в contents, независимо от того, растеризует он или нет, это не влияет на выдачу.

Кредит Джея Голливуда Humaan, резкого графического дизайнера, который сначала указал мне.

Ответ 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. 
*/

Заполненный круг под кольчатой ​​линией истечет кровью. В зависимости от цветов это было бы очень заметно. И во время userinteraction форма окажет еще хуже, что позволит мне сделать вывод о том, что shapelayer всегда будет отображаться с scalefactor 1.0, независимо от уровня scalefactor слоя, потому что он предназначен для анимации.

то есть. вы используете только CAShapeLayer, если у вас есть особая потребность в анимированных изменениях формы для bezierpath, а не для любых других свойств, которые являются анимированными с помощью обычных свойств слоя.

В конце концов я решил написать простой 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;
}

Я не пробовал это, но было бы интересно узнать результат...

Ответ 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 означает, что изображение будет непрозрачным) или тот, для которого необходим альфа-канал, включенный для прозрачных пленок. Вот соответствующие документы Apple для большего контекста: 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];