UIView bulat menggunakan CALayers - hanya beberapa sudut - Bagaimana caranya?

121

Dalam aplikasi saya - ada empat tombol yang diberi nama sebagai berikut:

  • Atas - kiri
  • Kiri bawah
  • Kanan atas
  • Kanan bawah

Di atas tombol ada tampilan gambar (atau UIView).

Sekarang, misalkan pengguna mengetuk tombol kiri atas. Gambar / tampilan di atas harus dibulatkan di sudut tertentu.

Saya mengalami kesulitan dalam menerapkan sudut membulat ke UIView.

Sekarang saya menggunakan kode berikut untuk menerapkan sudut membulat ke setiap tampilan:

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

Kode di atas menerapkan kebulatan ke setiap sudut View yang disediakan. Sebagai gantinya saya hanya ingin menerapkan kebulatan ke sudut yang dipilih seperti - atas / atas + kiri / bawah + kanan dll.

Apa itu mungkin? Bagaimana?

Sagar R. Kothari
sumber

Jawaban:

44

Saya menggunakan jawabannya di Bagaimana cara membuat UILabel dengan sudut bulat pada iPhone? dan kode dari Bagaimana tampilan persegi bundar dengan transparansi dilakukan di iphone? untuk membuat kode ini.

Kemudian saya menyadari bahwa saya telah menjawab pertanyaan yang salah (memberikan UILabel bulat daripada UIImage) jadi saya menggunakan kode ini untuk mengubahnya:

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

Buat proyek iPhone dengan template View. Di pengontrol tampilan, tambahkan ini:

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

MyViewhanyalah sebuah UIImageViewsubclass:

@interface MyView : UIImageView
{
}

