wake-up-neo.com

Wie zeichnet man einen glatten Kreis mit CAShapeLayer und UIBezierPath?

Ich versuche, einen gestrichenen Kreis mit einem CAShapeLayer zu zeichnen und einen kreisförmigen Pfad darauf zu setzen. Diese Methode ist jedoch beim Rendern auf dem Bildschirm durchweg ungenauer als die Verwendung von borderRadius oder das direkte Zeichnen des Pfads in einem CGContextRef.

Hier sind die Ergebnisse aller drei Methoden: enter image description here

Beachten Sie, dass der dritte Bereich schlecht gerendert ist, insbesondere innerhalb des Strichs oben und unten.

I have setze die Eigenschaft contentsScale auf [UIScreen mainScreen].scale.

Hier ist mein Zeichencode für diese drei Kreise. Was fehlt, damit der CAShapeLayer reibungslos zeichnet?

@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: Für diejenigen, die den Unterschied nicht sehen können, habe ich Kreise übereinander gezeichnet und vergrößert:

Hier habe ich einen roten Kreis mit drawRect: Gezeichnet und dann einen identischen Kreis mit drawRect: Wieder in Grün darüber gezeichnet. Beachten Sie das begrenzte Ausbluten von Rot. Diese beiden Kreise sind "glatt" (und identisch mit der cornerRadius -Implementierung):

enter image description here

In diesem zweiten Beispiel sehen Sie das Problem. Ich habe einmal mit einem CAShapeLayer in Rot und nochmals mit einer drawRect: - Implementierung desselben Pfades darüber gezeichnet, aber in Grün. Beachten Sie, dass Sie viel mehr Inkonsistenzen mit mehr Ausbluten aus dem roten Kreis darunter sehen können. Es wird offensichtlich auf eine andere (und schlimmere) Weise gezeichnet.

enter image description here

72
bcherry

Wer hätte gedacht, dass es so viele Möglichkeiten gibt, einen Kreis zu zeichnen?

TL; DR: Wenn Sie CAShapeLayer verwenden möchten und dennoch glatte Kreise erhalten, müssen Sie shouldRasterize und rasterizationScale vorsichtig.

Original

enter image description here

Hier ist dein ursprüngliches CAShapeLayer und ein Diff von der drawRect Version. Ich habe einen Screenshot von meinem iPad Mini mit Retina Display gemacht, ihn dann in Photoshop massiert und ihn zu 200% geblasen. Wie Sie deutlich sehen können, weist die CAShapeLayer -Version sichtbare Unterschiede auf, insbesondere am linken und rechten Rand (dunkelste Pixel im Diff).

Rastern im Bildschirmmaßstab

enter image description here

Lassen Sie uns im Bildschirmmaßstab rastern, der auf Retina-Geräten 2.0 Sein sollte. Füge diesen Code hinzu:

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

Beachten Sie, dass rasterizationScale auch auf Retina-Geräten standardmäßig 1.0 Ist, was die Unschärfe von shouldRasterize erklärt.

Der Kreis ist jetzt etwas glatter, aber die fehlerhaften Stellen (dunkelste Pixel im Diff) sind an den oberen und unteren Rand verschoben worden. Nicht wesentlich besser als kein Rastern!

Rastern im 2-fachen Bildschirmmaßstab

enter image description here

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

Dies rastert den Pfad im 2-fachen Bildschirmmaßstab oder bis zu 4.0 Auf Retina-Geräten.

Der Kreis ist jetzt sichtbar glatter, die Diffs sind viel heller und gleichmäßig verteilt.

Ich habe dies auch in Instruments: Core Animation ausgeführt und keine wesentlichen Unterschiede in den Debug-Optionen für Core Animation festgestellt. Es kann jedoch langsamer sein, da beim Herunterskalieren nicht nur eine Offscreen-Bitmap auf den Bildschirm geblendet wird. Möglicherweise müssen Sie während der Animation auch vorübergehend shouldRasterize = NO Einstellen.

