UIView arrondi utilisant CALayers - seulement quelques coins - Comment?

121

Dans mon application, il y a quatre boutons nommés comme suit:

  • En haut à gauche
  • En bas à gauche
  • En haut à droite
  • En bas à droite

Au-dessus des boutons, il y a une vue d'image (ou une vue UIV).

Maintenant, supposons qu'un utilisateur appuie sur le bouton en haut à gauche. L'image / vue ci-dessus doit être arrondie à ce coin particulier.

J'ai des difficultés à appliquer des coins arrondis à l'UIView.

En ce moment, j'utilise le code suivant pour appliquer les coins arrondis à chaque vue:

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

Le code ci-dessus applique l'arrondi à chacun des coins de la vue fournie. Au lieu de cela, je voulais juste appliquer l'arrondi aux coins sélectionnés comme - haut / haut + gauche / bas + droite etc.

C'est possible? Comment?

Sagar R. Kothari
la source

Réponses:

44

J'ai utilisé la réponse sur Comment créer un UILabel à coins arrondis sur l'iPhone? et le code de Comment une vue rectiligne arrondie avec transparence est-elle réalisée sur iPhone? pour faire ce code.

Ensuite, j'ai réalisé que j'avais répondu à la mauvaise question (j'ai donné un UILabel arrondi au lieu de UIImage), alors j'ai utilisé ce code pour le changer:

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

Créez un projet iPhone avec le modèle View. Dans le contrôleur de vue, ajoutez ceci:

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

MyViewest juste une UIImageViewsous - classe:

@interface MyView : UIImageView
{
}

Je n'avais jamais utilisé de contextes graphiques auparavant, mais j'ai réussi à entraver ce code. Il manque le code pour deux des coins. Si vous lisez le code, vous pouvez voir comment j'ai implémenté cela (en supprimant certains des CGContextAddArcappels et en supprimant certaines des valeurs de rayon dans le code. Le code pour tous les coins est là, alors utilisez-le comme point de départ et supprimez le des pièces qui créent des coins dont vous n'avez pas besoin. Notez que vous pouvez également créer des rectangles avec 2 ou 3 coins arrondis si vous le souhaitez.

Le code n'est pas parfait, mais je suis sûr que vous pouvez le ranger un peu.

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

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

N'oubliez pas que vous aurez besoin du framework QuartzCore pour que cela fonctionne.

roi nevan
la source
Ne fonctionne pas comme prévu avec les vues dont la taille peut changer. Exemple: un UILabel. La définition du chemin du masque, puis la modification de la taille en définissant du texte, conservera l'ancien masque. Aura besoin de sous-classer et de surcharger le drawRect.
user3099609
358

À partir d'iOS 3.2, vous pouvez utiliser la fonctionnalité de UIBezierPaths pour créer un rectangle arrondi prêt à l'emploi (où seuls les coins que vous spécifiez sont arrondis). Vous pouvez ensuite l'utiliser comme chemin d'un CAShapeLayer, et l'utiliser comme masque pour le calque de votre vue:

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

Et c'est tout - pas de problème de définition manuelle des formes dans Core Graphics, pas de création d'images de masquage dans Photoshop. La couche n'a même pas besoin d'être invalidée. L'application du coin arrondi ou le passage à un nouveau coin est aussi simple que de définir un nouveau coin UIBezierPathet de l'utiliser CGPathcomme tracé du calque de masque. Le cornersparamètre de la bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:méthode est un masque de bits, et donc plusieurs coins peuvent être arrondis en les OR en les associant.


EDIT: Ajout d'une ombre

Si vous cherchez à ajouter une ombre à cela, un peu plus de travail est nécessaire.

Parce que " imageView.layer.mask = maskLayer" applique un masque, une ombre n'apparaîtra généralement pas en dehors de celui-ci. L'astuce consiste à utiliser une vue transparente, puis à ajouter deux sous-couches CALayerau calque de la vue: shadowLayeret roundedLayer. Les deux doivent utiliser le UIBezierPath. L'image est ajoutée en tant que contenu de 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
la source
1
Nice one, cela m'a beaucoup aidé sur les cellules UITableView sélectionnéesBackgroundViews :-)
Luke47
Quoi qu'il en soit, ajouter une ombre portée à cette vue après avoir arrondi les coins? Tentative de ce qui suit sans succès. `self.layer.masksToBounds = NON; self.layer.shadowColor = [UIColor blackColor] .CGColor; self.layer.shadowRadius = 3; self.layer.shadowOffset = CGSizeMake (-3, 3); self.layer.shadowOpacity = 0,8; self.layer.shouldRasterize = OUI; `
Chris Wagner
1
@ChrisWagner: Voir ma modification, en ce qui concerne l'application d'une ombre à la vue arrondie.
Stuart
@StuDev excellent! J'étais allé avec une sous-vue pour la vue d'ombre mais c'est beaucoup plus agréable! Je n'ai pas pensé à ajouter des sous-couches comme celle-ci. Merci!
Chris Wagner
Pourquoi mon image devient-elle blanche, alors quand je fais défiler l'UITableView et que la cellule est recréée - cela fonctionne! Pourquoi?
Fernando Redondo
18

J'ai utilisé ce code à de nombreux endroits dans mon code et il fonctionne à 100% correctement. Vous pouvez changer n'importe quel corder en changeant une propriété "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
la source
Lorsque vous utilisez cette solution sur des sous- UITableViewvues (par exemple, UITableViewCells ou UITableViewHeaderFooterViews), il en résulte une mauvaise fluidité lors du défilement . Une autre approche pour cette utilisation est cette solution avec une meilleure performance (elle ajoute un cornerRadius à tous les coins). Pour `` afficher '' uniquement des coins spécifiques arrondis (comme les coins supérieurs et inférieurs droits), j'ai ajouté une sous-vue avec une valeur négative sur son frame.origin.xet assigné le cornerRadius à son calque. Si quelqu'un a trouvé une meilleure solution, cela m'intéresse.
anneblue
11

Dans iOS 11, nous ne pouvons désormais arrondir que certains coins

let view = UIView()

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

Extension CALayer avec syntaxe 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
    }
}

