UIView arredondado usando CALayers - apenas alguns cantos - Como?

121

No meu aplicativo - existem quatro botões com o seguinte nome:

  • Superior - esquerda
  • Inferior esquerdo
  • Canto superior direito
  • Canto inferior direito

Acima dos botões, há uma visualização de imagem (ou uma UIView).

Agora, suponha que um usuário toque no botão superior esquerdo. A imagem / vista acima deve ser arredondada nesse canto específico.

Estou com alguma dificuldade em aplicar cantos arredondados ao UIView.

No momento, estou usando o código a seguir para aplicar os cantos arredondados a cada exibição:

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

O código acima aplica a redondeza em cada um dos cantos da vista fornecida. Em vez disso, eu só queria aplicar a redondeza nos cantos selecionados, como - superior / superior + esquerdo / inferior + direito etc.

É possível? Quão?

Sagar R. Kothari
fonte

Respostas:

44

Eu usei a resposta em Como criar um UILabel de canto arredondado no iPhone? e o código de Como é feita uma visualização retangular arredondada com transparência no iphone? para fazer esse código.

Então percebi que havia respondido à pergunta errada (dei um UILabel arredondado em vez de UIImage), então usei esse código para alterá-lo:

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

Faça um projeto do iPhone com o modelo View. No controlador de exibição, adicione isto:

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

MyViewé apenas uma UIImageViewsubclasse:

@interface MyView : UIImageView
{
}

