具有阴影、圆角和自定义drawRect的UIView

61

我需要创建一个自定义的 UIView,它将具有圆角、边框和阴影,并且覆盖其 drawRect() 方法以提供自定义绘图代码,其中在视图中绘制了几条直线(因为需要渲染许多这样的视图,所以我需要使用一种快速轻量级的方法)。

我目前遇到的问题是,在视图类中覆盖 drawRect() 后,阴影不再适用于圆角(即使没有任何自定义代码)。请参见附带的图像以查看差异:

enter image description here

在视图控制器中,我正在使用以下代码:

    view.layer.cornerRadius = 10;
    view.layer.masksToBounds = true;

    view.layer.borderColor = UIColor.grayColor().CGColor;
    view.layer.borderWidth = 0.5;

    view.layer.contentsScale = UIScreen.mainScreen().scale;
    view.layer.shadowColor = UIColor.blackColor().CGColor;
    view.layer.shadowOffset = CGSizeZero;
    view.layer.shadowRadius = 5.0;
    view.layer.shadowOpacity = 0.5;
    view.layer.masksToBounds = false;
    view.clipsToBounds = false;

在被覆盖的drawContext()中,我会使用类似以下的代码:

    var context:CGContext = UIGraphicsGetCurrentContext();
    CGContextSetStrokeColorWithColor(context, UIColor.redColor().CGColor);
    // Draw them with a 2.0 stroke width so they are a bit more visible.
    CGContextSetLineWidth(context, 2.0);
    CGContextMoveToPoint(context, 0.0, 0.0); //start at this point
    CGContextAddLineToPoint(context, 20.0, 20.0); //draw to this point
    CGContextStrokePath(context);

但正如上面所说,即使没有添加此代码,阴影问题仍会发生。

除了这种方法之外,是否有其他/更好的方法可以将轻量级元素绘制到视图上,而且与圆角和阴影兼容?我不想向视图添加任何不必要的额外视图或图像上下文,因为它们需要轻巧和高性能。

16个回答

99

这个有点棘手。 UIViewclipsToBounds 是必要的,以获得圆角效果。但是 CALayermasksToBounds 必须设置为false,否则阴影不可见。不知怎么的,如果不覆盖 drawRect,一切都能正常工作,但实际上不应该这样。

解决方案是创建一个父视图来提供阴影(在下面的演示中,这是shadowView)。您可以在 Playground 中测试以下内容:

class MyView : UIView {
    override func drawRect(rect: CGRect) {
        let c = UIGraphicsGetCurrentContext()
        CGContextAddRect(c, CGRectMake(10, 10, 80, 80))
        CGContextSetStrokeColorWithColor(c , UIColor.redColor().CGColor)
        CGContextStrokePath(c)
    }
}

let superview = UIView(frame: CGRectMake(0, 0, 200, 200))

let shadowView = UIView(frame: CGRectMake(50, 50, 100, 100))
shadowView.layer.shadowColor = UIColor.blackColor().CGColor
shadowView.layer.shadowOffset = CGSizeZero
shadowView.layer.shadowOpacity = 0.5
shadowView.layer.shadowRadius = 5

let view = MyView(frame: shadowView.bounds)
view.backgroundColor = UIColor.whiteColor()
view.layer.cornerRadius = 10.0
view.layer.borderColor = UIColor.grayColor().CGColor
view.layer.borderWidth = 0.5
view.clipsToBounds = true

shadowView.addSubview(view)
superview.addSubview(shadowView)

结果:

enter image description here


谢谢您,Mundi!我想在任何情况下都无法避免使用第二个视图来创建阴影。感谢您的解释和代码示例! - BadmintonCat
处理剪裁边界和 maskToBounds 冲突的好答案。 - cloudcal

48
我编写了一个小扩展程序,用于管理UIView的圆角和投影。由于变量是@IBInspectable类型,因此一切都可以直接在Storyboard中设置!
//
//  UIView extensions.swift
//
//  Created by Frédéric ADDA on 25/07/2016.
//  Copyright © 2016 Frédéric ADDA. All rights reserved.
//

