web-dev-qa-db-fra.com

Deux coins de rue dans UIView

Il y a quelques instants, j'ai posté une question à propos de arrondir les angles d'une vue , et j'ai obtenu une excellente réponse, mais j'ai du mal à la mettre en œuvre. Voici ma méthode drawRect:

- (void)drawRect:(CGRect)rect {
    //[super drawRect:rect]; <------Should I uncomment this?
    int radius = 5;
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextBeginPath(context);
    CGContextAddArc(context, rect.Origin.x + radius, rect.Origin.y + rect.size.height - radius, radius, M_PI, M_PI / 2, 1);
    CGContextAddArc(context, rect.Origin.x + rect.size.width - radius, rect.Origin.y + rect.size.height - radius, radius, M_PI / 2, 0.0f, 1);
    CGContextClosePath(context);
    CGContextClip(context);
}

La méthode est appelée, mais ne semble pas affecter le résultat de la vue. Des idées pourquoi?

76
Jumhyn

CACornerMask introduit dans iOS 11, qui aide à définir les couches supérieure, supérieure, inférieure gauche et inférieure droite dans la vue. Vous trouverez ci-dessous un exemple à utiliser.

Ici, j'essaye d'arrondir seulement deux coins supérieurs:

myView.clipsToBounds = true
myView.layer.cornerRadius = 10
myView.layer.maskedCorners = [.layerMinXMinYCorner,.layerMaxXMinYCorner]

Merci d'avance.

FYI Ref: enter image description here

70
SachinVsSachin


autant que je sache, si vous devez également masquer les sous-vues, vous pouvez utiliser CALayer masquage. Il y a 2 façons de faire cela. Le premier est un peu plus élégant, le second est une solution de contournement :-) mais c'est aussi rapide. Les deux sont basés sur le masquage CALayer. J'ai utilisé les deux méthodes dans quelques projets l'année dernière, alors j'espère que vous pourrez trouver quelque chose d'utile.

Solution 1

Tout d'abord, j'ai créé cette fonction pour générer un masque d'image à la volée (UIImage) avec le coin arrondi dont j'ai besoin. Cette fonction nécessite essentiellement 5 paramètres: les limites de l’image et 4 rayons de coin (en haut à gauche, en haut à droite, en bas à gauche et en bas à droite).


static inline UIImage* MTDContextCreateRoundedMask( CGRect rect, CGFloat radius_tl, CGFloat radius_tr, CGFloat radius_bl, CGFloat radius_br ) {  

    CGContextRef context;
    CGColorSpaceRef colorSpace;

    colorSpace = CGColorSpaceCreateDeviceRGB();

    // create a bitmap graphics context the size of the image
    context = CGBitmapContextCreate( NULL, rect.size.width, rect.size.height, 8, 0, colorSpace, kCGImageAlphaPremultipliedLast );

    // free the rgb colorspace
    CGColorSpaceRelease(colorSpace);    

    if ( context == NULL ) {
        return NULL;
    }

    // cerate mask

    CGFloat minx = CGRectGetMinX( rect ), midx = CGRectGetMidX( rect ), maxx = CGRectGetMaxX( rect );
    CGFloat miny = CGRectGetMinY( rect ), midy = CGRectGetMidY( rect ), maxy = CGRectGetMaxY( rect );

    CGContextBeginPath( context );
    CGContextSetGrayFillColor( context, 1.0, 0.0 );
    CGContextAddRect( context, rect );
    CGContextClosePath( context );
    CGContextDrawPath( context, kCGPathFill );

    CGContextSetGrayFillColor( context, 1.0, 1.0 );
    CGContextBeginPath( context );
    CGContextMoveToPoint( context, minx, midy );
    CGContextAddArcToPoint( context, minx, miny, midx, miny, radius_bl );
    CGContextAddArcToPoint( context, maxx, miny, maxx, midy, radius_br );
    CGContextAddArcToPoint( context, maxx, maxy, midx, maxy, radius_tr );
    CGContextAddArcToPoint( context, minx, maxy, minx, midy, radius_tl );
    CGContextClosePath( context );
    CGContextDrawPath( context, kCGPathFill );

    // Create CGImageRef of the main view bitmap content, and then
    // release that bitmap context
    CGImageRef bitmapContext = CGBitmapContextCreateImage( context );
    CGContextRelease( context );

    // convert the finished resized image to a UIImage 
    UIImage *theImage = [UIImage imageWithCGImage:bitmapContext];
    // image is retained by the property setting above, so we can 
    // release the original
    CGImageRelease(bitmapContext);

    // return the image
    return theImage;
}  

