Rounded UIView using CALayers - only some corners - How?

后端 未结 14 1998
南方客
南方客 2020-11-22 13:53

In my application - there are four buttons named as follows:

  • Top - left
  • Bottom - left
  • Top - right
  • Bottom - right

Abov

相关标签:
14条回答
  • 2020-11-22 14:19

    I'd suggest defining a layer's mask. The mask itself should be a CAShapeLayer object with a dedicated path. You can use the next UIView extension (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
       }
    }
    
    0 讨论(0)
  • 2020-11-22 14:20

    Starting in iOS 3.2, you can use the functionality of UIBezierPaths to create an out-of-the-box rounded rect (where only corners you specify are rounded). You can then use this as the path of a CAShapeLayer, and use this as a mask for your view's layer:

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

    And that's it - no messing around manually defining shapes in Core Graphics, no creating masking images in Photoshop. The layer doesn't even need invalidating. Applying the rounded corner or changing to a new corner is as simple as defining a new UIBezierPath and using its CGPath as the mask layer's path. The corners parameter of the bezierPathWithRoundedRect:byRoundingCorners:cornerRadii: method is a bitmask, and so multiple corners can be rounded by ORing them together.


    EDIT: Adding a shadow

    If you're looking to add a shadow to this, a little more work is required.

    Because "imageView.layer.mask = maskLayer" applies a mask, a shadow will not ordinarily show outside of it. The trick is to use a transparent view, and then add two sublayers (CALayers) to the view's layer: shadowLayer and roundedLayer. Both need to make use of the UIBezierPath. The image is added as the content of 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];
    
    0 讨论(0)
  • 2020-11-22 14:21

    Rounding only some corners won't play nice with auto resizing or auto layout.

    So another option is to use regular cornerRadius and hide the corners you don't want under another view or outside its superview bounds making sure it is set to clip its contents.

    0 讨论(0)
  • 2020-11-22 14:21

    To add to to the answer and the addition, I created a simple, reusable UIView in Swift. Depending on your use case, you might want to make modifications (avoid creating objects on every layout etc.), but I wanted to keep it as simple as possible. The extension allows you to apply this to other view's (ex. UIImageView) easier if you do not like 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)
        }
    }
    

    Here's how you would apply it to 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)
        }
    }
    
    0 讨论(0)
  • 2020-11-22 14:22

    CALayer extension with Swift 3+ syntax

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

    It can be used like:

    let layer: CALayer = yourView.layer
    layer.round(roundedRect: yourView.bounds, byRoundingCorners: [.bottomLeft, .topLeft], cornerRadii: CGSize(width: 5, height: 5))
    
    0 讨论(0)
  • 2020-11-22 14:23

    there is an easier and faster answer that may work depending on your needs and also works with shadows. you can set maskToBounds on the superlayer to true, and offset the child layers so that 2 of their corners are outside the superlayer bounds, effectively cutting the rounded corners on 2 sides away.

    of course this only works when you want to have only 2 rounded corners on the same side and the content of the layer looks the same when you cut off a few pixels from one side. works great for having bar charts rounded only on the top side.

    0 讨论(0)
提交回复
热议问题