UIView làm tròn bằng cách sử dụng CALayers - chỉ một số góc - Làm thế nào?


121

Trong ứng dụng của tôi - có bốn nút được đặt tên như sau:

  • Trên cùng - bên trái
  • Dưới cùng - bên trái
  • Trên cùng bên phải
  • Góc phải ở phía dưới

Phía trên các nút có chế độ xem hình ảnh (hoặc UIView).

Bây giờ, giả sử một người dùng nhấn vào nút trên cùng bên trái. Hình ảnh / chế độ xem trên phải được làm tròn ở góc cụ thể đó.

Tôi gặp một số khó khăn trong việc áp dụng các góc tròn cho UIView.

Hiện tại, tôi đang sử dụng mã sau để áp dụng các góc tròn cho mỗi chế độ xem:

    // 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ạn mã trên áp dụng độ tròn cho từng góc của Chế độ xem được cung cấp. Thay vào đó, tôi chỉ muốn áp dụng độ tròn cho các góc đã chọn như - trên / trên + trái / dưới + phải, v.v.

Có khả thi không? Làm sao?

Câu trả lời:


44

Tôi đã sử dụng câu trả lời ở Làm cách nào để tạo Nhãn UIL có góc tròn trên iPhone? và mã từ Chế độ xem hình chữ nhật tròn với độ trong suốt được thực hiện trên iPhone như thế nào? để tạo mã này.

Sau đó, tôi nhận ra mình đã trả lời sai câu hỏi (đã đưa ra một UILabel tròn thay vì UIImage) vì vậy tôi đã sử dụng mã này để thay đổi nó:

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

Tạo một dự án iPhone với mẫu View. Trong bộ điều khiển chế độ xem, hãy thêm cái này:

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

MyViewchỉ là một UIImageViewlớp con:

@interface MyView : UIImageView
{
}

Tôi chưa bao giờ sử dụng các bối cảnh đồ họa trước đây, nhưng tôi đã xoay sở để tập hợp mã này. Nó thiếu mã cho hai trong số các góc. Nếu bạn đọc mã, bạn có thể thấy cách tôi triển khai điều này (bằng cách xóa một số CGContextAddArclệnh gọi và xóa một số giá trị bán kính trong mã. Mã cho tất cả các góc ở đó, vì vậy hãy sử dụng nó làm điểm bắt đầu và xóa phần tạo góc bạn không cần. Lưu ý rằng bạn cũng có thể tạo hình chữ nhật với 2 hoặc 3 góc tròn nếu muốn.

Mã không hoàn hảo, nhưng tôi chắc rằng bạn có thể chỉnh sửa nó một chút.

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

văn bản thay thế http://nevan.net/skitch/skitched-20100224-092237.png

Đừng quên rằng bạn sẽ cần phải có khung QuartzCore ở đó để điều này hoạt động.


Không hoạt động như mong đợi với các chế độ xem có kích thước có thể thay đổi. Ví dụ: một UILabel. Đặt đường dẫn mặt nạ và sau đó thay đổi kích thước bằng cách đặt văn bản cho nó sẽ giữ nguyên mặt nạ cũ. Sẽ cần phân lớp và nạp chồng cho drawRect.
dùng3099609

358

Bắt đầu từ iOS 3.2, bạn có thể sử dụng chức năng của UIBezierPaths để tạo hình chữ nhật tròn bên ngoài (nơi chỉ các góc bạn chỉ định mới được làm tròn). Sau đó, bạn có thể sử dụng nó làm đường dẫn của a CAShapeLayervà sử dụng nó làm mặt nạ cho lớp trong chế độ xem của bạn:

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

Và thế là xong - không cần phải xác định hình dạng theo cách thủ công trong Core Graphics, không cần tạo hình ảnh mặt nạ trong Photoshop. Lớp thậm chí không cần vô hiệu hóa. Áp dụng góc tròn hoặc thay đổi thành một góc mới cũng đơn giản như xác định một góc mới UIBezierPathvà sử dụng nó CGPathlàm đường dẫn của lớp mặt nạ. Các cornerstham số của bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:phương pháp là một bitmask, và vì vậy nhiều góc có thể được làm tròn bởi ORing chúng lại với nhau.


CHỈNH SỬA: Thêm bóng

Nếu bạn đang tìm cách thêm bóng cho cái này, bạn cần phải làm thêm một chút.

Bởi vì " imageView.layer.mask = maskLayer" áp dụng một mặt nạ, thông thường bóng sẽ không hiển thị bên ngoài nó. Bí quyết là sử dụng dạng xem trong suốt, sau đó thêm ( CALayercác) lớp con vào lớp của dạng xem: shadowLayerroundedLayer. Cả hai đều cần tận dụng UIBezierPath. Hình ảnh được thêm vào làm nội dung của 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
Thoải mái, tùy chọn này đã giúp tôi rất nhiều trên selectedBackgroundViews nhóm UITableView tế bào của :-)
Luke47