Was nicht funktioniert

  • Set shouldRasterize = YES Von selbst. Auf Retina-Geräten sieht dies unscharf aus, weil rasterizationScale != screenScale.

  • Set contentScale = screenScale. Da CAShapeLayer nicht in contents zeichnet, hat dies keinen Einfluss darauf, ob es rastert oder nicht die Überstellung.

Dank an Jay Hollywood von Humaan , einem scharfen Grafikdesigner, der mich zuerst darauf hingewiesen hat.

67
Glen Low

Ah, ich bin vor einiger Zeit auf das gleiche Problem gestoßen (es war noch iOS 5, dann iirc), und ich habe den folgenden Kommentar in den Code geschrieben:

/*
    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. 
*/

Ein gefüllter Kreis unter einer Kreisform würde seine Füllfarbe verlaufen lassen. Abhängig von den Farben wäre dies sehr auffällig. Und während der Benutzerinteraktion wird die Form noch schlechter, was zu dem Schluss führt, dass der Shapelayer unabhängig vom Skalierungsfaktor der Ebene immer mit einem Skalierungsfaktor von 1,0 gerendert wird, da er für Animationszwecke gedacht ist.

sie verwenden eine CAShapeLayer nur, wenn Sie animierbare Änderungen an der Form des Bezierpfads benötigen, nicht an einer der anderen Eigenschaften, die über die üblichen Ebeneneigenschaften animiert werden können.

Schließlich habe ich beschlossen, einen einfachen ShapeLayer zu schreiben, der sein eigenes Ergebnis zwischenspeichert, aber Sie könnten versuchen, den displayLayer: oder den drawLayer: inContext zu implementieren:

So etwas wie:

- (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;
}

Das habe ich noch nicht ausprobiert, aber es wäre interessant, das Ergebnis zu erfahren ...

9
32Beat

Ich weiß, dass dies eine ältere Frage ist, aber für diejenigen, die versuchen, mit der drawRect-Methode zu arbeiten und immer noch Probleme haben, war es eine kleine Verbesserung, die mir immens geholfen hat, die richtige Methode zum Abrufen des UIGraphicsContext zu verwenden. Standard verwenden:

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

würde zu verschwommenen Kreisen führen, egal welchen Vorschlag ich aus den anderen Antworten folgte. Schließlich stellte ich fest, dass die Standardmethode zum Abrufen eines ImageContext die Skalierung auf Nicht-Retina festlegt. Um einen ImageContext für eine Retina-Anzeige zu erhalten, müssen Sie diese Methode verwenden:

let newSize = CGSize(width: 50, height: 50)
UIGraphicsBeginImageContextWithOptions(newSize, false, 0)
let context = UIGraphicsGetCurrentContext()

von dort aus funktionierten die normalen Zeichenmethoden einwandfrei. Wenn Sie die letzte Option auf 0 Setzen, wird das System angewiesen, den Skalierungsfaktor des Hauptbildschirms des Geräts zu verwenden. Die mittlere Option false teilt dem Grafikkontext mit, ob Sie ein undurchsichtiges Bild zeichnen (true bedeutet, dass das Bild undurchsichtig ist) oder ein Bild, für das ein Alphakanal enthalten sein muss Folien. Hier sind die entsprechenden Apple Dokumente für mehr Kontext: https: //developer.Apple.com/reference/uikit/1623912-uigraphicsbeginimagecontextwitho? Language = objc

4
JiuJitsuCoder

Ich denke, CAShapeLayer wird durch eine performantere Art der Darstellung seiner Formen unterstützt und verwendet einige Abkürzungen. Jedenfalls kann CAShapeLayer im Haupt-Thread etwas langsam sein. Sofern Sie nicht zwischen verschiedenen Pfaden animieren müssen, würde ich vorschlagen, asynchron zu einem UIImage in einem Hintergrund-Thread zu rendern.

1
hfossli