Maintenant, vous avez juste besoin de quelques lignes de code. J'ai mis des éléments dans ma méthode viewController viewDidLoad car elle est plus rapide, mais vous pouvez également l'utiliser dans votre exemple personnalisé UIView avec la méthode layoutSubviews.



- (void)viewDidLoad {

    // Create the mask image you need calling the previous function
    UIImage *mask = MTDContextCreateRoundedMask( self.view.bounds, 50.0, 50.0, 0.0, 0.0 );
    // Create a new layer that will work as a mask
    CALayer *layerMask = [CALayer layer];
    layerMask.frame = self.view.bounds;       
    // Put the mask image as content of the layer
    layerMask.contents = (id)mask.CGImage;       
    // set the mask layer as mask of the view layer
    self.view.layer.mask = layerMask;              

    // Add a backaground color just to check if it works
    self.view.backgroundColor = [UIColor redColor];
    // Add a test view to verify the correct mask clipping
    UIView *testView = [[UIView alloc] initWithFrame:CGRectMake( 0.0, 0.0, 50.0, 50.0 )];
    testView.backgroundColor = [UIColor blueColor];
    [self.view addSubview:testView];
    [testView release];

    [super viewDidLoad];
}

Solution 2

Cette solution est un peu plus "sale". Pour l'essentiel, vous pouvez créer un calque de masque avec le coin arrondi dont vous avez besoin (tous les coins). Ensuite, vous devez augmenter la hauteur du calque de masque de la valeur du rayon de l'angle. De cette façon, les coins arrondis inférieurs sont masqués et vous ne pouvez voir que le coin arrondi supérieur. J'ai mis le code dans la méthode viewDidLoad car il est plus rapide, mais vous pouvez également l'utiliser dans votre UIView personnalisé avec la méthode layoutSubviews dans l'exemple.

  

- (void)viewDidLoad {

    // set the radius
    CGFloat radius = 50.0;
    // set the mask frame, and increase the height by the 
    // corner radius to hide bottom corners
    CGRect maskFrame = self.view.bounds;
    maskFrame.size.height += radius;
    // create the mask layer
    CALayer *maskLayer = [CALayer layer];
    maskLayer.cornerRadius = radius;
    maskLayer.backgroundColor = [UIColor blackColor].CGColor;
    maskLayer.frame = maskFrame;

    // set the mask
    self.view.layer.mask = maskLayer;

    // Add a backaground color just to check if it works
    self.view.backgroundColor = [UIColor redColor];
    // Add a test view to verify the correct mask clipping
    UIView *testView = [[UIView alloc] initWithFrame:CGRectMake( 0.0, 0.0, 50.0, 50.0 )];
    testView.backgroundColor = [UIColor blueColor];
    [self.view addSubview:testView];
    [testView release];

    [super viewDidLoad];
}

J'espère que cela t'aides. Ciao!

72
lomanf

En combinant les quelques réponses et commentaires, j'ai découvert que l'utilisation de UIBezierPath bezierPathWithRoundedRect et CAShapeLayer le moyen le plus simple et le plus direct. Cela peut ne pas être approprié pour les cas très complexes, mais pour les arrondis occasionnels, cela fonctionne rapidement et en douceur pour moi.

J'avais créé un assistant simplifié qui définit le coin approprié dans le masque:

-(void) setMaskTo:(UIView*)view byRoundingCorners:(UIRectCorner)corners
{
    UIBezierPath* rounded = [UIBezierPath bezierPathWithRoundedRect:view.bounds byRoundingCorners:corners cornerRadii:CGSizeMake(10.0, 10.0)];

    CAShapeLayer* shape = [[CAShapeLayer alloc] init];
    [shape setPath:rounded.CGPath];

    view.layer.mask = shape;
}

Pour l'utiliser, il vous suffit d'appeler avec l'enumération UIRectCorner appropriée, par exemple:

[self setMaskTo:self.photoView byRoundingCorners:UIRectCornerTopLeft|UIRectCornerBottomLeft];

Veuillez noter que pour moi, je l'utilise pour arrondir les angles d'une photo dans un groupe UITableViewCell groupé; le rayon de 10,0 fonctionne correctement pour moi, si nécessaire, il suffit de modifier la valeur comme il convient.