import UIKit

extension UIView {

    @IBInspectable var shadow: Bool {
        get {
            return layer.shadowOpacity > 0.0
        }
        set {
            if newValue == true {
                self.addShadow()
            }
        }
    }

    @IBInspectable var cornerRadius: CGFloat {
        get {
            return self.layer.cornerRadius
        }
        set {
            self.layer.cornerRadius = newValue

            // Don't touch the masksToBound property if a shadow is needed in addition to the cornerRadius
            if shadow == false {
                self.layer.masksToBounds = true
            }
        }
    }


    func addShadow(shadowColor: CGColor = UIColor.black.cgColor,
               shadowOffset: CGSize = CGSize(width: 1.0, height: 2.0),
               shadowOpacity: Float = 0.4,
               shadowRadius: CGFloat = 3.0) {
        layer.shadowColor = shadowColor
        layer.shadowOffset = shadowOffset
        layer.shadowOpacity = shadowOpacity
        layer.shadowRadius = shadowRadius
    }
}

这是在画板中的效果:

storyboard

效果如下:

enter image description here

有一个要求:不要通过修改视图(无论是在代码中还是在IB中)的clipToBounds属性或者层的masksToBounds属性来实现。

注:在tableView中可能无法实现此效果,因为UITableView会自动触发clipToBounds,这样我们就无法添加投影效果。

编辑:正如Claudia Fitero所指出的,您需要在向其添加阴影的视图周围留出一小段间距,否则阴影将不可见。通常留出2像素的间距就足够了(具体取决于阴影半径)。


1
扩展程序规则。这是一个巨大的时间节省者。谢谢,Frederic! - Vitalii
2
对我没用,看看我在另一个帖子中的帖子:https://dev59.com/2m445IYBdhLWcg3wpb8P#43958505 - Thomás Pereira

23

视图层内的任何内容都会被阴影覆盖。当你禁用剪切时,整个层矩形将填充默认的backgroundColor,因此阴影也变成了矩形。不要使用圆形掩模来剪切它,而是使层的内容变成圆形,并自己绘制它们。并且layer的边框是在其边界周围绘制的,所以你也需要自己绘制它。

例如,在backgroundColor设置器中,将实际背景颜色设置为clearColor,并在drawRect中使用传递的颜色来绘制一个圆角矩形。

在下面的示例中,我将属性声明为IBInspectable,并将整个类声明为IBDesignable,这样一切都可以在Storyboard中设置。这样,你甚至可以使用默认的背景选择器来更改你的圆角矩形颜色。

Swift

@IBDesignable class RoundRectView: UIView {

    @IBInspectable var cornerRadius: CGFloat = 0.0
    @IBInspectable var borderColor: UIColor = UIColor.blackColor()
    @IBInspectable var borderWidth: CGFloat = 0.5
    private var customBackgroundColor = UIColor.whiteColor()
    override var backgroundColor: UIColor?{
        didSet {
            customBackgroundColor = backgroundColor!
            super.backgroundColor = UIColor.clearColor()
        }
    }

    func setup() {
        layer.shadowColor = UIColor.blackColor().CGColor;
        layer.shadowOffset = CGSizeZero;
        layer.shadowRadius = 5.0;
        layer.shadowOpacity = 0.5;
        super.backgroundColor = UIColor.clearColor()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.setup()
    }

    override func drawRect(rect: CGRect) {
        customBackgroundColor.setFill()
        UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius ?? 0).fill()

        let borderRect = CGRectInset(bounds, borderWidth/2, borderWidth/2)
        let borderPath = UIBezierPath(roundedRect: borderRect, cornerRadius: cornerRadius - borderWidth/2)
        borderColor.setStroke()
        borderPath.lineWidth = borderWidth
        borderPath.stroke()

        // whatever else you need drawn
    }
}

Swift 3

