UIView arrotondato utilizzando CALayers - solo alcuni angoli - Come?


121

Nella mia applicazione ci sono quattro pulsanti denominati come segue:

  • In alto a sinistra
  • In basso a sinistra
  • In alto a destra
  • In basso a destra

Sopra i pulsanti c'è una visualizzazione dell'immagine (o una visualizzazione UIV).

Supponiamo ora che un utente tocchi il pulsante in alto a sinistra. L'immagine / visualizzazione sopra dovrebbe essere arrotondata in quel particolare angolo.

Ho qualche difficoltà nell'applicazione degli angoli arrotondati a UIView.

In questo momento sto usando il seguente codice per applicare gli angoli arrotondati a ciascuna vista:

    // imgVUserImg is a image view on IB.
    imgVUserImg.image=[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"any Url Here"];
    CALayer *l = [imgVUserImg layer];
    [l setMasksToBounds:YES];
    [l setCornerRadius:5.0];  
    [l setBorderWidth:2.0];
    [l setBorderColor:[[UIColor darkGrayColor] CGColor]];

Il codice precedente applica la rotondità a ciascuno degli angoli della vista fornita. Invece volevo solo applicare la rotondità agli angoli selezionati come - in alto / in alto + a sinistra / in basso + a destra ecc.

È possibile? Come?

Risposte:


44

Ho usato la risposta su Come faccio a creare un'etichetta UILabel con angoli arrotondati su iPhone? e il codice da Come viene eseguita una vista retta arrotondata con trasparenza su iPhone? per creare questo codice.

Poi ho capito di aver risposto alla domanda sbagliata (ho dato un UILabel arrotondato invece di UIImage) quindi ho usato questo codice per cambiarlo:

http://discussions.apple.com/thread.jspa?threadID=1683876

Crea un progetto per iPhone con il modello Visualizza. Nel controller della vista, aggiungi questo:

- (void)viewDidLoad
{
    CGRect rect = CGRectMake(10, 10, 200, 100);
    MyView *myView = [[MyView alloc] initWithFrame:rect];
    [self.view addSubview:myView];
    [super viewDidLoad];
}

MyViewè solo una UIImageViewsottoclasse:

@interface MyView : UIImageView
{
}

Non avevo mai usato contesti grafici prima, ma sono riuscito a zoppicare insieme questo codice. Manca il codice per due degli angoli. Se leggi il codice, puoi vedere come l'ho implementato (eliminando alcune delle CGContextAddArcchiamate ed eliminando alcuni dei valori del raggio nel codice. Il codice per tutti gli angoli è lì, quindi usalo come punto di partenza ed elimina il parti che creano angoli di cui non hai bisogno Nota che puoi anche creare rettangoli con 2 o 3 angoli arrotondati se lo desideri.

Il codice non è perfetto, ma sono sicuro che puoi sistemarlo un po '.