Il peut être utilisé comme:

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

L'exemple Stuarts pour arrondir des coins spécifiques fonctionne très bien. Si vous voulez arrondir plusieurs coins comme en haut à gauche et à droite, voici comment le faire

// 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
la source
Lorsque vous utilisez cette solution sur des sous- UITableViewvues (par exemple, UITableViewCells ou UITableViewHeaderFooterViews), il en résulte une mauvaise fluidité lors du défilement . Une autre approche pour cette utilisation est cette solution avec une meilleure performance (elle ajoute un cornerRadius à tous les coins). Pour `` afficher '' uniquement des coins spécifiques arrondis (comme les coins supérieurs et inférieurs droits), j'ai ajouté une sous-vue avec une valeur négative sur son frame.origin.xet assigné le cornerRadius à son calque. Si quelqu'un a trouvé une meilleure solution, cela m'intéresse.
anneblue
5

Merci d'avoir partagé. Ici, je voudrais partager la solution sur swift 2.0 pour plus de référence sur ce problème. (pour se conformer au protocole 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
la source
4

il existe une réponse plus simple et plus rapide qui peut fonctionner en fonction de vos besoins et qui fonctionne également avec les ombres. vous pouvez définir maskToBounds sur le super-calque sur true et décaler les calques enfants de sorte que 2 de leurs coins soient en dehors des limites du super-calque, coupant ainsi efficacement les coins arrondis sur 2 côtés.

bien sûr, cela ne fonctionne que lorsque vous souhaitez avoir seulement 2 coins arrondis du même côté et que le contenu du calque est le même lorsque vous coupez quelques pixels d'un côté. fonctionne très bien pour avoir des graphiques à barres arrondis uniquement sur la face supérieure.

user1259710
la source
3

Voir cette question connexe . Vous devez dessiner votre propre rectangle à un CGPathavec des coins arrondis, ajoutez CGPathà votre CGContextpuis à couper l'aide CGContextClip.

Vous pouvez également dessiner le rect arrondi avec des valeurs alpha sur une image, puis utiliser cette image pour créer un nouveau calque que vous définissez comme maskpropriété de votre calque ( voir la documentation d'Apple ).

MrMage
la source
2

Une demi-décennie de retard, mais je pense que la façon actuelle dont les gens font cela n'est pas correcte à 100%. Beaucoup de gens ont eu le problème que l'utilisation de la méthode UIBezierPath + CAShapeLayer interfère avec la mise en page automatique, en particulier lorsqu'elle est définie sur le Storyboard. Aucune réponse ne va plus loin, j'ai donc décidé d'ajouter la mienne.

Il existe un moyen très simple de contourner cela: Dessinez les coins arrondis dans la drawRect(rect: CGRect)fonction.

Par exemple, si je voulais des coins arrondis supérieurs pour un UIView, je sous-classerais UIView puis utiliserais cette sous-classe le cas échéant.

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

C'est le meilleur moyen de surmonter le problème et ne prend pas du tout de temps pour s'y adapter.

David
la source
1
Ce n'est pas la bonne façon de résoudre le problème. Vous ne devriez remplacer que drawRect(_:)si vous faites un dessin personnalisé dans votre vue (par exemple avec Core Graphics), sinon même une implémentation vide peut avoir un impact sur les performances. La création d'un nouveau calque de masque à chaque fois est également inutile. Tout ce que vous avez à faire est de mettre à jour la pathpropriété du calque de masque lorsque les limites de la vue changent, et l'endroit pour le faire est dans un remplacement de la layoutSubviews()méthode.
Stuart
2

Arrondir uniquement certains coins ne sera pas agréable avec le redimensionnement automatique ou la mise en page automatique.

Une autre option consiste donc à utiliser normal cornerRadiuset à masquer les coins que vous ne voulez pas sous une autre vue ou en dehors de ses limites de supervision en vous assurant qu'il est configuré pour découper son contenu.

Rivera
la source
1

Pour ajouter à la réponse et à l' ajout , j'ai créé un simple, réutilisable UIViewdans Swift. En fonction de votre cas d'utilisation, vous voudrez peut-être apporter des modifications (évitez de créer des objets sur chaque mise en page, etc.), mais je voulais que cela reste aussi simple que possible. L'extension vous permet de l'appliquer à d'autres vues (ex. UIImageView) Plus facilement si vous n'aimez pas le sous-classement.

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

Voici comment vous l'appliqueriez à un 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
la source
0

Pour résumer la réponse de Stuart, vous pouvez avoir la méthode des coins arrondis comme suit:

@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

Donc, pour appliquer le coin arrondi, il vous suffit de faire:

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

Je suggère de définir le masque d'un calque. Le masque lui-même doit être un CAShapeLayerobjet avec un chemin dédié. Vous pouvez utiliser la prochaine extension 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
la source