MODIFIER: remarquez juste une réponse déjà très semblable à celle-ci ( lien ). Vous pouvez toujours utiliser cette réponse comme une fonction pratique supplémentaire si nécessaire.


MODIFIER:
extension UIView {
    func maskByRoundingCorners(_ masks:UIRectCorner, withRadii radii:CGSize = CGSize(width: 10, height: 10)) {
        let rounded = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: masks, cornerRadii: radii)

        let shape = CAShapeLayer()
        shape.path = rounded.cgPath

        self.layer.mask = shape
    }
}

Pour l'utiliser, appelez simplement maskByRoundingCorner sur n'importe quel UIView:

view.maskByRoundingCorners([.topLeft, .bottomLeft])
68
P.L.

Je ne pouvais pas intégrer tout cela dans un commentaire à la réponse de @ lomanf. Donc, je l'ajoute comme une réponse.

Comme @lomanf l'a dit, vous devez ajouter un masque de calque pour empêcher les sous-couches de dessiner en dehors des limites de votre chemin. C'est beaucoup plus facile à faire maintenant, cependant. Tant que vous ciblez iOS 3.2 ou une version ultérieure, vous n'avez pas besoin de créer une image avec quartz et de la définir comme masque. Vous pouvez simplement créer un CAShapeLayer avec un UIBezierPath et l'utiliser comme masque.

De même, lorsque vous utilisez des masques de calque, assurez-vous que le calque que vous masquez ne fait pas partie d'une hiérarchie de calques lorsque vous ajoutez le masque. Sinon, le comportement n'est pas défini. Si votre vue est déjà dans la hiérarchie, vous devez la supprimer de sa vue d'ensemble, la masquer, puis la remettre à son emplacement d'origine.

CAShapeLayer *maskLayer = [CAShapeLayer layer];
UIBezierPath *roundedPath = 
  [UIBezierPath bezierPathWithRoundedRect:maskLayer.bounds
                        byRoundingCorners:UIRectCornerTopLeft |
                                          UIRectCornerBottomRight
                              cornerRadii:CGSizeMake(16.f, 16.f)];    
maskLayer.fillColor = [[UIColor whiteColor] CGColor];
maskLayer.backgroundColor = [[UIColor clearColor] CGColor];
maskLayer.path = [roundedPath CGPath];

//Don't add masks to layers already in the hierarchy!
UIView *superview = [self.view superview];
[self.view removeFromSuperview];
self.view.layer.mask = maskLayer;
[superview addSubview:self.view];

En raison du fonctionnement du rendu de Core Animation, le masquage est une opération relativement lente. Chaque masque nécessite une passe de rendu supplémentaire. Donc, utilisez les masques avec parcimonie.

L’un des avantages de cette approche est qu’il n’est plus nécessaire de créer un UIView personnalisé et de remplacer drawRect:. Cela devrait rendre votre code plus simple et peut-être même plus rapide.

56
Nathan Eror

J'ai pris l'exemple de Nathan et créé une catégorie sur UIView pour permettre à celui-ci d'adhérer aux principes DRY. Sans plus tarder:

UIView + Roundify.h

#import <UIKit/UIKit.h>

@interface UIView (Roundify)

-(void)addRoundedCorners:(UIRectCorner)corners withRadii:(CGSize)radii;
-(CALayer*)maskForRoundedCorners:(UIRectCorner)corners withRadii:(CGSize)radii;

@end

UIView + Roundify.m

#import "UIView+Roundify.h"

@implementation UIView (Roundify)

-(void)addRoundedCorners:(UIRectCorner)corners withRadii:(CGSize)radii {
    CALayer *tMaskLayer = [self maskForRoundedCorners:corners withRadii:radii];
    self.layer.mask = tMaskLayer;
}

-(CALayer*)maskForRoundedCorners:(UIRectCorner)corners withRadii:(CGSize)radii {
    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    maskLayer.frame = self.bounds;

    UIBezierPath *roundedPath = [UIBezierPath bezierPathWithRoundedRect:
        maskLayer.bounds byRoundingCorners:corners cornerRadii:radii];
    maskLayer.fillColor = [[UIColor whiteColor] CGColor];
    maskLayer.backgroundColor = [[UIColor clearColor] CGColor];
    maskLayer.path = [roundedPath CGPath];

    return maskLayer;
}

@end

Appeler:

[myView addRoundedCorners:UIRectCornerBottomLeft | UIRectCornerBottomRight
                withRadii:CGSizeMake(20.0f, 20.0f)];
30
FreeAsInBeer

Pour développer un peu la réponse de P.L, j'ai réécrit la méthode de manière à ne pas arrondir correctement certains objets tels que UIButton

- (void)setMaskTo:(id)sender byRoundingCorners:(UIRectCorner)corners withCornerRadii:(CGSize)radii
{
    // UIButton requires this
    [sender layer].cornerRadius = 0.0;

    UIBezierPath *shapePath = [UIBezierPath bezierPathWithRoundedRect:[sender bounds]
                                                byRoundingCorners:corners
                                                cornerRadii:radii];

    CAShapeLayer *newCornerLayer = [CAShapeLayer layer];
    newCornerLayer.frame = [sender bounds];
    newCornerLayer.path = shapePath.CGPath;
    [sender layer].mask = newCornerLayer;
}

Et appelez par

[self setMaskTo:self.continueButton byRoundingCorners:UIRectCornerBottomLeft|UIRectCornerBottomRight withCornerRadii:CGSizeMake(3.0, 3.0)];
24
Josh Valdivieso

Si vous voulez le faire dans Swift, vous pouvez utiliser l'extension de UIView. _. De cette manière, toutes les sous-classes pourront utiliser la méthode suivante:

import QuartzCore

extension UIView {
    func roundCorner(corners: UIRectCorner, radius: CGFloat) {
        let maskPath = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
        let maskLayer = CAShapeLayer()
        maskLayer.frame = bounds
        maskLayer.path = maskPath.CGPath
        layer.mask = maskLayer
    }
}    

Exemple d'utilisation:

self.anImageView.roundCorner(.topRight, radius: 10)
11
Kevin Delord

Toutes les solutions fournies permettent d'atteindre l'objectif. Mais, UIConstraints peut parfois exploser.

Par exemple, les coins inférieurs doivent être arrondis. Si la contrainte de hauteur ou d'espacement inférieur est définie sur UIView à arrondir, les extraits de code qui arrondissent les angles doivent être déplacés vers la méthode viewDidLayoutSubviews.

Mise en évidence:

UIBezierPath *maskPath = [UIBezierPath
    bezierPathWithRoundedRect:roundedView.bounds byRoundingCorners:
    (UIRectCornerTopRight | UIRectCornerBottomRight) cornerRadii:CGSizeMake(16.0, 16.0)];

L'extrait de code ci-dessus ne contournera le coin supérieur droit que si ce code est défini dans viewDidLoad. Car roundedView.bounds va changer après que les contraintes aient mis à jour le UIView.

2
hasan
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(5, 5, self.bounds.size.width-10, self.bounds.size.height-10)
                                               byRoundingCorners:UIRectCornerAllCorners
                                                     cornerRadii:CGSizeMake(12.0, 12.0)];

changez "AllCorners" selon vos besoins.

2
Umang

En commençant par votre code, vous pouvez utiliser quelque chose comme l'extrait ci-dessous.

Je ne sais pas si c'est le genre de résultat que vous recherchez. Il convient de noter également que si/lorsque le système appelle drawRect: là encore, demandant de ne redessiner qu’une partie du rect, cela se comportera très étrangement. approche de Nevan , noté ci-dessus, pourrait être une meilleure façon de faire.

 // make sure the view's background is set to [UIColor clearColor]
- (void)drawRect:(CGRect)rect
{
    CGFloat radius = 10.0;
    CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextTranslateCTM(context, rect.size.width/2, rect.size.height/2);
    CGContextRotateCTM(context, M_PI); // rotate so image appears right way up
    CGContextTranslateCTM(context, -rect.size.width/2, -rect.size.height/2);

    CGContextBeginPath(context);

    CGContextMoveToPoint(context, rect.Origin.x, rect.Origin.y);
    CGContextAddArc(context, rect.Origin.x + radius, rect.Origin.y + rect.size.height - radius, radius, M_PI, M_PI / 2, 1);
    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);
    CGContextClip(context); 

    // now do your drawing, e.g. draw an image
    CGImageRef anImage = [[UIImage imageNamed:@"image.jpg"] CGImage];
    CGContextDrawImage(context, rect, anImage);    
}
1
Obliquely