static void addRoundedRectToPath(CGContextRef context, CGRect rect, float radius, int roundedCornerPosition)
{

    // all corners rounded
    //  CGContextMoveToPoint(context, rect.origin.x, rect.origin.y + radius);
    //  CGContextAddLineToPoint(context, rect.origin.x, rect.origin.y + rect.size.height - radius);
    //  CGContextAddArc(context, rect.origin.x + radius, rect.origin.y + rect.size.height - radius, 
    //                  radius, M_PI / 4, M_PI / 2, 1);
    //  CGContextAddLineToPoint(context, rect.origin.x + rect.size.width - radius, 
    //                          rect.origin.y + rect.size.height);
    //  CGContextAddArc(context, rect.origin.x + rect.size.width - radius, 
    //                  rect.origin.y + rect.size.height - radius, radius, M_PI / 2, 0.0f, 1);
    //  CGContextAddLineToPoint(context, rect.origin.x + rect.size.width, rect.origin.y + radius);
    //  CGContextAddArc(context, rect.origin.x + rect.size.width - radius, rect.origin.y + radius, 
    //                  radius, 0.0f, -M_PI / 2, 1);
    //  CGContextAddLineToPoint(context, rect.origin.x + radius, rect.origin.y);
    //  CGContextAddArc(context, rect.origin.x + radius, rect.origin.y + radius, radius, 
    //                  -M_PI / 2, M_PI, 1);

    // top left
    if (roundedCornerPosition == 1) {
        CGContextMoveToPoint(context, rect.origin.x, rect.origin.y + radius);
        CGContextAddLineToPoint(context, rect.origin.x, rect.origin.y + rect.size.height - radius);
        CGContextAddArc(context, rect.origin.x + radius, rect.origin.y + rect.size.height - radius, 
                        radius, M_PI / 4, M_PI / 2, 1);
        CGContextAddLineToPoint(context, rect.origin.x + rect.size.width, 
                                rect.origin.y + rect.size.height);
        CGContextAddLineToPoint(context, rect.origin.x + rect.size.width, rect.origin.y);
        CGContextAddLineToPoint(context, rect.origin.x, rect.origin.y);
    }   

    // bottom left
    if (roundedCornerPosition == 2) {
        CGContextMoveToPoint(context, rect.origin.x, rect.origin.y);
        CGContextAddLineToPoint(context, rect.origin.x, rect.origin.y + rect.size.height);
        CGContextAddLineToPoint(context, rect.origin.x + rect.size.width, 
                                rect.origin.y + rect.size.height);
        CGContextAddLineToPoint(context, rect.origin.x + rect.size.width, rect.origin.y);
        CGContextAddLineToPoint(context, rect.origin.x + radius, rect.origin.y);
        CGContextAddArc(context, rect.origin.x + radius, rect.origin.y + radius, radius, 
                        -M_PI / 2, M_PI, 1);
    }

    // add the other corners here


    CGContextClosePath(context);
    CGContextRestoreGState(context);
}


-(UIImage *)setImage
{
    UIImage *img = [UIImage imageNamed:@"my_image.png"];
    int w = img.size.width;
    int h = img.size.height;

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(NULL, w, h, 8, 4 * w, colorSpace, kCGImageAlphaPremultipliedFirst);

    CGContextBeginPath(context);
    CGRect rect = CGRectMake(0, 0, w, h);


    addRoundedRectToPath(context, rect, 50, 1);
    CGContextClosePath(context);
    CGContextClip(context);

    CGContextDrawImage(context, rect, img.CGImage);

    CGImageRef imageMasked = CGBitmapContextCreateImage(context);
    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);
    [img release];

    return [UIImage imageWithCGImage:imageMasked];
}

testo alternativo http://nevan.net/skitch/skitched-20100224-092237.png

Non dimenticare che dovrai inserire il framework QuartzCore per far funzionare tutto ciò.


Non funziona come previsto con visualizzazioni le cui dimensioni potrebbero cambiare. Esempio: un'etichetta UIL. L'impostazione del percorso della maschera e quindi la modifica delle dimensioni impostando il testo manterrà la vecchia maschera. Sarà necessario creare sottoclassi e sovraccaricare il drawRect.
user3099609

358

A partire da iOS 3.2, è possibile utilizzare la funzionalità di UIBezierPaths per creare un rettangolo arrotondato pronto all'uso (dove vengono arrotondati solo gli angoli specificati). Puoi quindi usarlo come percorso di ae CAShapeLayerusarlo come maschera per il livello della tua vista:

// Create the path (with only the top-left corner rounded)
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds 
                                               byRoundingCorners:UIRectCornerTopLeft
                                                     cornerRadii:CGSizeMake(10.0, 10.0)];

// Create the shape layer and set its path
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = imageView.bounds;
maskLayer.path = maskPath.CGPath;

// Set the newly created shape layer as the mask for the image view's layer
imageView.layer.mask = maskLayer;

E questo è tutto: niente scherzi nella definizione manuale delle forme in Core Graphics, niente creazione di maschere di immagini in Photoshop. Il livello non ha nemmeno bisogno di essere invalidato. Applicare l'angolo arrotondato o passare a un nuovo angolo è semplice come definirne uno nuovo UIBezierPathe usarlo CGPathcome percorso del livello maschera. Il cornersparametro del bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:metodo è una maschera di bit, quindi più angoli possono essere arrotondati mediante OR insieme.


EDIT: aggiunta di un'ombra

Se stai cercando di aggiungere un'ombra a questo, è necessario un po 'più di lavoro.

