Як намалювати плавне коло за допомогою CAShapeLayer та UIBezierPath?


78

Я намагаюся намалювати обведене коло, використовуючи 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:реалізацією того самого шляху, але зеленим. Зверніть увагу, що ви можете побачити набагато більше невідповідностей з більшим кровотоком з червоного кола внизу. Очевидно, це намальовано по-іншому (і гірше).

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


1
не впевнений, що це пов’язано із захопленням вашого зображення, але ці три кола вище з’являються - принаймні на моє око - точно такими ж.
Макс Маклауд,

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

@MaxMacLeod перевір мою редакцію на різницю.
bcherry

@holex Це просто робить менший круг. Обидві мої реалізації використовують той самий шлях, і один виглядає добре, а інший - ні.
bcherry

У документації сказано, що растеризація сприяє швидше, аніж якості. Можливо, ви бачите артефакти неточної інтерполяції / згладжування.
Лео Натан

Відповіді:


70

Хто знав, що існує так багато способів намалювати коло?

TL; DR: Якщо ви хочете використовувати CAShapeLayerі по- , як і раніше отримувати гладкі кола, ви повинні будете використовувати shouldRasterizeі rasterizationScaleретельно.

Оригінал

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

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

Растеризувати в масштабі екрану

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

Давайте зростеризуємо в масштабі екрану, який повинен бути 2.0на пристроях сітківки. Додайте цей код:

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

Зверніть увагу, що rasterizationScaleзначення за замовчуванням 1.0навіть на пристроях сітківки, що пояснює нечіткість за замовчуванням shouldRasterize.

Коло тепер трохи згладжене, але погані біти (найтемніші пікселі в різниці) перемістилися до верхнього та нижнього країв. Не помітно краще, ніж відсутність растеризації!

Растеризувати в масштабі екрану в 2 рази

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

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

Це растеризує шлях із масштабом екрану у 2 рази або 4.0на пристроях сітківки.

Коло тепер помітно плавніше, різниці значно легші і розподілені рівномірно.

Я також запустив це в "Інструменти: Основна анімація" і не побачив жодних суттєвих відмінностей у параметрах налагодження Core Animation. Однак це може бути повільніше, оскільки це зменшення масштабу, а не просто перенесення екранного растрового зображення на екран. Можливо, вам також доведеться тимчасово встановити shouldRasterize = NOпід час анімації.

Що не працює

  • Встановлюється shouldRasterize = YESсам по собі. На пристроях сітківки це виглядає нечітко, оскільки rasterizationScale != screenScale.

  • Встановити contentScale = screenScale. Оскільки CAShapeLayerне втягує contents, незалежно від того, є це растеризацією чи ні, це не впливає на передачу.

Честь Джею Голлівуду з Хумана , гострому графічному дизайнеру, який вперше вказав мені це.


Дякую за відповідь! Звичайно, це забезпечує гідний спосіб вирішити деякі випадки. Чи знаєте ви, що @ 32Beat правильний, що CAShapeLayer надає копію з меншою точністю через оптимізацію анімації? Здається, прямим малюванням вдається намалювати потрібну річ із масштабом у 2 рази, тому збільшення масштабу допоможе вирішити проблему, але врешті-решт не вирішить основну проблему з CAShapeLayer.
bcherry

2
Я майже впевнений CAShapeLayer, що оптимізований для швидкого малювання та анімації. Однак він все ще має настільки інші переваги, що я вважаю за краще використовувати його над drawRect: незалежність роздільної здатності (просто почекайте, поки Apple випустить 4-кратний дисплей сітківки), менше пам'яті, що трансформується без спотворень, гнучка растеризація тощо
Глен Лоу

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

Я підозрюю, що Apple використовує високу площинність для CAShapeLayerпередачі. Іноді це можна побачити на дисплеях, що не мають сітківки ока: передача виглядає як багатокутник. Тоді виправлення полягало б у тому, щоб зробити сплющення самостійно. Див., Наприклад, ps.missouri.edu/ps2/support/tutorialfolder/flatness/index.html
Глен Лоу

9

Ах, я зіткнувся з тією ж проблемою деякий час тому (це все ще був 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. 
*/

Заповнене коло під фігурою кола виливало б колір його заливки. Залежно від кольорів це було б дуже помітно. А під час взаємодії з користувачем форма буде виглядати ще гірше, що дозволить мені зробити висновок, що шар форми завжди відображатиметься з масштабним коефіцієнтом 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;
}

Я цього не пробував, але було б цікаво дізнатись результат ...


Це має великий сенс. Я помітив, що неважливо, для чого я встановив contentScale, він все одно виглядає так само. Я спробував реалізацію displayLayer. На жаль, це не змінило ситуації. Я підозрюю, що ви маєте рацію щодо оптимізації малювання, орієнтованої на анімацію, для CAShapeLayer. Я продовжуватиму використовувати користувальницький малюнок для своїх неінімованих шарів.
bcherry

4

Я знаю, що це давнє запитання, але для тих, хто намагається працювати в методі 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


1

Я думаю, що CAShapeLayerвін підкріплений більш ефективним способом візуалізації його форм і використовує деякі ярлики. У будь-якому випадку CAShapeLayerможе бути трохи повільним щодо основної теми. Якщо вам не потрібно анімувати між різними шляхами, я б запропонував асинхронно відтворити їх UIImageу фоновому потоці.


-1

За допомогою цього методу намалюйте 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.
bcherry
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.