Un moyen légèrement hacky, mais relativement simple (pas de sous-classement, masquage, etc.), consiste à avoir deux vues UIV. Les deux avec clipToBounds = YES. Définissez des coins arrondis sur la vue enfant, puis positionnez-la dans la vue parent afin que les coins que vous souhaitez droits soient rognés.

UIView* parent = [[UIView alloc] initWithFrame:CGRectMake(10,10,100,100)];
parent.clipsToBounds = YES;

UIView* child = [[UIView alloc] new];
child.clipsToBounds = YES;
child.layer.cornerRadius = 3.0f;
child.backgroundColor = [UIColor redColor];
child.frame = CGRectOffset(parent.bounds, +4, -4);


[parent addSubView:child];

Ne prend pas en charge le cas où vous souhaitez arrondir les angles opposés en diagonale.

1
William Denniss

Solution UIBezierPath.

- (void) drawRect:(CGRect)rect {

    [super drawRect:rect];

    //Create shape which we will draw. 
    CGRect rectangle = CGRectMake(2, 
                                  2, 
                                  rect.size.width - 4,
                                  rect.size.height - 4);

    //Create BezierPath with round corners
    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:rectangle 
                                                   byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight 
                                                         cornerRadii:CGSizeMake(10.0, 10.0)];

    //Set path width
    [maskPath setLineWidth:2];

    //Set color
    [[UIColor redColor] setStroke];

    //Draw BezierPath to see it
    [maskPath stroke];
}
1
Alexey Bondarchuk

Le chemin de Bézier est la réponse, si vous avez besoin de code supplémentaire, celui-ci a fonctionné pour moi: https://stackoverflow.com/a/13163693/936957

UIBezierPath *maskPath;
maskPath = [UIBezierPath bezierPathWithRoundedRect:_backgroundImageView.bounds 
                                 byRoundingCorners:(UIRectCornerBottomLeft | UIRectCornerBottomRight) 
                                       cornerRadii:CGSizeMake(3.0, 3.0)];

CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = self.bounds;
maskLayer.path = maskPath.CGPath;
_backgroundImageView.layer.mask = maskLayer;
[maskLayer release];
1
Yunus Nedim Mehel

Créer un masque et le définir sur le calque de la vue

1
Steve Riggins

En élargissant la réponse acceptée, ajoutons-lui une compatibilité ascendante. Avant iOS 11, view.layer.maskedCorners n'était pas disponible. Donc on peut faire comme ça

if #available(iOS 11.0, *) {
    myView.layer.maskedCorners = [.layerMinXMinYCorner,.layerMaxXMinYCorner]
} else {
    myView.maskByRoundingCorners([.topLeft, .topRight])
}

extension UIView{
    func maskByRoundingCorners(_ masks:UIRectCorner, withRadii radii:CGSize = CGSize(width: 10, height: 10)) {
        let rounded = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: masks, cornerRadii: radii)

        let shape = CAShapeLayer()
        shape.path = rounded.cgPath

        self.layer.mask = shape
    }
}

Nous avons écrit maskByRoundingCorners en tant qu'extension UIView afin d'améliorer la réutilisation du code.

Crédits à @SachinVsSachin et @ P.L :) J'ai combiné leurs codes pour l'améliorer.

1
Rizwan Ahmed

Je me rends compte que vous essayez de contourner les deux coins supérieurs d'un UITableView, mais pour une raison quelconque, j'ai découvert que la meilleure solution consiste à utiliser:

self.tableView.layer.cornerRadius = 10;

Par programme, il devrait tourner autour des quatre coins, mais pour une raison quelconque, il ne fait que tourner les deux premiers. ** S'il vous plaît voir la capture d'écran ci-dessous pour voir l'effet du code que j'ai écrit ci-dessus.

J'espère que ça aide!

enter image description here

0
Adam Cooper

Cela ne peut fonctionner que si certaines choses sont correctement définies:

  1. clipsToBounds doit être défini sur YES
  2. opaque doit être NON
  3. backgroundColor devrait être "clearColor" (je ne suis pas tout à fait sûr à ce sujet)
  4. contentMode doit être "UIContentModeRedraw", car drawRect n'est pas appelé souvent s'il ne l'est pas.
  5. [super drawRect: rect] doit être appelé après le CGContextClip
  6. Votre vue ne peut pas contenir de sous-vues arbitraires (pas sûr à ce sujet)
  7. Assurez-vous de définir "needsDisplay:" au moins une fois pour déclencher votre drawrect
0
Dunkelstern