Poiché " imageView.layer.mask = maskLayer" applica una maschera, un'ombra normalmente non verrà visualizzata al di fuori di essa. Il trucco è usare una vista trasparente, quindi aggiungere due sottolivelli CALayeral livello della vista: shadowLayere roundedLayer. Entrambi devono utilizzare l'estensione UIBezierPath. L'immagine viene aggiunta come contenuto di roundedLayer.

// Create a transparent view
UIView *theView = [[UIView alloc] initWithFrame:theFrame];
[theView setBackgroundColor:[UIColor clearColor]];

// Create the path (with only the top-left corner rounded)
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:theView.bounds 
                                               byRoundingCorners:UIRectCornerTopLeft
                                                     cornerRadii:CGSizeMake(10.0f, 10.0f)];

// Create the shadow layer
CAShapeLayer *shadowLayer = [CAShapeLayer layer];
[shadowLayer setFrame:theView.bounds];
[shadowLayer setMasksToBounds:NO];
[shadowLayer setShadowPath:maskPath.CGPath];
// ...
// Set the shadowColor, shadowOffset, shadowOpacity & shadowRadius as required
// ...

// Create the rounded layer, and mask it using the rounded mask layer
CALayer *roundedLayer = [CALayer layer];
[roundedLayer setFrame:theView.bounds];
[roundedLayer setContents:(id)theImage.CGImage];

CAShapeLayer *maskLayer = [CAShapeLayer layer];
[maskLayer setFrame:theView.bounds];
[maskLayer setPath:maskPath.CGPath];

roundedLayer.mask = maskLayer;

// Add these two layers as sublayers to the view
[theView.layer addSublayer:shadowLayer];
[theView.layer addSublayer:roundedLayer];

1
Bello, questo mi ha aiutato molto su selectedBackgroundViews delle celle UITableView raggruppate :-)
Luke47

Comunque per aggiungere un'ombra esterna a questa vista dopo aver arrotondato gli angoli? Tentativo di eseguire quanto segue senza successo. `self.layer.masksToBounds = NO; self.layer.shadowColor = [UIColor blackColor] .CGColor; self.layer.shadowRadius = 3; self.layer.shadowOffset = CGSizeMake (-3, 3); self.layer.shadowOpacity = 0,8; self.layer.shouldRasterize = YES; `
Chris Wagner

1
@ ChrisWagner: vedi la mia modifica, per quanto riguarda l'applicazione di un'ombra alla vista arrotondata.
Stuart

@StuDev eccellente! Ero andato con una vista secondaria per la vista delle ombre ma questa è molto più bella! Non pensavo di aggiungere livelli secondari come questo. Grazie!
Chris Wagner

Perché la mia immagine diventa bianca, quindi quando scorro l'UITableView e la cella viene ricreata, funziona! Perché?
Fernando Redondo

18

Ho usato questo codice in molti punti del mio codice e funziona correttamente al 100%. Puoi cambiare qualsiasi corder cambiando una proprietà "byRoundingCorners: UIRectCornerBottomLeft"

UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds byRoundingCorners:UIRectCornerBottomLeft cornerRadii:CGSizeMake(10.0, 10.0)];

                CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
                maskLayer.frame = view.bounds;
                maskLayer.path = maskPath.CGPath;
                view.layer.mask = maskLayer;
                [maskLayer release];

Quando si utilizza questa soluzione su UITableView-subviews (ad esempio UITableViewCells o UITableViewHeaderFooterViews) si ottiene una scarsa fluidità durante lo scorrimento . Un altro approccio per questo utilizzo è questa soluzione con prestazioni migliori (aggiunge un cornerRadius a tutti gli angoli). Per 'mostrare' solo angoli specifici arrotondati (come gli angoli in alto e in basso a destra) ho aggiunto una vista secondaria con un valore negativo su di essa frame.origin.xe ho assegnato cornerRadius al suo livello. Se qualcuno ha trovato una soluzione migliore, mi interessa.
anneblue

11

In iOS 11, ora possiamo arrotondare solo alcuni angoli

let view = UIView()

view.clipsToBounds = true
view.layer.cornerRadius = 8
view.layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMinXMaxYCorner]

8

Estensione CALayer con sintassi Swift 3+

extension CALayer {

