Domanda Come disegnare un cerchio uniforme con CAShapeLayer e UIBezierPath?


Sto tentando di disegnare un cerchio tratteggiato usando un CAShapeLayer e impostando un percorso circolare su di esso. Tuttavia, questo metodo è costantemente meno accurato quando viene reso sullo schermo rispetto all'utilizzo di borderRadius o il disegno del percorso in un CGContextRef direttamente.

Ecco i risultati di tutti e tre i metodi: enter image description here

Si noti che il terzo è reso male, specialmente all'interno del tratto in alto e in basso.

io avere impostare il contentsScale proprietà a [UIScreen mainScreen].scale.

Ecco il mio codice di disegno per questi tre cerchi. Cosa manca per rendere il disegno di CAShapeLayer senza intoppi?

@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: Per coloro che non riescono a vedere la differenza, ho disegnato cerchi uno sopra l'altro e ingrandito:

Qui ho disegnato un cerchio rosso con drawRect:e quindi disegnato un cerchio identico con drawRect: di nuovo in verde sopra di esso. Notare il sanguinamento limitato di rosso. Entrambi questi cerchi sono "lisci" (e identici al cornerRadius implementazione):

enter image description here

In questo secondo esempio, vedrai il problema. Ho disegnato una volta usando un CAShapeLayer in rosso e di nuovo in alto con a drawRect: implementazione dello stesso percorso, ma in verde. Nota che puoi notare molta più incoerenza con più salassi dal cerchio rosso sottostante. È chiaramente disegnato in modo diverso (e peggiore).

enter image description here


70
2018-06-19 21:43


origine


risposte:


Chi sapeva che ci sono tanti modi per disegnare un cerchio?

TL; DR: Se vuoi usarlo CAShapeLayer e ancora ottenere cerchi lisci, è necessario utilizzare shouldRasterize e rasterizationScale accuratamente.

Originale

enter image description here

Ecco il tuo originale CAShapeLayer e un diff dal drawRect versione. Ho fatto uno screenshot sul mio iPad Mini con Retina Display, poi lo ho massaggiato in Photoshop e lo ho fatto saltare al 200%. Come puoi vedere chiaramente, il CAShapeLayer la versione ha differenze visibili, specialmente sui bordi sinistro e destro (i pixel più scuri del diff).

Rasterizza a scala di schermo

enter image description here

Ricalciamolo alla scala dello schermo, che dovrebbe essere 2.0 su dispositivi retina. Aggiungi questo codice:

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

Nota che rasterizationScale predefinito 1.0 anche su dispositivi retina, che rappresenta la sfocatura di default shouldRasterize.

Il cerchio ora è un po 'più liscio, ma i bit negativi (i pixel più scuri del diff) si sono spostati sui bordi superiore e inferiore. Non sensibilmente meglio di nessun rasterizzazione!

Rasterizza con una scala a schermo 2x

enter image description here

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

Questo rasterizza il percorso a scala di schermo 2x o fino a 4.0 su dispositivi retina.

Il cerchio ora è visibilmente più liscio, i differ sono molto più leggeri e si distribuiscono uniformemente.

Ho anche eseguito questo in Strumenti: Core Animation e non ho visto alcuna differenza importante nelle opzioni di debug di animazione core. Tuttavia potrebbe essere più lento dal momento che il downscaling non si limita a inserire una bitmap fuori schermo sullo schermo. Potrebbe anche essere necessario impostare temporaneamente shouldRasterize = NO durante l'animazione.

Cosa non funziona

  • Impostato shouldRasterize = YES da solo. Sui dispositivi a retina, questo sembra sfocato perché rasterizationScale != screenScale.

  • Impostato contentScale = screenScale. Da CAShapeLayer non attira contents, sia che rasterizzi o meno, ciò non influisce sulla resa.

Ringraziamento a Jay Hollywood di Farmaci Anti-, un abile graphic designer che per primo mi ha fatto notare.


62
2017-08-11 08:33



Ah, mi sono imbattuto nello stesso problema qualche tempo fa (era ancora iOS 5 poi iirc), e ho scritto il seguente commento nel codice:

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

Un cerchio pieno sotto una forma di cerchi avrebbe sanguinato il suo colore di riempimento. A seconda dei colori questo sarebbe molto evidente. E durante l'interazione dell'utente, la forma risulterebbe ancora peggiore, il che mi permette di concludere che lo shapelayer dovrebbe sempre rappresentare con un fattore di scala di 1.0, indipendentemente dal fattore di scala del livello, perché è inteso per scopi di animazione.

ad esempio, utilizzi un CAShapeLayer solo se hai esigenze specifiche di modifiche animabili la forma di bezierpath, non a nessuna delle altre proprietà animabili tramite le solite proprietà del layer.

Alla fine ho deciso di scrivere un semplice ShapeLayer in grado di memorizzare il proprio risultato nella cache, ma potresti provare a implementare displayLayer: o drawLayer: inContext:

Qualcosa di simile a:

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

Non l'ho provato, ma sarebbe interessante sapere il risultato ...


9
2017-07-01 11:55



So che questa è una domanda più vecchia, ma per coloro che stanno cercando di lavorare con il metodo drawRect e hanno ancora problemi, una piccola modifica che mi ha aiutato immensamente è stata l'utilizzo del metodo corretto per recuperare UIGraphicsContext. Utilizzando l'impostazione predefinita:

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

risulterebbe in cerchi sfocati, non importa quale suggerimento ho seguito dalle altre risposte. Ciò che alla fine ha fatto per me è stato rendersi conto che il metodo predefinito per ottenere un ImageContext imposta il ridimensionamento su non-retina. Per ottenere un ImageContext per un display retina è necessario utilizzare questo metodo:

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

da lì usando i normali metodi di disegno ha funzionato bene. Impostazione dell'ultima opzione su 0 indicherà al sistema di utilizzare il fattore di scala della schermata principale del dispositivo. L'opzione centrale false è usato per dire al contesto grafico se disegnerete o meno un'immagine opaca (true significa che l'immagine sarà opaca) o che necessita di un canale alfa incluso per i lucidi. Ecco i documenti Apple appropriati per più contesto: https://developer.apple.com/reference/uikit/1623912-uigraphicsbeginimagecontextwitho?language=objc


4
2017-11-21 18:15



suppongo CAShapeLayer è supportato da un modo più performante di rendere le sue forme e prende alcune scorciatoie. Comunque CAShapeLayer può essere un po 'lento sul thread principale. A meno che non sia necessario animare tra percorsi diversi, suggerirei di eseguire il rendering in modo asincrono su a UIImage su un thread in background.


1
2017-07-01 12:05



Utilizzare questo metodo per disegnare 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;
}

E disegnare in questo modo

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
2017-07-01 12:44