Dù sao để thêm bóng đổ vào chế độ xem này sau khi bạn đã làm tròn các góc? Đã thử những điều sau đây mà không thành công. `self.layer.masksToBounds = KHÔNG; self.layer.shadowColor = [UIColor blackColor] .CGColor; self.layer.shadowRadius = 3; self.layer.shadowOffset = CGSizeMake (-3, 3); self.layer.shadowOpacity = 0,8; self.layer.shouldRasterize = CÓ; '
Chris Wagner

1
@ChrisWagner: Xem chỉnh sửa của tôi, liên quan đến việc áp dụng bóng cho chế độ xem tròn.
Stuart

@StuDev xuất sắc! Tôi đã đi với một chế độ xem phụ cho chế độ xem bóng tối nhưng nó đẹp hơn nhiều! Không nghĩ đến việc thêm các lớp phụ như thế này. Cảm ơn!
Chris Wagner

Tại sao hình ảnh của tôi trở thành màu trắng, sau đó khi tôi cuộn UITableView và ô được tạo lại - nó hoạt động! Tại sao?
Fernando Redondo

18

Tôi đã sử dụng mã này ở nhiều nơi trong mã của mình và nó hoạt động chính xác 100%. Bạn có thể thay đổi bất kỳ corder nào bằng cách thay đổi một thuộc tính "byRoundsCorners: 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];

Khi sử dụng giải pháp này trên- UITableViewsubviews (ví dụ: UITableViewCells hoặc UITableViewHeaderFooterViews), nó dẫn đến độ mượt kém khi cuộn . Một cách tiếp cận khác cho việc sử dụng đó là giải pháp này có hiệu suất tốt hơn (nó thêm một angleRadius cho tất cả các góc). Để 'chỉ hiển thị' các góc cụ thể được làm tròn (như góc trên và góc dưới bên phải), tôi đã thêm một chế độ xem phụ với giá trị âm trên nó frame.origin.xvà gán angleRadius cho lớp của nó. Nếu ai đó tìm thấy một giải pháp tốt hơn, tôi quan tâm.
anneblue

11

Trong iOS 11, giờ đây chúng ta chỉ có thể làm tròn một số góc

let view = UIView()

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

8

Phần mở rộng CALayer với cú pháp 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
    }
}

Nó có thể được sử dụng như:

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

5

Ví dụ về Stuarts để làm tròn các góc cụ thể hoạt động tốt. Nếu bạn muốn làm tròn nhiều góc như trên cùng bên trái và bên phải thì đây là cách thực hiện

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

Khi sử dụng giải pháp này trên- UITableViewsubviews (ví dụ: UITableViewCells hoặc UITableViewHeaderFooterViews), nó dẫn đến độ mượt kém khi cuộn . Một cách tiếp cận khác cho việc sử dụng đó là giải pháp này có hiệu suất tốt hơn (nó thêm một angleRadius cho tất cả các góc). Để 'chỉ hiển thị' các góc cụ thể được làm tròn (như góc trên và góc dưới bên phải), tôi đã thêm một chế độ xem phụ với giá trị âm trên nó frame.origin.xvà gán angleRadius cho lớp của nó. Nếu ai đó tìm thấy một giải pháp tốt hơn, tôi quan tâm.
anneblue

5

Cám ơn vì đã chia sẻ. Ở đây tôi muốn chia sẻ giải pháp trên swift 2.0 để tham khảo thêm về vấn đề này. (để phù hợp với giao thức của 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

có một câu trả lời dễ dàng hơn và nhanh hơn có thể hoạt động tùy thuộc vào nhu cầu của bạn và cũng hoạt động với bóng đổ. bạn có thể đặt maskToBounds trên lớp chồng thành true và bù đắp các lớp con sao cho 2 góc của chúng nằm ngoài giới hạn của lớp siêu, cắt hiệu quả các góc tròn ở 2 bên.

tất nhiên điều này chỉ hoạt động khi bạn muốn chỉ có 2 góc tròn ở cùng một bên và nội dung của lớp trông giống nhau khi bạn cắt bỏ một vài pixel từ một bên. hoạt động tuyệt vời khi chỉ làm tròn biểu đồ thanh ở phía trên cùng.


3

Xem câu hỏi liên quan này . Bạn sẽ phải vẽ hình chữ nhật của riêng mình thành a CGPathvới một số góc tròn, thêm hình chữ nhật CGPathvào của bạn CGContextvà sau đó kẹp vào nó bằng cách sử dụngCGContextClip .

Bạn cũng có thể vẽ hình chữ nhật tròn với các giá trị alpha vào một hình ảnh và sau đó sử dụng hình ảnh đó để tạo một lớp mới mà bạn đặt làm thuộc tính lớp của mình mask( xem tài liệu của Apple ).


2

Trễ nửa thập kỷ, nhưng tôi nghĩ cách mọi người làm hiện nay không đúng 100%. Nhiều người đã gặp sự cố rằng việc sử dụng phương thức UIBezierPath + CAShapeLayer gây trở ngại cho Bố cục tự động, đặc biệt là khi nó được đặt trên Bảng phân cảnh. Không có câu trả lời nào về vấn đề này, vì vậy tôi quyết định thêm câu trả lời của riêng mình.

Có một cách rất dễ dàng để phá vỡ điều đó: Vẽ các góc tròn trong drawRect(rect: CGRect) hàm.

Ví dụ: nếu tôi muốn các góc tròn trên cùng cho một UIView, tôi sẽ phân lớp UIView và sau đó sử dụng lớp con đó bất cứ khi nào thích hợp.

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

Đây là cách tốt nhất để chinh phục vấn đề và không mất bất kỳ thời gian nào để thích nghi.


1
Đây không phải là cách phù hợp để giải quyết vấn đề. Bạn chỉ nên ghi đè drawRect(_:)nếu bạn đang vẽ tùy chỉnh trong chế độ xem của mình (ví dụ: với Đồ họa lõi), nếu không, ngay cả một triển khai trống cũng có thể ảnh hưởng đến hiệu suất. Tạo một lớp mặt nạ mới mỗi lần cũng không cần thiết. Tất cả những gì bạn thực sự cần làm là cập nhật thuộc pathtính của lớp mặt nạ khi giới hạn của chế độ xem thay đổi và nơi để làm điều đó nằm trong phần ghi đè của layoutSubviews()phương thức.
Stuart

2

Chỉ làm tròn một số góc sẽ không tốt với tính năng tự động thay đổi kích thước hoặc bố cục tự động.

Vì vậy, một tùy chọn khác là sử dụng thông thường cornerRadiusvà ẩn các góc bạn không muốn dưới chế độ xem khác hoặc bên ngoài giới hạn siêu chế độ xem của nó để đảm bảo rằng nó được đặt để cắt nội dung của nó.


1

Để thêm vào câu trả lời và phần bổ sung , tôi đã tạo một câu lệnh đơn giản, có thể tái sử dụng UIViewtrong Swift. Tùy thuộc vào trường hợp sử dụng của bạn, bạn có thể muốn thực hiện các sửa đổi (tránh tạo các đối tượng trên mọi bố cục, v.v.), nhưng tôi muốn giữ nó càng đơn giản càng tốt. Tiện ích mở rộng cho phép bạn áp dụng điều này cho chế độ xem khác (ví dụ UIImageView:) dễ dàng hơn nếu bạn không thích phân lớp.

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

Đây là cách bạn sẽ áp dụng nó cho 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

Tóm tắt câu trả lời của Stuart, bạn có thể có phương pháp làm tròn góc như sau:

@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

Vì vậy, để áp dụng góc làm tròn, bạn chỉ cần làm:

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

0

Tôi khuyên bạn nên xác định mặt nạ của lớp. Bản thân mặt nạ phải là một CAShapeLayerđối tượng có đường dẫn dành riêng. Bạn có thể sử dụng tiện ích mở rộng UIView tiếp theo (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
   }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.