    func round(roundedRect rect: CGRect, byRoundingCorners corners: UIRectCorner, cornerRadii: CGSize) -> Void {
        let bp = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: cornerRadii)
        let sl = CAShapeLayer()
        sl.frame = self.bounds
        sl.path = bp.cgPath
        self.mask = sl
    }
}

Può essere utilizzato come:

let layer: CALayer = yourView.layer
layer.round(roundedRect: yourView.bounds, byRoundingCorners: [.bottomLeft, .topLeft], cornerRadii: CGSize(width: 5, height: 5))

5

L'esempio di Stuarts per arrotondare angoli specifici funziona alla grande. Se vuoi arrotondare più angoli come in alto a sinistra e a destra, ecco come farlo

// Create the path (with only the top-left corner rounded)
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageview
                                               byRoundingCorners:UIRectCornerTopLeft|UIRectCornerTopRight
                                                     cornerRadii:CGSizeMake(10.0, 10.0)];

// Create the shape layer and set its path
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = imageview.bounds;
maskLayer.path = maskPath.CGPath;

// Set the newly created shape layer as the mask for the image view's layer
imageview.layer.mask = maskLayer; 

Quando si utilizza questa soluzione su UITableView-subviews (ad esempio UITableViewCells o UITableViewHeaderFooterViews) si ottiene una scarsa fluidità durante lo scorrimento . Un altro approccio per questo utilizzo è questa soluzione con prestazioni migliori (aggiunge un cornerRadius a tutti gli angoli). Per 'mostrare' solo angoli specifici arrotondati (come gli angoli in alto e in basso a destra) ho aggiunto una vista secondaria con un valore negativo su di essa frame.origin.xe ho assegnato cornerRadius al suo livello. Se qualcuno ha trovato una soluzione migliore, mi interessa.
anneblue

5

Grazie per la condivisione. Qui vorrei condividere la soluzione su swift 2.0 per ulteriori riferimenti su questo problema. (per conformarsi al protocollo UIRectCorner)

let mp = UIBezierPath(roundedRect: cell.bounds, byRoundingCorners: [.bottomLeft, .TopLeft], cornerRadii: CGSize(width: 10, height: 10))
let ml = CAShapeLayer()
ml.frame = self.bounds
ml.path = mp.CGPath
self.layer.mask = ml

4

c'è una risposta più semplice e veloce che può funzionare a seconda delle tue esigenze e funziona anche con le ombre. puoi impostare maskToBounds sul superlayer su true e sfalsare i livelli secondari in modo che 2 dei loro angoli siano fuori dai limiti del superlayer, tagliando efficacemente gli angoli arrotondati su 2 lati.

Ovviamente questo funziona solo quando vuoi avere solo 2 angoli arrotondati sullo stesso lato e il contenuto del livello sembra lo stesso quando tagli alcuni pixel da un lato. funziona alla grande per avere grafici a barre arrotondati solo sul lato superiore.


3

Vedi questa domanda correlata . Dovrai disegnare il tuo rettangolo CGPathcon alcuni angoli arrotondati, aggiungere il CGPathtuo CGContexte poi agganciarlo usando CGContextClip.

Puoi anche disegnare il rettangolo arrotondato con valori alfa su un'immagine e quindi usare quell'immagine per creare un nuovo livello che imposti come maskproprietà del tuo livello ( vedi la documentazione di Apple ).


2

Mezzo decennio di ritardo, ma penso che il modo in cui le persone lo fanno attualmente non è corretto al 100%. Molte persone hanno riscontrato il problema che l'utilizzo del metodo UIBezierPath + CAShapeLayer interferisce con il layout automatico, specialmente quando è impostato sullo Storyboard. Nessuna risposta va oltre questo, quindi ho deciso di aggiungere la mia.

C'è un modo molto semplice per aggirare questo problema: Disegna gli angoli arrotondati nella drawRect(rect: CGRect)funzione.

Ad esempio, se volessi gli angoli arrotondati in alto per una UIView, creerei una sottoclasse UIView e quindi utilizzerei quella sottoclasse laddove appropriato.

import UIKit

class TopRoundedView: UIView {

    override func drawRect(rect: CGRect) {
        super.drawRect(rect)

        var maskPath = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: UIRectCorner.TopLeft | UIRectCorner.TopRight, cornerRadii: CGSizeMake(5.0, 5.0))

        var maskLayer = CAShapeLayer()
        maskLayer.frame = self.bounds
        maskLayer.path = maskPath.CGPath

