Wie zeichne ich mit CAShapeLayer und UIBezierPath einen glatten Kreis?

78

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

Hier sind die Ergebnisse aller drei Methoden: Geben Sie hier die Bildbeschreibung ein

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

Ich habe die contentsScaleEigenschaft auf gesetzt [UIScreen mainScreen].scale.

Hier ist mein Zeichnungscode 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

BEARBEITEN: 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 grün darüber gezeichnet . Beachten Sie das begrenzte Ausbluten von Rot. Beide Kreise sind "glatt" (und identisch mit der cornerRadiusImplementierung):

Geben Sie hier die Bildbeschreibung ein

In diesem zweiten Beispiel sehen Sie das Problem. Ich habe einmal mit einem CAShapeLayerin rot gezeichnet und wieder oben mit einer drawRect:Implementierung des gleichen Pfades, aber in grün. Beachten Sie, dass Sie viel mehr Inkonsistenzen mit mehr Blutungen aus dem roten Kreis darunter sehen können. Es wird eindeutig anders (und schlimmer) gezeichnet.

Geben Sie hier die Bildbeschreibung ein

Bcherry
quelle
1
Ich bin mir nicht sicher, ob es mit Ihrer Bilderfassung zusammenhängt, aber diese drei Kreise oben erscheinen - zumindest für mein Auge - genau gleich.
Max MacLeod
in dieser Zeile: [UIBezierPath bezierPathWithOvalInRect:CGRectInset(self.bounds, 4, 4)]Die self.boundHaltepunkte (keine Pixel), sodass Sie technisch gesehen eine Kurve ohne Netzhautgröße erstellen. Wenn Sie diese Größe mit dem [UIScreen mainScreen].scaleWert multiplizieren, ist Ihr Oval auf Retina-Bildschirmen perfekt glatt.
Holex
@ MaxMacLeod überprüfe meine Bearbeitung auf den Unterschied.
Bcherry
@holex Das macht nur einen kleineren Kreis. Meine beiden Implementierungen verwenden denselben Pfad, und eine sieht gut aus und die andere nicht.
Bcherry
In der Dokumentation heißt es, dass die Rasterung eher die Geschwindigkeit als die Qualität bevorzugt. Könnte sein, dass Sie Artefakte ungenauer Interpolation / Antialiasing sehen.
Leo Natan

Antworten:

70

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

TL; DR: Wenn Sie verwenden möchten , CAShapeLayerund noch glatt Kreise zu bekommen, werden Sie verwenden müssen shouldRasterizeund rasterizationScalesorgfältig.

Original

Geben Sie hier die Bildbeschreibung ein

Hier ist dein Original CAShapeLayerund ein Unterschied zur drawRectVersion. Ich habe mit Retina Display einen Screenshot von meinem iPad Mini gemacht, ihn dann in Photoshop massiert und zu 200% gesprengt. Wie Sie deutlich sehen können, CAShapeLayerweist die Version sichtbare Unterschiede auf, insbesondere am linken und rechten Rand (dunkelste Pixel im Diff).

Im Bildschirmmaßstab rastern

Geben Sie hier die Bildbeschreibung ein

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

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

Beachten Sie, dass rasterizationScalestandardmäßig 1.0auch auf Retina - Geräte, die für die Verschwommenheit von Standardkonten shouldRasterize.

Der Kreis ist jetzt etwas glatter, aber die schlechten Stellen (dunkelste Pixel im Diff) haben sich an den oberen und unteren Rand verschoben. Nicht nennenswert besser als kein Rastering!

Bei 2x Bildschirmskala rastern

Geben Sie hier die Bildbeschreibung ein

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

Dadurch wird der Pfad im 2-fachen Bildschirmmaßstab oder bis zu 4.0Retina-Geräten gerastert .

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

Ich habe dies auch in Instruments: Core Animation ausgeführt und keine wesentlichen Unterschiede bei den Debug-Optionen für Core Animation festgestellt. Es kann jedoch langsamer sein, da es nicht nur eine Offscreen-Bitmap auf den Bildschirm verkleinert. Möglicherweise müssen Sie shouldRasterize = NOwährend der Animation auch vorübergehend Einstellungen vornehmen.

Was funktioniert nicht?

  • Stellen Sie shouldRasterize = YESsich. Auf Netzhautgeräten sieht dies da unscharf aus rasterizationScale != screenScale.

  • Stellen Sie ein contentScale = screenScale. Da CAShapeLayernicht berücksichtigt wird contents, ob es sich um ein Rastering handelt oder nicht, wirkt sich dies nicht auf die Wiedergabe aus.

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

