Zaokrąglony UIView przy użyciu CALayers - tylko niektóre rogi - jak?

121

W mojej aplikacji - są cztery przyciski nazwane następująco:

  • U góry z lewej
  • Na dole po lewej
  • W prawym górnym rogu
  • Prawy dolny

Nad przyciskami znajduje się widok obrazu (lub UIView).

Teraz załóżmy, że użytkownik naciska lewy górny przycisk. Powyższy obraz / widok powinien być zaokrąglony w tym konkretnym rogu.

Mam pewne trudności z zastosowaniem zaokrąglonych rogów do UIView.

W tej chwili używam następującego kodu, aby zastosować zaokrąglone rogi do każdego widoku:

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

Powyższy kod stosuje zaokrąglenie do każdego z rogów dostarczonego widoku. Zamiast tego chciałem po prostu zastosować zaokrąglenie do wybranych narożników, takich jak - górny / górny + lewy / dolny + prawy itp.

Czy to możliwe? W jaki sposób?

Sagar R. Kothari
źródło

Odpowiedzi:

44

Skorzystałem z odpowiedzi w artykule Jak utworzyć zaokrąglony UILabel na iPhonie? i kod z Jak wygląda zaokrąglony prostokąt z przezroczystością na iPhonie? aby stworzyć ten kod.

Potem zdałem sobie sprawę, że odpowiedziałem na złe pytanie (podałem zaokrąglony UILabel zamiast UIImage), więc użyłem tego kodu, aby to zmienić:

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

Utwórz projekt na telefon iPhone za pomocą szablonu widoku. W kontrolerze widoku dodaj to:

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

MyViewto tylko UIImageViewpodklasa:

@interface MyView : UIImageView
{
}

Nigdy wcześniej nie korzystałem z kontekstów graficznych, ale udało mi się utykać razem ten kod. Brakuje kodu dla dwóch rogów. Jeśli czytasz kod, możesz zobaczyć, jak to zaimplementowałem (usuwając niektóre CGContextAddArcwywołania i niektóre wartości promienia w kodzie. Kod dla wszystkich rogów jest tam, więc użyj go jako punktu wyjścia i usuń części, które tworzą rogi, których nie potrzebujesz. Pamiętaj, że możesz również tworzyć prostokąty z 2 lub 3 zaokrąglonymi rogami, jeśli chcesz.

Kod nie jest doskonały, ale jestem pewien, że możesz go trochę uporządkować.

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

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

Nie zapominaj, że musisz mieć tam strukturę QuartzCore, aby to działało.

Nevan King
źródło
Nie działa zgodnie z oczekiwaniami w przypadku widoków, których rozmiar może się zmienić. Przykład: UILabel. Ustawienie ścieżki maski, a następnie zmiana rozmiaru poprzez nadanie jej tekstu spowoduje zachowanie starej maski. Będzie potrzebować podklasy i przeciążenia drawRect.
user3099609
358

Począwszy od iOS 3.2, możesz użyć funkcji UIBezierPaths, aby utworzyć gotowy prostokąt z zaokrągleniem (gdzie tylko określone rogi są zaokrąglane). Możesz następnie użyć tego jako ścieżki a CAShapeLayeri użyć tego jako maski dla warstwy widoku:

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

I to wszystko - bez majstrowania przy ręcznym definiowaniu kształtów w Core Graphics, bez tworzenia maskujących obrazów w Photoshopie. Warstwa nie wymaga nawet unieważnienia. Zastosowanie zaokrąglonego narożnika lub zmiana na nowy jest tak proste, jak zdefiniowanie nowego UIBezierPathi użycie go CGPathjako ścieżki warstwy maski. cornersParametrem bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:sposobu jest maską bitów, a więc wiele rogi mogą być zaokrąglone przez Oring je razem.


EDYCJA: dodawanie cienia

Jeśli chcesz dodać do tego cień, potrzeba trochę więcej pracy.

Ponieważ " imageView.layer.mask = maskLayer" nakłada maskę, zwykle cień nie będzie widoczny poza nią. Sztuczka polega na użyciu przezroczystego widoku, a następnie dodaniu dwóch podwarstw CALayerdo warstwy widoku: shadowLayeri roundedLayer. Oba muszą korzystać z UIBezierPath. Obraz zostanie dodany jako zawartość 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
źródło
1
Fajnie, to mi bardzo pomogło na zaznaczonychBackgroundViews :-)
Luke47
W każdym razie, aby dodać cień do tego widoku po zaokrągleniu rogów? Próbowano wykonać następujące czynności bez powodzenia. `self.layer.masksToBounds = NIE; self.layer.shadowColor = [UIColor blackColor] .CGColor; self.layer.shadowRadius = 3; self.layer.shadowOffset = CGSizeMake (-3, 3); self.layer.shadowOpacity = 0,8; self.layer.shouldRasterize = TAK; `
Chris Wagner
1
@ChrisWagner: Zobacz moją edycję dotyczącą zastosowania cienia do zaokrąglonego widoku.
Stuart
@StuDev doskonały! Poszedłem z podziałem na widok cienia, ale jest o wiele ładniejszy! Nie pomyślałem o dodaniu takich warstw podrzędnych. Dzięki!
Chris Wagner
Dlaczego mój obraz staje się biały, a kiedy przewijam UITableView i komórka jest odtwarzana - to działa! Czemu?
Fernando Redondo
18

