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

#ios #uikit #core-graphics #calayer #cashapelayer

#iOS #uikit #ядро-графика #calayer #cashapelayer

Вопрос:

Я пытаюсь нарисовать обводный круг, используя 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 реализации):

введите описание изображения здесь

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

введите описание изображения здесь

Комментарии:

1. не уверен, связано ли это с вашим захватом изображения, но эти три круга выше выглядят — по крайней мере, на мой взгляд — точно так же.

2. в этой строке: [UIBezierPath bezierPathWithOvalInRect:CGRectInset(self.bounds, 4, 4)] self.bound содержит точки (не пиксели), поэтому технически вы создаете кривую размером не с сетчатку. если вы умножите этот размер на [UIScreen mainScreen].scale значение, ваш овал будет идеально ровным на экранах retina.

3. @MaxMacLeod проверьте мое редактирование на предмет разницы.

4. @holex Это просто делает меньший круг. Обе мои реализации используют один и тот же путь, и одна выглядит хорошо, а другая — нет.

5. В документации говорится, что растеризация способствует скорости, а не качеству. Может быть, вы видите артефакты неточной интерполяции / сглаживания.

Ответ №1:

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

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) переместились к верхнему и нижнему краям. Не заметно лучше, чем отсутствие растрирования!

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

введите описание изображения здесь

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

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

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

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

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

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

Спасибо Джею Голливуду из Humaan, опытному графическому дизайнеру, который первым указал мне на это.

Комментарии:

1. Спасибо за ответ! Это, безусловно, кажется достойным решением для некоторых случаев. Знаете ли вы, что @32Beat верен, что CAShapeLayer отображает копию с более низкой точностью из-за оптимизации анимации? Похоже, что direct drawing удается нарисовать правильную вещь всего в 2 раза, поэтому увеличение масштаба позволит обойти проблему, но в конечном итоге не решит основную проблему с CAShapeLayer.

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

3. Re: гибкая растеризация. Вы можете определить ad hoc, следует ли растрировать или нет, или даже на каком уровне, например, в слое, который состоит из нескольких CAShapeLayer .

4. Я подозреваю, что Apple использует высокую плоскостность для CAShapeLayer визуализации. Иногда это можно увидеть на дисплеях, отличных от retina: изображение выглядит как многоугольник. Тогда решение проблемы состояло бы в том, чтобы сделать выравнивание самостоятельно. См . , например , 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 только в том случае, если у вас есть особая потребность в анимируемых изменениях формы 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;
}
 

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

Комментарии:

1. Это имеет большой смысл. Я заметил, что не имеет значения, на что я устанавливаю contentsScale, он все равно выглядит одинаково. Я попробовал вашу реализацию displayLayer. К сожалению, это ничего не изменило. Я подозреваю, что вы правы в том, что для CAShapeLayer существует оптимизация рисования, ориентированная на анимацию. Я продолжу использовать пользовательский рисунок для своих неанимированных слоев.

Ответ №3:

Я знаю, что это старый вопрос, но для тех, кто пытается работать в методе drawRect и все еще испытывает проблемы, одна небольшая настройка, которая мне очень помогла, заключалась в использовании правильного метода для извлечения UIGraphicsContext. Используя значение по умолчанию:

 let newSize = CGSize(width: 50, height: 50)
UIGraphicsBeginImageContext(newSize)
let context = UIGraphicsGetCurrentContext()
 

это приведет к размытым кругам, независимо от того, какое предложение я следовал из других ответов. Что, наконец, сделало это для меня, так это осознание того, что метод по умолчанию для получения ImageContext устанавливает масштабирование без сетчатки. Чтобы получить 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 ?язык = objc

Ответ №4:

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

Комментарии:

1. Я пробовал этот метод создания окружности, а также использования закругленной прямой траектории. Ни то, ни другое не решает проблему с пикселизацией по краям контура, нарисованного с помощью CAShapeLayer.