Abgerundete UIView mit CALayers - nur einige Ecken - Wie?

121

In meiner Anwendung gibt es vier Schaltflächen, die wie folgt benannt sind:

  • Oben links
  • Unten links
  • Oben rechts
  • Unten rechts

Über den Schaltflächen befindet sich eine Bildansicht (oder eine UIView).

Angenommen, ein Benutzer tippt auf die Schaltfläche oben links. Das obige Bild / die obige Ansicht sollte an dieser bestimmten Ecke abgerundet sein.

Ich habe einige Schwierigkeiten, abgerundete Ecken auf das UIView anzuwenden.

Im Moment verwende ich den folgenden Code, um die abgerundeten Ecken auf jede Ansicht anzuwenden:

    // 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]];

Der obige Code wendet die Rundheit auf jede Ecke der bereitgestellten Ansicht an. Stattdessen wollte ich nur Rundheit auf ausgewählte Ecken wie - oben / oben + links / unten + rechts usw. anwenden.

Ist es möglich? Wie?

Sagar R. Kothari
quelle

Antworten:

44

Ich habe die Antwort unter Wie erstelle ich ein UILabel mit runden Ecken auf dem iPhone? und der Code von Wie wird eine abgerundete rechte Ansicht mit Transparenz auf dem iPhone erstellt? um diesen Code zu machen.

Dann wurde mir klar, dass ich die falsche Frage beantwortet hatte (gab ein abgerundetes UILabel anstelle von UIImage), also habe ich diesen Code verwendet, um ihn zu ändern:

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

Erstellen Sie ein iPhone-Projekt mit der Ansichtsvorlage. Fügen Sie im Ansichts-Controller Folgendes hinzu:

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

MyViewist nur eine UIImageViewUnterklasse:

@interface MyView : UIImageView
{
}