@IBDesignable class RoundedView: UIView {

@IBInspectable var cornerRadius: CGFloat = 0.0
@IBInspectable var borderColor: UIColor = UIColor.black
@IBInspectable var borderWidth: CGFloat = 0.5
private var customBackgroundColor = UIColor.white
override var backgroundColor: UIColor?{
    didSet {
        customBackgroundColor = backgroundColor!
        super.backgroundColor = UIColor.clear
    }
}

func setup() {
    layer.shadowColor = UIColor.black.cgColor
    layer.shadowOffset = CGSize.zero
    layer.shadowRadius = 5.0
    layer.shadowOpacity = 0.5
    super.backgroundColor = UIColor.clear
}

override init(frame: CGRect) {
    super.init(frame: frame)
    self.setup()
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    self.setup()
}

override func draw(_ rect: CGRect) {
    customBackgroundColor.setFill()
    UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius ?? 0).fill()

    let borderRect = bounds.insetBy(dx: borderWidth/2, dy: borderWidth/2)
    let borderPath = UIBezierPath(roundedRect: borderRect, cornerRadius: cornerRadius - borderWidth/2)
    borderColor.setStroke()
    borderPath.lineWidth = borderWidth
    borderPath.stroke()

    // whatever else you need drawn
}
}

Objective-C .h

IB_DESIGNABLE
@interface RoundRectView : UIView
@property IBInspectable CGFloat cornerRadius;
@property IBInspectable UIColor *borderColor;
@property IBInspectable CGFloat borderWidth;
@end

Objective-C .m

@interface RoundRectView()
@property UIColor *customBackgroundColor;
@end

@implementation RoundRectView

-(void)setup{
    self.layer.shadowColor = [UIColor blackColor].CGColor;
    self.layer.shadowOffset = CGSizeZero;
    self.layer.shadowRadius = 5.0;
    self.layer.shadowOpacity = 0.5;
    [super setBackgroundColor:[UIColor clearColor]];
}

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self setup];
    }
    return self;
}

- (instancetype)initWithCoder:(NSCoder *)coder
{
    self = [super initWithCoder:coder];
    if (self) {
        [self setup];
    }
    return self;
}

-(void)setBackgroundColor:(UIColor *)backgroundColor{
    self.customBackgroundColor = backgroundColor;
    super.backgroundColor = [UIColor clearColor];
}

-(void)drawRect:(CGRect)rect{
    [self.customBackgroundColor setFill];
    [[UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:self.cornerRadius] fill];

    CGFloat borderInset = self.borderWidth/2;
    CGRect borderRect = CGRectInset(self.bounds, borderInset, borderInset);
    UIBezierPath *borderPath = [UIBezierPath bezierPathWithRoundedRect:borderRect cornerRadius:self.cornerRadius - borderInset];
    [self.borderColor setStroke];
    borderPath.lineWidth = self.borderWidth;
    [borderPath stroke];

    // whatever else you need drawn
}

@end

结果


1
这实际上是一个更优雅的解决方案!谢谢! - TonyGW
@Hodit 谢谢你,你节省了我很多时间。谢谢 :-) - Vaibhav Limbani
@VaibhavLimbani 很高兴能帮到你 :-) - Hodit

8

这是Hodit的答案的Swift3版本,我使用它并在此处找到它,并对XCode 8进行了一般性的更正。完美运行!

@IBDesignable class RoundRectView: UIView {

@IBInspectable var cornerRadius: CGFloat = 0.0
@IBInspectable var borderColor: UIColor = UIColor.black
@IBInspectable var borderWidth: CGFloat = 0.5
private var customBackgroundColor = UIColor.white
override var backgroundColor: UIColor?{
    didSet {
        customBackgroundColor = backgroundColor!
        super.backgroundColor = UIColor.clear
    }
}

func setup() {
    layer.shadowColor = UIColor.black.cgColor;
    layer.shadowOffset = CGSize.zero
    layer.shadowRadius = 5.0;
    layer.shadowOpacity = 0.5;
    super.backgroundColor = UIColor.clear
}

override init(frame: CGRect) {
    super.init(frame: frame)
    self.setup()
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    self.setup()
}

override func draw(_ rect: CGRect) {
    customBackgroundColor.setFill()
    UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius ?? 0).fill()

