如何使用CALayers创建带有圆角的UIView,只圆角化部分角落?

125

我的应用程序中有四个按钮,分别命名为:

  • 左上
  • 左下
  • 右上
  • 右下

在这些按钮的上方有一个图像视图(或UIView)。

现在,假设用户点击了“左上”按钮,则上方的图像/视图应该在该特定角落处变为圆角。

我在应用圆角到UIView上有些困难。

目前,我正在使用以下代码将圆角应用于每个视图:

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

以上代码将圆角应用于提供的视图的每个角落。但我只想将圆角应用于所选的角落,例如 - 顶部/顶部+左侧/底部+右侧等。

这是否可行?怎么做?

14个回答

358

自iOS 3.2起,您可以使用UIBezierPath的功能创建一个开箱即用的圆角矩形(仅使您指定的角变为圆角)。 然后,您可以将其用作CAShapeLayer的路径,并将其用作视图层的蒙版:

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

这就是全部内容了 - 无需手动在核心图形中定义形状,也不需要在Photoshop中创建遮罩图像。甚至不需要使图层失效。只需定义一个新的UIBezierPath并使用其CGPath作为掩码层的路径即可应用圆角或更改为新圆角。bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:方法的corners参数是一个位掩码(bitmask),因此可以通过对它们进行逻辑或(OR)操作来圆角化多个角落。


编辑:添加阴影

如果您想要添加阴影到这个图像,需要做一些额外的工作。

因为"imageView.layer.mask = maskLayer"应用了一个掩码,所以阴影通常不会显示在它的外部。诀窍是使用一个透明视图,然后将两个子层(CALayers)添加到该视图的层中:shadowLayerroundedLayer。两者都需要使用UIBezierPath。图像被添加为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
不错,这对我在分组的UITableView单元格的selectedBackgroundViews方面帮助很大 :-) - Luke47
1
@ChrisWagner: 请看我的编辑,关于给圆角视图应用阴影。 - Stuart
如果有人对按位或运算感到困惑,可以使用以下代码: UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:_tableView.bounds byRoundingCorners:UIRectCornerBottomLeft | UIRectCornerBottomRight cornerRadii:CGSizeMake(10.0, 10.0)]; CAShapeLayer *maskLayer = [CAShapeLayer layer]; maskLayer.frame = _tableView.bounds; maskLayer.path = maskPath.CGPath; _tableView.layer.mask = maskLayer; - Desert Rose
@Stuart:有没有办法让这个遮罩自动调整大小?我正在尝试使用此代码仅将视图的左上角圆角化。代码可以工作,但仅适用于调用此代码时视图的任何大小。如果我将视图放大,我就看不到它了,因为新区域在遮罩之外。 - MusiGenesis
@MusiGenesis UIView 会根据其 autoresizingMask 自动调整子视图的大小,但是不支持该 UIView 的子层不会自动调整大小,因此您需要手动调整。不过这很容易 - 每当您调整视图的大小(或在 layoutSubviews 中如果您的视图是子类化的,甚至在您的视图控制器的 viewWill/DidLayoutSubviews 方法中如果适用),获取对蒙版的引用 (CAShapeLayer *mask = (CAShapeLayer *)view.layer.mask;),使用视图的新边界创建一个新的 UIBezierPath 并更改路径 (mask.path = newPath.CGPath; mask.frame = view.bounds;)。 - Stuart
显示剩余10条评论

45

我使用了来自如何在iPhone上创建圆角UILabel?的答案以及如何在iPhone上制作具有透明度的圆角矩形视图?中的代码,生成了这段代码。

然后我意识到我回答了错误的问题(给出了一个圆角UILabel而不是UIImage),所以我使用了此代码进行更改:

使用View模板创建一个iPhone项目,在视图控制器中添加以下内容:

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

MyView只是UIImageView的子类:

@interface MyView : UIImageView
{
}

我以前从未使用过图形上下文,但我设法拼凑出了这段代码。它缺少两个角的代码。如果您阅读代码,可以看到我如何实现此功能(通过删除一些CGContextAddArc调用和代码中的一些半径值。所有角的代码都已经在那里,所以以此为起点并删除不需要创建的角部分。请注意,您也可以制作具有2或3个圆角的矩形。

这段代码还不完美,但我相信您能稍微整理一下。

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

alt text http://nevan.net/skitch/skitched-20100224-092237.png

别忘了你需要将 QuartzCore 框架添加进来才能让它工作。


对于可能会改变大小的视图,如UILabel,不会按预期工作。设置遮罩路径,然后通过将文本设置为其更改大小将保留旧遮罩。需要子类化和重载drawRect。 - user3099609

18

我在我的代码中许多地方都使用了这段代码,它可以完全正确地运行。您可以通过更改一个属性 "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];

当在UITableView的子视图(例如UITableViewCellUITableViewHeaderFooterView)上使用此解决方案时,滚动时会导致不良平滑度。另一种适用于该用途的方法是这个具有更好性能的解决方案(它将圆角半径添加到所有角落)。要仅“显示”特定角落(如右上角和右下角),我添加了一个带有负值的子视图frame.origin.x并将cornerRadius分配给其层。如果有人找到更好的解决方案,我很感兴趣。 - anneblue

17
在iOS 11中,我们现在可以只圆角一些角落。
let view = UIView()

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

只是额外说明,这适用于使用自动布局的Storyboard。其他一些发布的解决方案在自动布局下无法正常工作。 - xdeleon

8

使用 Swift 3+ 语法的 CALayer 扩展

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

它可以这样使用:

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

5

Stuart的圆角示例很好用。如果你想要像左上和右上这样圆角多个角,可以按照以下步骤进行操作。

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

当在UITableView的子视图(例如UITableViewCellUITableViewHeaderFooterView)上使用此解决方案时,滚动时会导致不良平滑度。另一种适用于该用途的方法是这个具有更好性能的解决方案(它将cornerRadius添加到所有角落)。要仅“显示”特定角落(如右上角和右下角),我添加了一个带有负值的子视图frame.origin.x并将cornerRadius分配给其层。如果有人找到更好的解决方案,我很感兴趣。 - anneblue

4

如果您的需求允许,有一个更简单、更快速的方法可以实现圆角效果,并且还适用于阴影。您可以将maskToBounds设置为true,然后调整子层的偏移量,使它们的两个角落超出superlayer的边界,从而有效地去除2个边上的圆角。

当然,这仅适用于您只想在同一侧拥有2个圆角,并且从一侧削减几个像素时,图层内容看起来相同的情况。对于只在顶部拥有圆角的条形图非常有效。


4

感谢分享。这里我想分享一下关于swift 2.0的解决方案,以供参考。(符合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

3
请参阅相关问题。您需要将自己的矩形绘制到一个带有一些圆角的CGPath中,将CGPath添加到您的CGContext中,然后使用CGContextClip进行剪切。
您还可以使用alpha值将圆角矩形绘制到图像中,然后使用该图像创建一个新图层,并将其设置为您的图层的mask属性(请参阅Apple的文档)。

2

仅针对某些角进行圆角处理可能会影响自动调整大小或自动布局的表现。

因此,另一种选项是使用常规cornerRadius并将不想要的角隐藏在另一个视图下面或超出其父视图边界,确保它被设置为裁剪其内容。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接