You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为BezierPath创建的自定义TicketView添加贴合路径的阴影?

问题描述

我用BezierPath创建了一个TicketView,样式如下:
Ticket View

当前给该视图添加阴影时遇到问题:视图左侧存在一张图片,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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 12:07:55