    let borderRect = bounds.insetBy(dx: borderWidth/2, dy: borderWidth/2)
    let borderPath = UIBezierPath(roundedRect: borderRect, cornerRadius: cornerRadius - borderWidth/2)
    borderColor.setStroke()
    borderPath.lineWidth = borderWidth
    borderPath.stroke()

    // whatever else you need drawn
}
}

是的,可以。只需在tableViewCell中拉起一个UIView,并从RoundRectView继承它的类即可。虽然我没有尝试过,但应该可以正常工作。 - amagain

5
我发现以下链接对理解设置阴影非常有帮助: 如何向UIView添加阴影 为了设置UIVIEW的圆角,只需要在界面构建器中设置layer.cornerRadius值,请查看屏幕截图。 enter image description here

4

SWIFT 3解决方案

改编自Mundi的答案

class MyView : UIView {
        override func draw(_ rect: CGRect) {
            let c = UIGraphicsGetCurrentContext()
            c!.addRect(CGRect(x: 10, y: 10, width: 80, height: 80))
            c!.setStrokeColor(UIColor.red.cgColor)
            c!.strokePath()
        }
    }

let superview = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))

let shadowView = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 100))
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOffset = CGSize.zero
shadowView.layer.shadowOpacity = 0.5
shadowView.layer.shadowRadius = 5

let view = MyView(frame: shadowView.bounds)
view.backgroundColor = UIColor.white
view.layer.cornerRadius = 10.0
view.layer.borderColor = UIColor.gray.cgColor
view.layer.borderWidth = 0.5
view.clipsToBounds = true

shadowView.addSubview(view)
superview.addSubview(shadowView)

3
在Swift 4.1中,为了将UIView的圆角处理,我创建了以下的UIView扩展。
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var viewOuter: UIView!
    @IBOutlet weak var viewInner: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
        viewOuter.backgroundColor = UIColor.clear
        viewInner.roundCorners(15.0)
        viewOuter.addViewShadow()
    }
}
extension UIView {
    public func roundCorners(_ cornerRadius: CGFloat) {
        self.layer.cornerRadius = cornerRadius
        self.clipsToBounds = true
        self.layer.masksToBounds = true
    }

    public func addViewShadow() {
        DispatchQueue.main.asyncAfter(deadline: (.now() + 0.2)) {
            let shadowLayer = CAShapeLayer()
            shadowLayer.path = UIBezierPath(roundedRect: self.bounds, cornerRadius: 15).cgPath
            shadowLayer.fillColor = UIColor.white.cgColor

            shadowLayer.shadowColor = UIColor.lightGray.cgColor
            shadowLayer.shadowPath = shadowLayer.path
            shadowLayer.shadowOffset = CGSize(width: 2.6, height: 2.6)
            shadowLayer.shadowOpacity = 0.8
            shadowLayer.shadowRadius = 8.0
            self.layer.insertSublayer(shadowLayer, at: 0)
        }
    }
}

enter image description here enter image description here


2
我认为你误解了这个问题。问题不是关于如何在视图中添加圆角半径,而是如何在圆角视图上添加一个圆形阴影。请更新你的回答 :-) - Iraniya Naynesh

3
我使用这个扩展来处理 UIView
Import UIKit

extension UIView {
    
    /// A property that accesses the backing layer's opacity.
    @IBInspectable
    open var opacity: Float {
        get {
            return layer.opacity
        }
        set(value) {
            layer.opacity = value
        }
    }
    
    /// A property that accesses the backing layer's shadow
    @IBInspectable
    open var shadowColor: UIColor? {
        get {
            guard let v = layer.shadowColor else {
                return nil
            }
            
            return UIColor(cgColor: v)
        }
        set(value) {
            layer.shadowColor = value?.cgColor
        }
    }
    
    /// A property that accesses the backing layer's shadowOffset.
    @IBInspectable
    open var shadowOffset: CGSize {
        get {
            return layer.shadowOffset
        }
        set(value) {
            layer.shadowOffset = value
        }
    }
    