Użyłem tego kodu w wielu miejscach w moim kodzie i działa on w 100% poprawnie. Możesz zmienić dowolny corder, zmieniając jedną właściwość „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];
itsaboutcode
źródło
Użycie tego rozwiązania na UITableView-subviews (np. UITableViewCellS lub UITableViewHeaderFooterViews) skutkuje złą płynnością podczas przewijania . Innym podejściem do tego zastosowania jest to rozwiązanie z lepszą wydajnością (dodaje cornerRadius do wszystkich rogów). Aby „pokazać” tylko określone rogi zaokrąglone (jak górne i dolne prawe rogi) dodałem widok podrzędny z ujemną wartością frame.origin.xi przypisałem cornerRadius do jego warstwy. Jeśli ktoś znalazł lepsze rozwiązanie, jestem zainteresowany.
anneblue
11

W iOS 11 możemy teraz zaokrąglić tylko niektóre rogi

let view = UIView()

view.clipsToBounds = true
view.layer.cornerRadius = 8
view.layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMinXMaxYCorner]
onmyway133
źródło
8

Rozszerzenie CALayer ze składnią 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
    }
}

Może być używany jak:

let layer: CALayer = yourView.layer
layer.round(roundedRect: yourView.bounds, byRoundingCorners: [.bottomLeft, .topLeft], cornerRadii: CGSize(width: 5, height: 5))
Politycznie niezależny
źródło
5

Przykład Stuarta do zaokrąglania określonych rogów sprawdza się świetnie. Jeśli chcesz zaokrąglić wiele rogów, takich jak lewy górny i prawy, oto jak to zrobić

// 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
źródło
Użycie tego rozwiązania na UITableView-subviews (np. UITableViewCellS lub UITableViewHeaderFooterViews) skutkuje złą płynnością podczas przewijania . Innym podejściem do tego zastosowania jest to rozwiązanie z lepszą wydajnością (dodaje cornerRadius do wszystkich rogów). Aby „pokazać” tylko określone rogi zaokrąglone (jak górne i dolne prawe rogi) dodałem widok podrzędny z ujemną wartością frame.origin.xi przypisałem cornerRadius do jego warstwy. Jeśli ktoś znalazł lepsze rozwiązanie, jestem zainteresowany.
anneblue
5

