Округлений UIView за допомогою CALayers - лише деякі кути - Як?


121

У моїй програмі - є чотири кнопки, названі так:

  • Вгорі - зліва
  • Знизу - зліва
  • Вгорі - праворуч
  • Знизу - справа

Над кнопками - зображення зображення (або 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]];

Наведений вище код застосовує округлість до кожного з кутів поданого виду. Натомість я просто хотів застосувати округлість до вибраних кутів, наприклад - верхній / верхній + лівий / нижній + правий тощо.

Це можливо? Як?

Відповіді:


44

Я використовував відповідь у розділі Як створити кругленький UILabel з круглим кутом на iPhone? і код з Як робиться закруглений прямий вигляд з прозорістю на iphone? зробити цей код.

Тоді я зрозумів, що відповів неправильно на запитання (дав округлий UILabel замість UIImage), тому застосував цей код, щоб змінити його:

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

Створіть проект 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. Якщо встановити шлях маски, а потім змінити її розмір, встановивши текст, вона збереже стару маску. Буде потрібно підкласифікація та перевантаження малюнкаReRect.
користувач3099609

358

Починаючи з iOS 3.2, ви можете використовувати функціонал UIBezierPaths для створення нестандартної округлої прямої коробки (де закруглені лише вказані вами кути). Потім ви можете використовувати це як шлях до а 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;

І це все - ні возитися навколо визначення ручних фігур в Core Graphics, ні створення маскуючих зображень у Photoshop. Шар навіть не потребує недійсності. Застосування закругленого кута або зміна на новий кут настільки ж просто, як визначення нового UIBezierPathта використання його CGPathяк шляху шару маски. cornersПараметр bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:методу є бітову маску, і тому кілька кути можуть бути заокруглені по ORing їх разом.


EDIT: Додавання тіні

Якщо ви хочете додати до цього тінь, потрібно ще трохи працювати.

Оскільки " imageView.layer.mask = maskLayer" застосовує маску, тінь звичайно не відображатиметься поза нею. Трюк полягає у використанні прозорого перегляду, а потім додати два CALayerшари (шари) до шару подання: shadowLayerі roundedLayer. Обом потрібно скористатися 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
Приємно, це мені дуже допомогло в групі вибраних комірок UITableViewBackgroundViews :-)
Luke47

Все-таки додати краєвид до цього виду після того, як ви закруглили кути? Спробували наступне без успіху. `self.layer.masksToBounds = НІ; self.layer.shadowColor = [UIColor blackColor] .CGColor; self.layer.shadowRadius = 3; self.layer.shadowOffset = CGSizeMake (-3, 3); self.layer.shadowOpacity = 0,8; self.layer.shouldRasterize = ТАК; `
Кріс Вагнер

1
@ChrisWagner: Дивіться мою редакцію щодо застосування тіні до округленого виду.
Стюарт

@StuDev чудово! Я пішов із підзаглядом на тіньовий вигляд, але це набагато приємніше! Не думав додавати подібні підшари. Дякую!
Кріс Вагнер

Чому моє зображення стає білим, тоді коли я прокручую UITableView і клітина відтворюється - це працює! Чому?
Фернандо Редондо

18

Я використовував цей код у багатьох місцях свого коду, і він працює на 100% правильно. Ви можете змінити будь-який корд, змінивши одне властивість "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-відстеженнях (наприклад, UITableViewCells або UITableViewHeaderFooterViews) це призводить до поганої гладкості при прокручуванні . Ще одним підходом до цього використання є це рішення з кращою продуктивністю (воно додає кутовий Радіус у всі кути). Щоб "показати" лише окремі кути, закруглені (як, наприклад, верхній і нижній праві кути), я додав підвід з негативним значенням на нього frame.origin.xта призначив його cornerRadius своєму шару. Якщо хтось знайшов краще рішення, мені цікаво.
anneblue

11

В iOS 11 тепер ми можемо обходити лише деякі кути

let view = UIView()

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

7

Розширення CALayer із синтаксисом 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
    }
}

Його можна використовувати як:

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

5