Glen Low
quelle
Danke für die Antwort! Dies scheint in einigen Fällen eine angemessene Lösung zu bieten. Wissen Sie, dass @ 32Beat korrekt ist und dass CAShapeLayer aufgrund von Optimierungen für Animationen eine Kopie mit niedrigerer Wiedergabetreue rendert? Es scheint, als ob das direkte Zeichnen es schafft, das Richtige im 2-fachen Maßstab zu zeichnen. Wenn Sie also den Maßstab erhöhen, wird das Problem umgangen, aber das zugrunde liegende Problem mit CAShapeLayer wird letztendlich nicht behoben.
Bcherry
2
Ich bin mir ziemlich sicher, dass CAShapeLayeres für schnelles Zeichnen und Animieren optimiert ist. Es hat jedoch noch so andere Vorteile, dass ich es lieber benutze drawRect: Auflösungsunabhängigkeit (warten Sie nur, bis Apple ein 4x Retina-Display produziert), weniger Speicher, ohne Verzerrung transformierbar, flexible Rasterung usw.
Glen Low
Betreff: flexible Rasterung. Sie können ad hoc festlegen, ob gerastert werden soll oder nicht, oder sogar auf welcher Ebene, z. B. in einer Ebene, die aus mehreren Ebenen besteht CAShapeLayer.
Glen Low
Ich vermute, Apple verwendet eine hohe Ebenheit für die CAShapeLayerWiedergabe. Sie können dies manchmal in Nicht-Retina-Anzeigen sehen: Die Wiedergabe sieht aus wie ein Polygon. Die Lösung wäre dann, die Abflachung selbst vorzunehmen. Siehe z. B. ps.missouri.edu/ps2/support/tutorialfolder/flatness/index.html
Glen Low
9

Ah, ich bin vor einiger Zeit auf dasselbe Problem gestoßen (es war immer 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 ausbluten lassen. Abhängig von den Farben wäre dies sehr auffällig. Und während der Benutzerinteraktion würde die Form noch schlechter gerendert, was mich zu dem Schluss führen würde, dass die Formschicht unabhängig vom Ebenen-Skalierungsfaktor immer mit einem Skalierungsfaktor von 1,0 gerendert wird, da sie für Animationszwecke gedacht ist.

Das heißt, Sie verwenden einen CAShapeLayer nur, wenn Sie bestimmte animierbare Änderungen an der Form des Bezierpfads benötigen, nicht an anderen Eigenschaften, die über die üblichen Ebeneneigenschaften animierbar sind.

Ich habe mich schließlich entschlossen, einen einfachen ShapeLayer zu schreiben, der sein eigenes Ergebnis zwischenspeichert, aber Sie könnten versuchen, den displayLayer: oder den drawLayer: inContext: zu implementieren.

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

Ich habe das nicht versucht, wäre aber interessant, das Ergebnis zu erfahren ...

32Beat
quelle
Das macht sehr viel Sinn. Ich habe festgestellt, dass es egal ist, auf was ich contentScale eingestellt habe, es sieht immer noch genauso aus. Ich habe Ihre displayLayer-Implementierung ausprobiert. Es machte leider keinen Unterschied. Ich vermute, Sie haben Recht damit, dass es animationsorientierte Zeichnungsoptimierungen für CAShapeLayer gibt. Ich werde weiterhin benutzerdefinierte Zeichnungen für meine nicht animierten Ebenen verwenden.
Bcherry
4

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 eine kleine Änderung, 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 welchem ​​Vorschlag ich aus den anderen Antworten folgte. Für mich war es schließlich klar, dass die Standardmethode zum Abrufen eines ImageContext die Skalierung auf Nicht-Retina setzt. Um einen ImageContext für eine Retina-Anzeige zu erhalten, müssen Sie folgende 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 0einstellen, wird das System angewiesen, den Skalierungsfaktor des Hauptbildschirms des Geräts zu verwenden. Mit der mittleren Option falsekönnen Sie dem Grafikkontext mitteilen, ob Sie ein undurchsichtiges Bild zeichnen ( truedh das Bild ist undurchsichtig) oder einen Alphakanal für Transparentfolien benötigen. Hier sind die entsprechenden Apple-Dokumente für mehr Kontext: https://developer.apple.com/reference/uikit/1623912-uigraphicsbeginimagecontextwitho?language=objc

JiuJitsuCoder
quelle
1

Ich denke, es CAShapeLayerwird durch eine performantere Art des Renderns seiner Formen unterstützt und es werden einige Verknüpfungen verwendet. Auf jeden CAShapeLayerFall kann der Hauptfaden etwas langsam sein. Sofern Sie nicht zwischen verschiedenen Pfaden animieren müssen, würde ich vorschlagen, asynchron zu UIImageeinem Hintergrundthread zu rendern .

hfossli
quelle
-1

Verwenden Sie diese Methode, um UIBezierPath zu zeichnen

/*********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;
}

Und so zeichnen

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];
Sandeep Singh
quelle
Ich habe diese Methode ausprobiert, um einen Kreis zu bilden und einen abgerundeten rechten Pfad zu verwenden. Beides löst das Problem nicht mit der Pixelierung entlang der Ränder eines mit CAShapeLayer gezeichneten Pfads.
Bcherry