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:
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):
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.
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 SieshouldRasterize
undrasterizationScale
vorsichtig.
Original
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
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
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.
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 ...
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
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.