UIView redondeado usando CALayers - solo algunas esquinas - ¿Cómo?


121

En mi aplicación, hay cuatro botones nombrados de la siguiente manera:

  • Arriba a la izquierda
  • Abajo - izquierda
  • Parte superior derecha
  • Abajo a la derecha

Sobre los botones hay una vista de imagen (o una vista UIView).

Ahora, supongamos que un usuario toca el botón - superior - izquierdo. La imagen / vista anterior debe redondearse en esa esquina en particular.

Tengo algunas dificultades para aplicar esquinas redondeadas a la UIView.

En este momento estoy usando el siguiente código para aplicar las esquinas redondeadas a cada 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]];

El código anterior está aplicando la redondez a cada una de las esquinas de la Vista suministrada. En cambio, solo quería aplicar la redondez a las esquinas seleccionadas como: arriba / arriba + izquierda / abajo + derecha, etc.

¿Es posible? ¿Cómo?

Respuestas:


44

Utilicé la respuesta en ¿Cómo creo un UILabel acorralado en el iPhone? y el código de ¿Cómo se hace una vista rectángulo redondeada con transparencia en iphone? para hacer este código

Luego me di cuenta de que había respondido la pregunta incorrecta (le di un UILabel redondeado en lugar de UIImage), así que usé este código para cambiarlo:

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

Haga un proyecto de iPhone con la plantilla Ver. En el controlador de vista, agregue esto:

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

MyViewes solo una UIImageViewsubclase:

@interface MyView : UIImageView
{
}

Nunca antes había usado contextos gráficos, pero me las arreglé para crear este código. Falta el código para dos de las esquinas. Si lees el código, puedes ver cómo implementé esto (eliminando algunas de las CGContextAddArcllamadas y eliminando algunos de los valores de radio en el código. El código para todas las esquinas está ahí, así que úsalo como punto de partida y elimina el partes que crean esquinas que no necesita. Tenga en cuenta que también puede hacer rectángulos con 2 o 3 esquinas redondeadas si lo desea.

El código no es perfecto, pero estoy seguro de que puedes arreglarlo un poco.

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

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

No olvides que necesitarás tener el framework QuartzCore para que esto funcione.


No funciona como se esperaba con vistas cuyo tamaño puede cambiar. Ejemplo: un UILabel. Al configurar la ruta de la máscara y luego cambiar el tamaño configurando el texto, se mantendrá la máscara anterior. Necesitará subclasificar y sobrecargar el drawRect.
user3099609

358

A partir de iOS 3.2, puede usar la funcionalidad de UIBezierPaths para crear un rectángulo redondeado listo para usar (donde solo se redondean las esquinas que especifique). Luego puede usar esto como la ruta de acceso de a CAShapeLayer, y usar esto como una máscara para la capa de su 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;

Y eso es todo: no perder el tiempo definiendo formas manualmente en Core Graphics, sin crear imágenes de enmascaramiento en Photoshop. La capa ni siquiera necesita invalidarse. Aplicar la esquina redondeada o cambiar a una nueva esquina es tan simple como definir una nueva UIBezierPathy usarla CGPathcomo la ruta de la capa de máscara. El cornersparámetro del bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:método es una máscara de bits, por lo que se pueden redondear varias esquinas orándolas juntas.


EDITAR: Agregar una sombra

Si está buscando agregar una sombra a esto, se requiere un poco más de trabajo.

Como " imageView.layer.mask = maskLayer" aplica una máscara, normalmente no se mostrará una sombra fuera de ella. El truco consiste en utilizar una vista transparente y luego agregar dos subcapas CALayera la capa de la vista: shadowLayery roundedLayer. Ambos necesitan hacer uso de UIBezierPath. La imagen se agrega como contenido 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];

1
Bien hecho, esto me ha ayudado mucho en selectedBackgroundViews agrupado de teléfonos UITableView :-)
luke47