Eu nunca tinha usado contextos gráficos antes, mas consegui manobrar esse código. Está faltando o código para dois dos cantos. Se você ler o código, poderá ver como eu implementei isso (excluindo algumas das CGContextAddArcchamadas e excluindo alguns dos valores de raio no código. O código para todos os cantos está lá, use-o como ponto de partida e exclua o peças que criam cantos de que você não precisa.Tenha em mente que também pode fazer retângulos com 2 ou 3 cantos arredondados.

O código não é perfeito, mas tenho certeza que você pode arrumar um pouco.

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

Não esqueça que você precisará obter a estrutura do QuartzCore para que isso funcione.

rei nevan
fonte
Não funciona como esperado com visualizações cujo tamanho pode mudar. Exemplo: um UILabel. Definir o caminho da máscara e, em seguida, alterar o tamanho, definindo o texto como ele manterá a máscara antiga. Será necessário subclassificar e sobrecarregar o drawRect.
user3099609
358

A partir do iOS 3.2, é possível usar a funcionalidade UIBezierPaths para criar um retângulo arredondado pronto para uso (onde apenas os cantos especificados são arredondados). Você pode usar isso como o caminho de a CAShapeLayere usá-lo como uma máscara para a camada da sua exibição:

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

E é isso: não é necessário definir formas manualmente no Core Graphics, nem criar imagens mascaradas no Photoshop. A camada nem precisa ser invalidada. Aplicar o canto arredondado ou alterar para um novo canto é tão simples quanto definir um novo UIBezierPathe usá-lo CGPathcomo o caminho da camada de máscara. O cornersparâmetro do bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:método é uma máscara de bits e, portanto, vários cantos podem ser arredondados ORing juntos.


EDIT: Adicionando uma sombra

Se você deseja adicionar uma sombra a isso, é necessário um pouco mais de trabalho.

Como " imageView.layer.mask = maskLayer" aplica uma máscara, uma sombra normalmente não aparece fora dela. O truque é usar uma vista transparente e, em seguida, adicionar duas subcamadas CALayerà camada da vista: shadowLayere roundedLayer. Ambos precisam fazer uso do UIBezierPath. A imagem é adicionada como o conteúdo 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
fonte
1
Boa, isso me ajudou muito na selectedBackgroundViews agrupados célula UITableView :-)
Luke47
De qualquer forma, para adicionar uma sombra projetada a essa exibição depois de virar as esquinas? Tentou o seguinte sem sucesso. `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: veja minha edição, com relação à aplicação de uma sombra na exibição arredondada.
Stuart
@StuDev excellent! Eu tinha escolhido uma subview para a view shadow, mas isso é muito melhor! Não pensou em adicionar sub-camadas como esta. Obrigado!
21711 Chris
Por que minha imagem fica branca, quando eu rolar o UITableView e a célula é recriada - ela funciona! Por quê?
Fernando Redondo
18

Eu usei esse código em muitos lugares no meu código e funciona 100% corretamente. Você pode alterar qualquer corder alterando uma propriedade "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
fonte
Ao usar esta solução em UITableView-subviews (por exemplo, UITableViewCells ou UITableViewHeaderFooterViews), isso resulta em uma suavidade ruim ao rolar . Outra abordagem para esse uso é essa solução com melhor desempenho (ela adiciona um cornerRadius a todos os cantos). Para 'mostrar' apenas cantos arredondados específicos (como os cantos superior e inferior direito), adicionei uma subvisão com um valor negativo frame.origin.xe atribuai o cornerRadius à sua camada. Se alguém encontrou uma solução melhor, estou interessado.
AnneBlue
11

No iOS 11, agora podemos arredondar apenas alguns cantos

let view = UIView()

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

Extensão CALayer com sintaxe 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
    }
}

Pode ser usado como:

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

O exemplo de stuarts para arredondar cantos específicos funciona muito bem. Se você deseja arredondar vários cantos, como o canto superior esquerdo e o direito, é assim que se faz

// 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
fonte
Ao usar esta solução em UITableView-subviews (por exemplo, UITableViewCells ou UITableViewHeaderFooterViews), isso resulta em uma suavidade ruim ao rolar . Outra abordagem para esse uso é essa solução com melhor desempenho (ela adiciona um cornerRadius a todos os cantos). Para 'mostrar' apenas cantos arredondados específicos (como os cantos superior e inferior direito), adicionei uma subvisão com um valor negativo frame.origin.xe atribuai o cornerRadius à sua camada. Se alguém encontrou uma solução melhor, estou interessado.
precisa saber é
5

Obrigado por compartilhar. Aqui, gostaria de compartilhar a solução no swift 2.0 para obter mais referências sobre esse problema. (para conformar o protocolo do 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
fonte
4

existe uma resposta mais fácil e rápida que pode funcionar dependendo de suas necessidades e também funciona com sombras. você pode definir maskToBounds na superlayer como true e deslocar as camadas filho para que 2 dos seus cantos fiquem fora dos limites da superlayer, cortando efetivamente os cantos arredondados em 2 lados.

é claro que isso só funciona quando você deseja ter apenas 2 cantos arredondados no mesmo lado e o conteúdo da camada fica igual quando você corta alguns pixels de um lado. funciona muito bem com gráficos de barras arredondados apenas no lado superior.

user1259710
fonte
3

Veja esta pergunta relacionada . Você terá que desenhar seu próprio retângulo em um CGPathcom alguns cantos arredondados, adicionar o CGPathao seu CGContexte depois prendê-lo usando CGContextClip.

Você também pode desenhar o retângulo arredondado com valores alfa em uma imagem e usá-la para criar uma nova camada que você define como maskpropriedade da sua camada ( consulte a documentação da Apple ).

MrMage
fonte
2

Meia década atrasada, mas acho que a maneira atual como as pessoas fazem isso não é 100% correta. Muitas pessoas tiveram o problema de que o uso do método UIBezierPath + CAShapeLayer interfere no layout automático, especialmente quando ele é definido no Storyboard. Não há respostas sobre isso, então decidi adicionar as minhas.

Existe uma maneira muito fácil de contornar isso: Desenhe os cantos arredondados na drawRect(rect: CGRect)função.

Por exemplo, se eu quisesse os cantos arredondados no topo de uma UIView, subclasseia o UIView e usaria essa subclasse sempre que apropriado.

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

Essa é a melhor maneira de conquistar o problema e não leva tempo para se adaptar.

David
fonte
1
Este não é o caminho certo para resolver o problema. Você deve substituir apenas drawRect(_:)se estiver fazendo um desenho personalizado em sua visualização (por exemplo, com gráficos principais), caso contrário, mesmo uma implementação vazia poderá afetar o desempenho. Criar uma nova camada de máscara sempre é desnecessário. Tudo o que você realmente precisa fazer é atualizar a pathpropriedade da camada de máscara quando os limites da exibição mudarem e o local para fazer isso estiver dentro de uma substituição do layoutSubviews()método.
Stuart
2

Arredondar apenas alguns cantos não funcionará bem com redimensionamento automático ou layout automático.

Portanto, outra opção é usar regular cornerRadiuse ocultar os cantos que você não deseja em outra visualização ou fora dos limites da superview, certificando-se de que ela esteja configurada para cortar seu conteúdo.

Rivera
fonte
1

Para adicionar à resposta e à adição , criei um simples, reutilizável UIViewno Swift. Dependendo do seu caso de uso, convém fazer modificações (evite criar objetos em todos os layouts etc.), mas eu queria mantê-lo o mais simples possível. A extensão permite aplicar isso a outras visualizações (ex. UIImageView) Com mais facilidade se você não gosta de subclassificar.

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

Veja como você o aplicaria 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)
    }
}
kgaidis
fonte
0

Para finalizar a resposta de Stuart, você pode usar o método de canto arredondado da seguinte maneira:

@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

Para aplicar o canto arredondado, basta:

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

Eu sugiro definir a máscara de uma camada. A própria máscara deve ser um CAShapeLayerobjeto com um caminho dedicado. Você pode usar a próxima extensão 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
fonte