    /// A property that accesses the backing layer's shadowOpacity.
    @IBInspectable
    open var shadowOpacity: Float {
        get {
            return layer.shadowOpacity
        }
        set(value) {
            layer.shadowOpacity = value
        }
    }
    
    /// A property that accesses the backing layer's shadowRadius.
    @IBInspectable
    open var shadowRadius: CGFloat {
        get {
            return layer.shadowRadius
        }
        set(value) {
            layer.shadowRadius = value
        }
    }
    
    /// A property that accesses the backing layer's shadowPath.
    @IBInspectable
    open var shadowPath: CGPath? {
        get {
            return layer.shadowPath
        }
        set(value) {
            layer.shadowPath = value
        }
    }
    
    
    /// A property that accesses the layer.cornerRadius.
    @IBInspectable
    open var cornerRadius: CGFloat {
        get {
            return layer.cornerRadius
        }
        set(value) {
            layer.cornerRadius = value
        }
    }
    
    
    /// A property that accesses the layer.borderWith.
    @IBInspectable
    open var borderWidth: CGFloat {
        get {
            return layer.borderWidth
        }
        set(value) {
            layer.borderWidth = value
        }
    }
    
    /// A property that accesses the layer.borderColor property.
    @IBInspectable
    open var borderColor: UIColor? {
        get {
            guard let bcolor = layer.borderColor else {
                return nil
            }
            return UIColor(cgColor: bcolor)
        }
        set(value) {
            layer.borderColor = value?.cgColor
        }
    }
}

3

Swift 3

我制作了一个UIView扩展,基本上与Mundi建议的想法相同:

extension UIView {

func addShadowView() {
    //Remove previous shadow views
    superview?.viewWithTag(119900)?.removeFromSuperview()

    //Create new shadow view with frame
    let shadowView = UIView(frame: frame)
    shadowView.tag = 119900
    shadowView.layer.shadowColor = UIColor.black.cgColor
    shadowView.layer.shadowOffset = CGSize(width: 2, height: 3)
    shadowView.layer.masksToBounds = false

    shadowView.layer.shadowOpacity = 0.3
    shadowView.layer.shadowRadius = 3
    shadowView.layer.shadowPath = UIBezierPath(rect: bounds).cgPath
    shadowView.layer.rasterizationScale = UIScreen.main.scale
    shadowView.layer.shouldRasterize = true

    superview?.insertSubview(shadowView, belowSubview: self)
}}

使用:

class MyCVCell: UICollectionViewCell {

@IBOutlet weak var containerView: UIView!

override func awakeFromNib() {
    super.awakeFromNib()
}

override func draw(_ rect: CGRect) {
    super.draw(rect)
    containerView.addShadowView()
}}

Result


2
圆角半径怎么样? 你什么时候设置它? - Bptstmlgt
并非所有的代码都提供了,以便能够获得与上面相同的结果。 - Jimbo
谢谢,它正在工作。只需要提到conrerRaidus需要设置在主视图上。圆角半径与阴影无关。 - Hitesh Agarwal
当方向改变时,阴影没有改变! - YodagamaHeshan

2
解决方案似乎比问题可能暗示的要简单得多。我在我的一个视图中遇到了这个问题,使用了@Hodit答案的核心部分来使它正常运作。实际上,这是你所需要的全部内容:
- (void) drawRect:(CGRect)rect {
    // make sure the background is set to a transparent color using IB or code
    // e.g.: self.backgroundColor = [UIColor clearColor]; 

    // draw a rounded rect in the view
    [[UIColor whiteColor] setFill];
    [[UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:5.0] fill];

    // apply shadow if you haven't already
    self.layer.masksToBounds = NO;
    self.layer.shadowColor = [[UIColor blackColor] CGColor];
    self.layer.shadowOffset = CGSizeMake(0.0,3.0);
    self.layer.shadowRadius= 1.0;
    self.layer.shadowOpacity = 0.1;

    // more code here

}

请注意,这不会剪裁子视图。在视图中位于0,0位置的任何内容都将重叠显示在可见的左上角圆角处。

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