        self.layer.mask = maskLayer
    }
}

Questo è il modo migliore per risolvere il problema e non ci vuole tempo per adattarlo.


1
Questo non è il modo giusto per risolvere il problema. Dovresti sovrascrivere solo drawRect(_:)se stai facendo un disegno personalizzato nella tua vista (ad esempio con Core Graphics), altrimenti anche un'implementazione vuota può influire sulle prestazioni. Anche la creazione di un nuovo livello maschera ogni volta non è necessaria. Tutto quello che devi fare è aggiornare la pathproprietà del livello maschera quando i limiti della vista cambiano, e il punto per farlo è all'interno di una sostituzione del layoutSubviews()metodo.
Stuart

2

Arrotondare solo alcuni angoli non funzionerà bene con il ridimensionamento automatico o il layout automatico.

Quindi un'altra opzione è usare normale cornerRadiuse nascondere gli angoli che non vuoi sotto un'altra vista o al di fuori dei suoi limiti di superview assicurandoti che sia impostato per ritagliare il suo contenuto.


1

Per aggiungere alla risposta e all'aggiunta , ho creato un semplice, riutilizzabile UIViewin Swift. A seconda del tuo caso d'uso, potresti voler apportare modifiche (evitare di creare oggetti su ogni layout ecc.), Ma volevo mantenerlo il più semplice possibile. L'estensione ti consente di applicarlo ad altre viste (es. UIImageView) Più facilmente se non ti piace la sottoclasse.

extension UIView {

    func roundCorners(_ roundedCorners: UIRectCorner, toRadius radius: CGFloat) {
        roundCorners(roundedCorners, toRadii: CGSize(width: radius, height: radius))
    }

    func roundCorners(_ roundedCorners: UIRectCorner, toRadii cornerRadii: CGSize) {
        let maskBezierPath = UIBezierPath(
            roundedRect: bounds,
            byRoundingCorners: roundedCorners,
            cornerRadii: cornerRadii)
        let maskShapeLayer = CAShapeLayer()
        maskShapeLayer.frame = bounds
        maskShapeLayer.path = maskBezierPath.cgPath
        layer.mask = maskShapeLayer
    }
}

class RoundedCornerView: UIView {

    var roundedCorners: UIRectCorner = UIRectCorner.allCorners
    var roundedCornerRadii: CGSize = CGSize(width: 10.0, height: 10.0)

    override func layoutSubviews() {
        super.layoutSubviews()
        roundCorners(roundedCorners, toRadii: roundedCornerRadii)
    }
}

Ecco come lo applicheresti a UIViewController:

class MyViewController: UIViewController {

    private var _view: RoundedCornerView {
        return view as! RoundedCornerView
    }

    override func loadView() {
        view = RoundedCornerView()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        _view.roundedCorners = [.topLeft, .topRight]
        _view.roundedCornerRadii = CGSize(width: 10.0, height: 10.0)
    }
}

0

Concludendo la risposta di Stuart, puoi avere il metodo dell'angolo di arrotondamento come segue:

@implementation UIView (RoundCorners)

- (void)applyRoundCorners:(UIRectCorner)corners radius:(CGFloat)radius {
    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.bounds byRoundingCorners:corners cornerRadii:CGSizeMake(radius, radius)];

    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    maskLayer.frame = self.bounds;
    maskLayer.path = maskPath.CGPath;

    self.layer.mask = maskLayer;
}

@end

Quindi, per applicare l'angolo di arrotondamento, fai semplicemente:

[self.imageView applyRoundCorners:UIRectCornerTopRight|UIRectCornerTopLeft radius:10];

0

Suggerirei di definire una maschera di livello. La maschera stessa dovrebbe essere un CAShapeLayeroggetto con un percorso dedicato. Puoi utilizzare la prossima estensione UIView (Swift 4.2):

extension UIView {
    func round(corners: UIRectCorner, with radius: CGFloat) {
        let maskLayer = CAShapeLayer()
        maskLayer.frame = bounds
        maskLayer.path = UIBezierPath(
            roundedRect: bounds,
            byRoundingCorners: corners,
            cornerRadii: CGSize(width: radius, height: radius)
        ).cgPath
        layer.mask = maskLayer
   }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.