如何为BezierPath创建的自定义TicketView添加贴合路径的阴影?
问题描述
我用BezierPath创建了一个TicketView,样式如下:
当前给该视图添加阴影时遇到问题:视图左侧存在一张图片,TicketView的圆角需作为遮罩作用于这张图片,但添加遮罩后阴影无法正常显示。目前唯一临时方案是在TicketView后方添加容器视图并为其添加阴影,但该方案无法让阴影贴合自定义路径。
请问是否存在方法,既能添加贴合自定义路径的阴影,又能保留遮罩效果?
参考代码
import UIKit /// 左侧带圆角、右侧带锯齿形状的卡片视图 class TicketView: UIView { /// 视图左侧的圆角半径 let radius: CGFloat /// 锯齿的宽度 let zigZagsWidth: CGFloat /// 锯齿在卡片视图内的起始位置(1.0 = 视图末端) let zigZagStartPosition: CGFloat public init( radius: CGFloat = 10, zigZagsWidth: CGFloat = 5, zigZagStartPosition: CGFloat = 1.0, frame: CGRect = .zero ) { self.radius = radius self.zigZagsWidth = zigZagsWidth self.zigZagStartPosition = zigZagStartPosition super.init(frame: frame) self.backgroundColor = .clear } required init?(coder: NSCoder) { fatalError("init(coder:) 尚未实现") } override func layoutSubviews() { super.layoutSubviews() layer.shadowColor = UIColor.black.cgColor layer.shadowOpacity = 0.5 layer.shadowRadius = 5.0 layer.shadowOffset = CGSize(width: 0, height: 0) layer.shadowPath = (layer.mask as? CAShapeLayer)?.path } public override func draw(_ rect: CGRect) { super.draw(rect) guard let context = UIGraphicsGetCurrentContext() else { return } let path = UIBezierPath() // 左上部分 let topLeftCorner = CGPoint(x: bounds.minX, y: bounds.minY) let bottomLeftCorner = CGPoint(x: bounds.minX, y: bounds.maxY) path.move(to: CGPoint(x: topLeftCorner.x, y: topLeftCorner.y)) path.addArc(withCenter: CGPoint(x: topLeftCorner.x + radius, y: topLeftCorner.y + radius), radius: radius, startAngle: .pi, endAngle: .pi * 1.5, clockwise: true) // 锯齿部分 let zigzagHeight: CGFloat = rect.size.height / (rect.size.height / 7.3) let zigzagWidth: CGFloat = zigZagsWidth let numberOfSegments = Int(rect.size.height / zigzagHeight) + 1 for index in 0..<numberOfSegments { let inY = CGFloat(index) * zigzagHeight let inX = (index % 2 == 0) ? rect.size.width * zigZagStartPosition : rect.size.width * zigZagStartPosition - zigzagWidth path.addLine(to: CGPoint(x: inX, y: inY)) let zigZagCurveRadius: CGFloat = 2 let zigZagCurveCenter = CGPoint( x: inX + zigZagCurveRadius, y: inY + zigZagCurveRadius ) path.addArc(withCenter: zigZagCurveCenter, radius: zigZagCurveRadius, startAngle: .pi, endAngle: .pi, clockwise: true) } // 左下部分 path.addLine(to: CGPoint(x: bottomLeftCorner.x + radius, y: bottomLeftCorner.y)) path.addArc(withCenter: CGPoint(x: bottomLeftCorner.x + radius, y: bottomLeftCorner.y - radius), radius: radius, startAngle: .pi * 1.5, endAngle: .pi, clockwise: true) path.close() context.setFillColor(UIColor.white.cgColor) path.fill() // 遮罩 let mask = CAShapeLayer() mask.path = path.cgPath layer.mask = mask } }
解决方案
核心思路是将阴影与遮罩分离处理:用独立图层绘制贴合自定义路径的阴影,同时保留原视图的遮罩功能实现图片裁切。具体实现步骤如下:
1. 抽离路径生成逻辑
将自定义Ticket路径的代码单独封装成方法,避免重复创建路径:
private func createTicketPath() -> UIBezierPath { let path = UIBezierPath() let topLeftCorner = CGPoint(x: bounds.minX, y: bounds.minY) let bottomLeftCorner = CGPoint(x: bounds.minX, y: bounds.maxY) // 左上圆角 path.move(to: CGPoint(x: topLeftCorner.x, y: topLeftCorner.y)) path.addArc(withCenter: CGPoint(x: topLeftCorner.x + radius, y: topLeftCorner.y + radius), radius: radius, startAngle: .pi, endAngle: .pi * 1.5, clockwise: true) // 锯齿部分 let zigzagHeight: CGFloat = bounds.size.height / (bounds.size.height / 7.3) let zigzagWidth: CGFloat = zigZagsWidth let numberOfSegments = Int(bounds.size.height / zigzagHeight) + 1 for index in 0..<numberOfSegments { let inY = CGFloat(index) * zigzagHeight let inX = (index % 2 == 0) ? bounds.size.width * zigZagStartPosition : bounds.size.width * zigZagStartPosition - zigzagWidth path.addLine(to: CGPoint(x: inX, y: inY)) let zigZagCurveRadius: CGFloat = 2 let zigZagCurveCenter = CGPoint( x: inX + zigZagCurveRadius, y: inY + zigZagCurveRadius ) path.addArc(withCenter: zigZagCurveCenter, radius: zigZagCurveRadius, startAngle: .pi, endAngle: .pi, clockwise: true) } // 左下圆角 path.addLine(to: CGPoint(x: bottomLeftCorner.x + radius, y: bottomLeftCorner.y)) path.addArc(withCenter: CGPoint(x: bottomLeftCorner.x + radius, y: bottomLeftCorner.y - radius), radius: radius, startAngle: .pi * 1.5, endAngle: .pi, clockwise: true) path.close() return path }
2. 重构图层结构
移除draw(_:)方法,改用两个独立图层分别处理阴影和内容遮罩:
private let contentLayer = CAShapeLayer() private let shadowLayer = CAShapeLayer() public init( radius: CGFloat = 10, zigZagsWidth: CGFloat = 5, zigZagStartPosition: CGFloat = 1.0, frame: CGRect = .zero ) { self.radius = radius self.zigZagsWidth = zigZagsWidth self.zigZagStartPosition = zigZagStartPosition super.init(frame: frame) setupLayers() } private func setupLayers() { backgroundColor = .clear // 配置阴影图层(放在最底层) shadowLayer.fillColor = UIColor.black.cgColor shadowLayer.shadowColor = UIColor.black.cgColor shadowLayer.shadowOpacity = 0.5 shadowLayer.shadowRadius = 5.0 shadowLayer.shadowOffset = .zero layer.insertSublayer(shadowLayer, at: 0) // 配置内容图层(承载白色填充,同时作为遮罩) contentLayer.fillColor = UIColor.white.cgColor layer.addSublayer(contentLayer) } override func layoutSubviews() { super.layoutSubviews() let ticketPath = createTicketPath() // 更新阴影路径,确保贴合Ticket形状 shadowLayer.path = ticketPath.cgPath // 扩展阴影图层边界,避免阴影被视图裁切 shadowLayer.frame = bounds.insetBy(dx: -shadowLayer.shadowRadius, dy: -shadowLayer.shadowRadius) // 更新内容图层路径,并将其设为视图的遮罩,实现图片裁切 contentLayer.path = ticketPath.cgPath layer.mask = contentLayer } required init?(coder: NSCoder) { fatalError("init(coder:) 尚未实现") }
原理说明
- 遮罩功能:通过将
contentLayer设为TicketView的layer.mask,所有子视图(包括左侧图片)都会被自定义路径裁切,保留圆角效果。 - 阴影贴合:独立的
shadowLayer使用完全相同的自定义路径绘制阴影,且图层边界留出阴影半径空间,确保阴影完整显示且完全贴合Ticket形状。 - 层级分离:阴影图层放在最底层,内容图层和子视图在上层,避免阴影被遮挡。
内容的提问来源于stack exchange,提问作者chr0x
相关产品推荐
相关产品推荐