¿De todos modos agregar una sombra paralela a esta vista después de haber redondeado las esquinas? Intenté lo siguiente sin éxito. `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 el

1
@ChrisWagner: vea mi edición, con respecto a la aplicación de una sombra a la vista redondeada.
Stuart

@StuDev excelente! Me había ido con una subvista para la vista de sombra, ¡pero esto es mucho mejor! No pensé en agregar subcapas como esta. ¡Gracias!
Chris Wagner

¿Por qué mi imagen se vuelve blanca y luego cuando desplazo el UITableView y se recrea la celda, funciona! ¿Por qué?
Fernando Redondo

18

He usado este código en muchos lugares de mi código y funciona 100% correctamente. Puede cambiar cualquier cable cambiando una propiedad "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];

Cuando se utiliza esta solución en las UITableViewsubvistas (p. Ej. UITableViewCellS o UITableViewHeaderFooterViews), se produce una suavidad incorrecta al desplazarse . Otro enfoque para ese uso es esta solución con un mejor rendimiento (agrega un cornerRadius a todos los rincones). Para 'mostrar' solo las esquinas específicas redondeadas (como las esquinas superior e inferior derecha) agregué una subvista con un valor negativo frame.origin.xy asigné el cornerRadius a su capa. Si alguien encuentra una mejor solución, estoy interesado.
anneblue

11

En iOS 11, ahora solo podemos redondear algunas esquinas

let view = UIView()

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

7

Extensión CALayer con sintaxis 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
    }
}

Se puede usar como:

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

5

El ejemplo de Stuarts para redondear esquinas específicas funciona muy bien. Si desea redondear varias esquinas, como arriba a la izquierda y derecha, así es cómo hacerlo

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

Cuando se utiliza esta solución en las UITableViewsubvistas (p. Ej. UITableViewCellS o UITableViewHeaderFooterViews), se produce una suavidad incorrecta al desplazarse . Otro enfoque para ese uso es esta solución con un mejor rendimiento (agrega un cornerRadius a todos los rincones). Para 'mostrar' solo las esquinas específicas redondeadas (como las esquinas superior e inferior derecha) agregué una subvista con un valor negativo frame.origin.xy asigné el cornerRadius a su capa. Si alguien encuentra una mejor solución, estoy interesado.
anneblue

5

Gracias por compartir. Aquí me gustaría compartir la solución en swift 2.0 para obtener más información sobre este tema. (para conformar el protocolo de 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

Hay una respuesta más fácil y rápida que puede funcionar dependiendo de sus necesidades y también funciona con sombras. puede establecer maskToBounds en la supercapa en verdadero y compensar las capas secundarias para que 2 de sus esquinas estén fuera de los límites de la supercapa, cortando efectivamente las esquinas redondeadas en 2 lados.

por supuesto, esto solo funciona cuando desea tener solo 2 esquinas redondeadas en el mismo lado y el contenido de la capa se ve igual cuando corta unos pocos píxeles de un lado. funciona muy bien para tener gráficos de barras redondeados solo en la parte superior.


3

Ver esta pregunta relacionada . Tendrás que dibujar tu propio rectángulo CGPathcon unas esquinas redondeadas, agregarlo CGPatha tu CGContexty luego recortarlo usando CGContextClip.

También puede dibujar el rectángulo redondeado con valores alfa en una imagen y luego usar esa imagen para crear una nueva capa que establezca como maskpropiedad de su capa ( consulte la documentación de Apple ).


2

Media década tarde, pero creo que la forma actual en que las personas hacen esto no es 100% correcta. Muchas personas han tenido el problema de que usar el método UIBezierPath + CAShapeLayer interfiere con el diseño automático, especialmente cuando está configurado en el Storyboard. No hay respuestas sobre esto, así que decidí agregar la mía.

Hay una manera muy fácil de evitar eso: dibuja las esquinas redondeadas en la drawRect(rect: CGRect)función.

Por ejemplo, si quisiera las esquinas redondeadas superiores para una UIView, subclase UIView y luego use esa subclase cuando corresponda.

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

Esta es la mejor manera de conquistar el problema y no requiere tiempo para adaptarse.


1
Esta no es la forma correcta de resolver el problema. Solo debe anular drawRect(_:)si está haciendo un dibujo personalizado en su vista (por ejemplo, con Core Graphics), de lo contrario, incluso una implementación vacía puede afectar el rendimiento. Crear una nueva capa de máscara cada vez es innecesario también. Todo lo que realmente necesita hacer es actualizar la pathpropiedad de la capa de máscara cuando cambian los límites de la vista, y el lugar para hacerlo está dentro de una anulación del layoutSubviews()método.
Stuart

2

Redondear solo algunas esquinas no funcionará bien con el cambio de tamaño automático o el diseño automático.

Entonces, otra opción es usar regular cornerRadiusy ocultar las esquinas que no desea debajo de otra vista o fuera de los límites de la supervista asegurándose de que esté configurado para recortar su contenido.


1

Para agregar a la respuesta y la adición , creé un simple, reutilizable UIViewen Swift. Dependiendo de su caso de uso, es posible que desee realizar modificaciones (evite crear objetos en cada diseño, etc.), pero quería que fuera lo más simple posible. La extensión le permite aplicar esto a otras vistas (ej. UIImageView) Más fácilmente si no le gusta la subclase.

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

Así es como lo aplicaría 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

Para concluir la respuesta de Stuart, puede tener el método de redondeo de la siguiente manera:

@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

Entonces, para aplicar el redondeo de esquina, simplemente debes hacer:

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

0

Sugeriría definir la máscara de una capa. La máscara en sí misma debe ser un CAShapeLayerobjeto con una ruta dedicada. Puede usar la próxima extensión 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
   }
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.