Saya belum pernah menggunakan konteks grafis sebelumnya, tetapi saya berhasil mengatur kode ini dengan pincang. Ini kehilangan kode untuk dua sudut. Jika Anda membaca kode, Anda dapat melihat bagaimana saya menerapkan ini (dengan menghapus beberapa CGContextAddArcpanggilan, dan menghapus beberapa nilai radius dalam kode. Kode untuk semua sudut ada di sana, jadi gunakan itu sebagai titik awal dan hapus bagian yang membuat sudut tidak perlu. Perhatikan bahwa Anda dapat membuat persegi panjang dengan 2 atau 3 sudut membulat juga jika mau.

Kodenya tidak sempurna, tapi saya yakin Anda bisa sedikit merapikannya.

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

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

Jangan lupa bahwa Anda harus mendapatkan kerangka QuartzCore di sana agar ini berfungsi.

raja nevan
sumber
Tidak berfungsi seperti yang diharapkan dengan tampilan yang ukurannya mungkin berubah. Contoh: sebuah UILabel. Mengatur jalur topeng dan kemudian mengubah ukurannya dengan mengatur teks untuk itu akan membuat topeng lama. Akan membutuhkan subclassing dan overloading drawRect.
pengguna3099609
358

Mulai di iOS 3.2, Anda dapat menggunakan fungsionalitas UIBezierPaths untuk membuat persegi bundar di luar kotak (di mana hanya sudut yang Anda tentukan yang dibulatkan). Anda kemudian dapat menggunakan ini sebagai jalur a CAShapeLayer, dan menggunakan ini sebagai topeng untuk lapisan tampilan Anda:

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

Dan hanya itu - tidak mengotak-atik mendefinisikan bentuk secara manual di Core Graphics, tidak membuat gambar masking di Photoshop. Lapisan tersebut bahkan tidak perlu dibatalkan. Menerapkan sudut membulat atau mengubah ke sudut baru semudah mendefinisikan yang baru UIBezierPathdan menggunakannya CGPathsebagai jalur lapisan topeng. The cornersparameter bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:metode adalah bitmask, dan sehingga beberapa sudut dapat dibulatkan oleh ORing mereka bersama-sama.


EDIT: Menambahkan bayangan

Jika Anda ingin menambahkan bayangan ke ini, diperlukan sedikit pekerjaan lagi.

Karena imageView.layer.mask = maskLayermenggunakan topeng, bayangan biasanya tidak akan muncul di luarnya. Triknya adalah dengan menggunakan tampilan transparan, lalu menambahkan dua sub CALayer-lapisan ke lapisan tampilan: shadowLayerdan roundedLayer. Keduanya perlu memanfaatkan UIBezierPath. Gambar ditambahkan sebagai konten 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
sumber
1
Bagus, ini banyak membantu saya pada sel UITableView yang dikelompokkan dari selectedBackgroundViews :-)
Luke47
Adakah cara untuk menambahkan bayangan jatuh ke tampilan ini setelah Anda membulatkan sudut? Mencoba yang berikut ini tidak berhasil. `self.layer.masksToBounds = TIDAK; 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: Lihat hasil edit saya, sehubungan dengan menerapkan bayangan ke tampilan bulat.
Stuart
@StuDev luar biasa! Saya telah pergi dengan subview untuk tampilan bayangan tetapi ini jauh lebih bagus! Tidak berpikir untuk menambahkan sub lapisan seperti ini. Terima kasih!
Chris Wagner
Mengapa gambar saya menjadi putih, lalu saat saya menggulir UITableView dan sel dibuat ulang - berhasil! Mengapa?
Fernando Redondo
18

Saya telah menggunakan kode ini di banyak tempat di kode saya dan berfungsi 100% dengan benar. Anda dapat mengubah urutan apa pun dengan mengubah satu properti "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
sumber
Saat menggunakan solusi ini pada UITableView-subviews (misalnya UITableViewCells atau UITableViewHeaderFooterViews), ini menghasilkan kelancaran yang buruk saat menggulir . Pendekatan lain untuk penggunaan itu adalah solusi ini dengan kinerja yang lebih baik (menambahkan sudut Radius ke semua sudut). Untuk 'menampilkan' hanya sudut tertentu yang dibulatkan (seperti sudut kanan atas dan bawah) saya menambahkan subview dengan nilai negatif pada frame.origin.xdan menetapkan cornerRadius ke lapisannya. Jika seseorang menemukan solusi yang lebih baik, saya tertarik.
anneblue
11

Di iOS 11, kami sekarang hanya dapat melewati beberapa sudut

let view = UIView()

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

Ekstensi CALayer dengan sintaks 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
    }
}

Ini bisa digunakan seperti:

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

Contoh Stuarts untuk membulatkan sudut tertentu bekerja dengan baik. Jika Anda ingin membulatkan beberapa sudut seperti kiri dan kanan atas, inilah cara melakukannya

// 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
sumber
Saat menggunakan solusi ini pada UITableView-subviews (misalnya UITableViewCells atau UITableViewHeaderFooterViews), ini menghasilkan kelancaran yang buruk saat menggulir . Pendekatan lain untuk penggunaan itu adalah solusi ini dengan kinerja yang lebih baik (menambahkan sudut Radius ke semua sudut). Untuk 'menampilkan' hanya sudut tertentu yang dibulatkan (seperti sudut kanan atas dan bawah) saya menambahkan subview dengan nilai negatif pada frame.origin.xdan menetapkan cornerRadius ke lapisannya. Jika seseorang menemukan solusi yang lebih baik, saya tertarik.
anneblue
5

Terima kasih telah berbagi. Di sini saya ingin membagikan solusi pada swift 2.0 untuk referensi lebih lanjut tentang masalah ini. (untuk menyesuaikan protokol 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
sumber
4

ada jawaban yang lebih mudah dan lebih cepat yang mungkin bekerja tergantung pada kebutuhan Anda dan juga bekerja dengan bayangan. Anda dapat menyetel maskToBounds pada superlayer ke true, dan mengimbangi layer anak sehingga 2 sudutnya berada di luar batas superlayer, secara efektif memotong sudut membulat pada 2 sisi.

tentu saja ini hanya berfungsi jika Anda ingin memiliki hanya 2 sudut membulat di sisi yang sama dan konten lapisan terlihat sama saat Anda memotong beberapa piksel dari satu sisi. berfungsi dengan baik karena diagram batang hanya dibulatkan di sisi atas.

pengguna1259710
sumber
3

Lihat pertanyaan terkait ini . Anda harus menggambar persegi panjang Anda sendiri ke a CGPathdengan beberapa sudut membulat, tambahkan CGPathke Anda CGContextdan kemudian klip ke sana menggunakan CGContextClip.

Anda juga dapat menggambar persegi bulat dengan nilai alfa ke gambar dan kemudian menggunakan gambar itu untuk membuat lapisan baru yang Anda tetapkan sebagai maskproperti lapisan Anda ( lihat dokumentasi Apple ).

MrMage
sumber
2

Terlambat setengah dekade, tapi menurut saya cara orang saat ini melakukan ini tidak 100% benar. Banyak orang memiliki masalah bahwa menggunakan metode UIBezierPath + CAShapeLayer mengganggu Tata letak otomatis, terutama saat disetel di Storyboard. Tidak ada jawaban yang membahas ini, jadi saya memutuskan untuk menambahkan jawaban saya sendiri.

Ada cara yang sangat mudah untuk mengelaknya: Gambarlah sudut yang membulat di drawRect(rect: CGRect) fungsi tersebut.

Misalnya, jika saya menginginkan sudut membulat atas untuk UIView, saya akan membuat subkelas UIView dan kemudian menggunakan subkelas itu di mana saja.

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

Ini adalah cara terbaik untuk mengatasi masalah dan tidak membutuhkan waktu sama sekali untuk beradaptasi.

David
sumber
1
Ini bukanlah cara yang tepat untuk menyelesaikan masalah. Anda sebaiknya hanya menimpa drawRect(_:)jika Anda melakukan gambar kustom dalam tampilan Anda (mis. Dengan Core Graphics), jika tidak, implementasi kosong pun dapat memengaruhi performa. Membuat lapisan topeng baru setiap saat juga tidak perlu. Yang perlu Anda lakukan hanyalah memperbarui pathproperti lapisan topeng saat batas tampilan berubah, dan tempat untuk melakukannya adalah di dalam layoutSubviews()metode yang diganti .
Stuart
2

Membulatkan hanya beberapa sudut tidak akan berfungsi baik dengan pengubahan ukuran otomatis atau tata letak otomatis.

Jadi opsi lain adalah menggunakan reguler cornerRadiusdan menyembunyikan sudut yang tidak Anda inginkan di bawah tampilan lain atau di luar batas tampilan supernya memastikan itu diatur untuk memotong kontennya.

Rivera
sumber
1

Untuk menambah jawaban dan penambahan , saya membuat yang sederhana, dapat digunakan kembali UIViewdi Swift. Bergantung pada kasus penggunaan Anda, Anda mungkin ingin membuat modifikasi (hindari membuat objek pada setiap tata letak, dll.), Tetapi saya ingin membuatnya sesederhana mungkin. Ekstensi memungkinkan Anda untuk menerapkan ini ke tampilan lain (mis. UIImageView) Lebih mudah jika Anda tidak suka subclassing.

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

Inilah cara Anda menerapkannya ke 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
sumber
0

Mengakhiri jawaban Stuart, Anda dapat menggunakan metode sudut pembulatan sebagai berikut:

@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

Jadi untuk menerapkan sudut pembulatan, Anda cukup melakukan:

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

Saya sarankan untuk mendefinisikan topeng lapisan. Topeng itu sendiri harus berupa CAShapeLayerobjek dengan jalur khusus. Anda dapat menggunakan ekstensi UIView berikutnya (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
sumber