Приклад Стюартів для закруглення конкретних кутів чудово працює. Якщо ви хочете округлити декілька кутів, таких як верхній лівий і правий, це зробити

// 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-відстеженнях (наприклад, UITableViewCells або UITableViewHeaderFooterViews) це призводить до поганої гладкості при прокручуванні . Ще одним підходом до цього використання є це рішення з кращою продуктивністю (воно додає кутовий Радіус у всі кути). Щоб "показати" лише окремі кути, закруглені (як, наприклад, верхній і нижній праві кути), я додав підвід з негативним значенням на нього frame.origin.xта призначив його cornerRadius своєму шару. Якщо хтось знайшов краще рішення, мені цікаво.
anneblue

5

Дякую, що поділились. Тут я хотів би поділитися рішенням на швидкій версії 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

4

є простіша та швидша відповідь, яка може працювати залежно від ваших потреб, а також працює з тінями. ви можете встановити maskToBounds на супершарі на істинне та змістити дочірні шари так, щоб 2 їх кути знаходилися поза межами супершару, ефективно вирізаючи закруглені кути на 2 сторони.

звичайно, це працює лише тоді, коли ви хочете мати лише 2 закруглені кути на одній стороні, а вміст шару виглядає однаково, коли ви відрізаєте кілька пікселів з одного боку. чудово підходить для того, щоб діаграми закруглені лише у верхній частині.


3

Дивіться це пов'язане питання . Вам доведеться намалювати свій власний прямокутник до a CGPathз деякими закругленими кутами, додати його CGPathдо свого, CGContextа потім зафіксувати його за допомогою CGContextClip.

Ви також можете намалювати закруглену пряму зі значеннями альфа до зображення, а потім використати це зображення для створення нового шару, який ви встановите як maskвластивість свого шару ( див. Документацію Apple ).


2

Запізнилося на пів десятиліття, але я думаю, що нинішній спосіб людей це не на 100%. У багатьох людей виникло питання про те, що використання методу UIBezierPath + CAShapeLayer перешкоджає автоматичному розташуванню, особливо коли він встановлений на дошці розкадрувань. Відповіді на це немає, тому я вирішив додати свою.

Існує дуже простий спосіб обійти це: Намалюйте закруглені кути у drawRect(rect: CGRect)функції.

Наприклад, якщо я хотів би верхньо закруглені кути для UIView, я б підклас UIView, а потім використовував цей підклас, де це можливо.

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

Це найкращий спосіб перемогти проблему і зовсім не потребує часу на адаптацію.


1
Це не правильний спосіб вирішити проблему. Ви можете переосмислити, лише drawRect(_:)якщо ви робите власні малюнки у своєму поданні (наприклад, із Core Graphics), інакше навіть порожня реалізація може вплинути на продуктивність. Створення нового шару маски щоразу теж непотрібне. Все, що вам потрібно зробити, це оновити pathвластивість шару маски, коли змінюються межі перегляду, а місце, що потрібно зробити, знаходиться в межах переоцінки layoutSubviews()методу.
Стюарт

2

Округлення лише деяких кутів не буде добре грати з автоматичним зміною розміру або автоматичним розташуванням.

Отже, інший варіант - використовувати звичайні cornerRadiusта ховати кути, які ви не хочете, під іншим видом або поза межами його перегляду, переконуючись, що він налаштований на відсікання його вмісту.


1

Щоб додати до відповіді та доповнення , я створив простий, багаторазовий використання UIViewу Swift. Залежно від вашого випадку використання, ви можете внести зміни (уникати створення об'єктів на кожному макеті тощо), але я хотів би зробити це максимально простим. Розширення дозволяє застосувати це до іншого вигляду (наприклад UIImageView) простіше, якщо вам не подобається підкласифікація.

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

Ось як би ви застосували його до 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

Підводячи відповідь Стюарта, ви можете мати метод кутового округлення як наступний:

@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

Отже, щоб застосувати закруглений кут, ви просто зробите:

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

0

Я б запропонував визначити маску шару. Сама маска повинна бути CAShapeLayerоб’єктом із виділеним шляхом. Ви можете використовувати наступне розширення 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
   }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.