Ich hatte noch nie Grafikkontexte verwendet, aber ich habe es geschafft, diesen Code zusammen zu humpeln. Es fehlt der Code für zwei der Ecken. Wenn Sie den Code lesen, können Sie sehen, wie ich dies implementiert habe (indem Sie einige der CGContextAddArcAufrufe und einige der Radiuswerte im Code löschen. Der Code für alle Ecken ist vorhanden. Verwenden Sie ihn also als Ausgangspunkt und löschen Sie den Code Teile, die Ecken erstellen, die Sie nicht benötigen. Beachten Sie, dass Sie bei Bedarf auch Rechtecke mit 2 oder 3 abgerundeten Ecken erstellen können.

Der Code ist nicht perfekt, aber ich bin sicher, Sie können ihn ein wenig aufräumen.

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

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

Vergessen Sie nicht, dass Sie das QuartzCore-Framework benötigen, damit dies funktioniert.

Nevan König
quelle
Funktioniert nicht wie erwartet mit Ansichten, deren Größe sich ändern kann. Beispiel: ein UILabel. Wenn Sie den Maskenpfad festlegen und dann die Größe ändern, indem Sie Text darauf setzen, bleibt die alte Maske erhalten. Benötigt eine Unterklasse und Überladung des drawRect.
user3099609
358

Ab iOS 3.2 können Sie die Funktionalität von UIBezierPaths verwenden, um ein sofort einsatzbereites abgerundetes Rechteck zu erstellen (wobei nur die von Ihnen angegebenen Ecken abgerundet sind). Sie können dies dann als Pfad von a CAShapeLayerund als Maske für die Ebene Ihrer Ansicht verwenden:

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

Und das war's - kein Herumspielen des manuellen Definierens von Formen in Core Graphics, kein Erstellen von Maskierungsbildern in Photoshop. Die Ebene muss nicht einmal ungültig gemacht werden. Das Anwenden der abgerundeten Ecke oder das Ändern einer neuen Ecke ist so einfach wie das Definieren einer neuen Ecke UIBezierPathund deren Verwendung CGPathals Pfad der Maskenebene. Der cornersParameter der bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:Methode ist eine Bitmaske, sodass mehrere Ecken durch ODER-Verknüpfung abgerundet werden können.


BEARBEITEN: Hinzufügen eines Schattens

Wenn Sie dem einen Schatten hinzufügen möchten, ist etwas mehr Arbeit erforderlich.

Da " imageView.layer.mask = maskLayer" eine Maske anwendet, wird ein Schatten normalerweise nicht außerhalb der Maske angezeigt. Der Trick besteht darin, eine transparente Ansicht zu verwenden und dann zwei Unterebenen CALayerzur Ebene der Ansicht hinzuzufügen : shadowLayerund roundedLayer. Beide müssen das nutzen UIBezierPath. Das Bild wird als Inhalt von hinzugefügt 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];
Stuart
quelle
1
Schön, das hat mir sehr geholfen bei den ausgewählten
BackgroundViews der
Wie auch immer, um dieser Ansicht einen Schlagschatten hinzuzufügen, nachdem Sie die Ecken abgerundet haben? Versuchte das Folgende ohne Erfolg. `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: Siehe meine Bearbeitung in Bezug auf das Anwenden eines Schattens auf die abgerundete Ansicht.
Stuart
@StuDev ausgezeichnet! Ich hatte eine Unteransicht für die Schattenansicht gewählt, aber das ist viel schöner! Ich habe nicht daran gedacht, solche Unterebenen hinzuzufügen. Vielen Dank!
Chris Wagner
Warum wird mein Bild weiß? Wenn ich dann durch die UITableView scrolle und die Zelle neu erstellt wird, funktioniert es! Warum?
Fernando Redondo
18

Ich habe diesen Code an vielen Stellen in meinem Code verwendet und er funktioniert zu 100% korrekt. Sie können jeden Corder ändern, indem Sie eine Eigenschaft "byRoundingCorners: UIRectCornerBottomLeft" ändern.

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];
itsaboutcode
quelle
Wenn Sie diese Lösung in UITableViewUnteransichten (z. B. UITableViewCells oder UITableViewHeaderFooterViews) verwenden, führt dies zu einer schlechten Glätte beim Scrollen . Ein anderer Ansatz für diese Verwendung ist diese Lösung mit einer besseren Leistung (sie fügt allen Ecken einen CornerRadius hinzu). Um nur bestimmte abgerundete Ecken anzuzeigen (wie die oberen und unteren rechten Ecken), habe ich eine Unteransicht mit einem negativen Wert hinzugefügt frame.origin.xund der Ebene denRadius zugewiesen. Wenn jemand eine bessere Lösung gefunden hat, bin ich interessiert.
Anneblue
11

In iOS 11 können wir jetzt nur einige Ecken abrunden

let view = UIView()

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

CALayer Verlängerung mit Swift 3+ Syntax

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

Es kann verwendet werden wie:

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

Stuarts Beispiel zum Abrunden bestimmter Ecken funktioniert hervorragend. Wenn Sie mehrere Ecken wie oben links und rechts abrunden möchten, gehen Sie wie folgt vor

// 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; 
aZtraL-EnForceR
quelle
Wenn Sie diese Lösung in UITableViewUnteransichten (z. B. UITableViewCells oder UITableViewHeaderFooterViews) verwenden, führt dies zu einer schlechten Glätte beim Scrollen . Ein anderer Ansatz für diese Verwendung ist diese Lösung mit einer besseren Leistung (sie fügt allen Ecken einen CornerRadius hinzu). Um nur bestimmte abgerundete Ecken anzuzeigen (wie die oberen und unteren rechten Ecken), habe ich eine Unteransicht mit einem negativen Wert hinzugefügt frame.origin.xund der Ebene denRadius zugewiesen. Wenn jemand eine bessere Lösung gefunden hat, bin ich interessiert.
Anneblue
5

Danke für das Teilen. Hier möchte ich die Lösung auf Swift 2.0 zur weiteren Bezugnahme auf dieses Problem teilen. (um das UIRectCorner-Protokoll anzupassen)

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
Joggen Sie Dan
quelle
4

Es gibt eine einfachere und schnellere Antwort, die je nach Ihren Anforderungen funktioniert und auch mit Schatten funktioniert. Sie können maskToBounds auf der Superlayer auf true setzen und die untergeordneten Ebenen so versetzen, dass 2 ihrer Ecken außerhalb der Superlayer-Grenzen liegen, wodurch die abgerundeten Ecken auf 2 Seiten effektiv abgeschnitten werden.

Dies funktioniert natürlich nur, wenn Sie nur zwei abgerundete Ecken auf derselben Seite haben möchten und der Inhalt der Ebene gleich aussieht, wenn Sie einige Pixel von einer Seite abschneiden. funktioniert hervorragend, wenn Balkendiagramme nur auf der Oberseite gerundet sind.

user1259710
quelle
3

Siehe diese verwandte Frage . Sie müssen Ihr eigenes Rechteck CGPathmit einigen abgerundeten Ecken zu einem zeichnen , das CGPathzu Ihrem hinzufügen CGContextund dann mit verwendenCGContextClip .

Sie können auch das abgerundete Rechteck mit Alpha-Werten in ein Bild zeichnen und dann mit diesem Bild eine neue Ebene erstellen, die Sie als maskEigenschaft Ihrer Ebene festlegen ( siehe Apple-Dokumentation ).

MrMage
quelle
2

Ein halbes Jahrzehnt zu spät, aber ich denke, die derzeitige Vorgehensweise ist nicht 100% richtig. Viele Leute hatten das Problem, dass die Verwendung der UIBezierPath + CAShapeLayer-Methode das automatische Layout beeinträchtigt, insbesondere wenn es im Storyboard festgelegt ist. Da keine Antworten darauf eingehen, habe ich beschlossen, meine eigenen hinzuzufügen.

Es gibt eine sehr einfache Möglichkeit, dies zu umgehen: Zeichnen Sie die abgerundeten Ecken in die drawRect(rect: CGRect) Funktion.

Wenn ich beispielsweise obere abgerundete Ecken für eine UIView haben möchte, würde ich UIView unterordnen und diese Unterklasse gegebenenfalls verwenden.

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

Dies ist der beste Weg, um das Problem zu lösen, und es dauert überhaupt keine Zeit, sich daran zu gewöhnen.

David
quelle
1
Dies ist nicht der richtige Weg, um das Problem zu lösen. Sie sollten nur überschreiben, drawRect(_:)wenn Sie in Ihrer Ansicht benutzerdefinierte Zeichnungen erstellen (z. B. mit Core Graphics). Andernfalls kann selbst eine leere Implementierung die Leistung beeinträchtigen. Es ist auch nicht erforderlich, jedes Mal eine neue Maskenebene zu erstellen. Alles, was Sie tatsächlich tun müssen, ist, die pathEigenschaft der Maskenebene zu aktualisieren, wenn sich die Grenzen der Ansicht ändern, und der Ort, an dem dies getan werden soll, liegt innerhalb einer Überschreibung der layoutSubviews()Methode.
Stuart
2

Das Abrunden nur einiger Ecken eignet sich nicht für die automatische Größenänderung oder das automatische Layout.

Eine andere Möglichkeit besteht darin, regulär zu verwenden cornerRadiusund die nicht gewünschten Ecken unter einer anderen Ansicht oder außerhalb der Grenzen der Übersicht auszublenden, um sicherzustellen, dass der Inhalt abgeschnitten ist.

Rivera
quelle
1

Um die Antwort und die Hinzufügung zu ergänzen , habe ich eine einfache, UIViewin Swift wiederverwendbare Datei erstellt. Abhängig von Ihrem Anwendungsfall möchten Sie möglicherweise Änderungen vornehmen (vermeiden Sie das Erstellen von Objekten in jedem Layout usw.), aber ich wollte es so einfach wie möglich halten. Mit der Erweiterung können Sie dies UIImageVieweinfacher auf andere Ansichten (z. B.) anwenden, wenn Sie keine Unterklassen mögen.

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

So würden Sie es auf Folgendes anwenden 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)
    }
}
kgaidis
quelle
0

Wenn Sie die Antwort von Stuart zusammenfassen, können Sie die folgende Methode anwenden:

@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

Um eine abgerundete Ecke anzuwenden, tun Sie einfach:

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

Ich würde vorschlagen, die Maske einer Ebene zu definieren. Die Maske selbst sollte ein CAShapeLayerObjekt mit einem dedizierten Pfad sein. Sie können die nächste UIView-Erweiterung (Swift 4.2) verwenden:

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
   }
}
sgl0v
quelle