Dzięki za udostępnienie. Tutaj chciałbym podzielić się rozwiązaniem na swift 2.0, aby uzyskać dalsze informacje na ten temat. (w celu zgodności z protokołem 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
Jog Dan
źródło
4

istnieje łatwiejsza i szybsza odpowiedź, która może działać w zależności od Twoich potrzeb, a także działa z cieniami. możesz ustawić maskToBounds w superwarstwie na true i przesunąć warstwy potomne tak, aby 2 z ich rogów znajdowały się poza granicami superwarstwy, skutecznie odcinając zaokrąglone rogi z 2 stron.

oczywiście działa to tylko wtedy, gdy chcesz mieć tylko 2 zaokrąglone rogi po tej samej stronie, a zawartość warstwy wygląda tak samo, gdy odetniesz kilka pikseli z jednej strony. doskonale sprawdza się w przypadku wykresów słupkowych zaokrąglonych tylko na górze.

user1259710
źródło
3

Zobacz to powiązane pytanie . Będziesz musiał narysować własny prostokąt CGPathz kilkoma zaokrąglonymi rogami, dodać CGPathdo swojego, CGContexta następnie przyciąć do niego za pomocą CGContextClip.

Możesz również narysować zaokrąglony prostokąt z wartościami alfa na obrazie, a następnie użyć tego obrazu do utworzenia nowej warstwy, którą ustawisz jako maskwłaściwość warstwy ( zobacz dokumentację Apple ).

MrMage
źródło
2

Pół dekady późno, ale myślę, że obecny sposób, w jaki ludzie to robią, nie jest w 100% prawidłowy. Wiele osób miało problem, że użycie metody UIBezierPath + CAShapeLayer koliduje z automatycznym układem, zwłaszcza gdy jest ustawiony na Storyboard. Żadne odpowiedzi nie przekraczają tego, więc zdecydowałem się dodać własne.

Jest bardzo łatwy sposób na obejście tego: Narysuj zaokrąglone rogi w drawRect(rect: CGRect)funkcji.

Na przykład, jeśli chciałbym mieć zaokrąglone górne rogi dla UIView, podklasę podklasę UIView użyłbym wtedy, gdy jest to konieczne.

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

To najlepszy sposób na pokonanie problemu i dostosowanie się do niego nie zajmuje dużo czasu.

David
źródło
1
To nie jest właściwy sposób rozwiązania problemu. Powinieneś nadpisać tylko drawRect(_:)wtedy, gdy robisz niestandardowy rysunek w swoim widoku (np. Z Core Graphics), w przeciwnym razie nawet pusta implementacja może wpłynąć na wydajność. Tworzenie nowej warstwy maski za każdym razem również nie jest konieczne. Wszystko, co musisz zrobić, to zaktualizować właściwość warstwy maski, pathgdy zmienią się granice widoku, a miejsce do zrobienia tego znajduje się w zastąpieniu layoutSubviews()metody.
Stuart
2

Zaokrąglanie tylko niektórych rogów nie będzie dobrze działać z automatyczną zmianą rozmiaru lub automatycznym układem.

Więc inną opcją jest użycie zwykłych cornerRadiusi ukrycie rogów, których nie chcesz, pod innym widokiem lub poza jego granicami nadzoru, upewniając się, że jest ustawiona na przycinanie zawartości.

Rivera
źródło
1

Aby dodać do odpowiedzi i uzupełnienia , stworzyłem prosty, wielokrotnego użytku UIVieww Swift. W zależności od przypadku użycia możesz chcieć wprowadzić modyfikacje (unikaj tworzenia obiektów na każdym układzie itp.), Ale chciałem, aby było to tak proste, jak to tylko możliwe. Rozszerzenie umożliwia UIImageViewłatwiejsze zastosowanie tego do innych widoków (np. ), Jeśli nie lubisz tworzenia podklas.

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

Oto jak można to zastosować do 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
źródło
0

Podsumowując odpowiedź Stuarta, możesz zastosować metodę zaokrąglania narożników w następujący sposób:

@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

Aby zastosować zaokrąglenie narożnika, po prostu wykonaj:

[self.imageView applyRoundCorners:UIRectCornerTopRight|UIRectCornerTopLeft radius:10];
Willy
źródło
0

Sugerowałbym zdefiniowanie maski warstwy. Sama maska ​​powinna być CAShapeLayerobiektem z dedykowaną ścieżką. Możesz użyć następnego rozszerzenia 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
   }
}
sgl0v
źródło