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

Swift实现贝塞尔路径(三角形)内的波浪进度视图问题

问题描述

我正在制作一个包含双动态波浪的进度视图,要求波浪被限制在三角形贝塞尔路径内,且根据进度值填充路径。目前代码运行后,三角形被渐变颜色完全填充,波浪虽按进度值移动,但未被限制在三角形范围内,不符合预期。

当前代码

class WaveView: UIView {
    
    private let firstLayer = CAShapeLayer()
    private let secondLayer = CAShapeLayer()
    private let gradientLayer = CAGradientLayer()
    private let shapeLayer = CAShapeLayer()
    
    private var trianglePath: UIBezierPath {
        let w = bounds.width
        let h = bounds.height
        let path = UIBezierPath()
        path.move(to: CGPoint(x: w / 2, y: 0))
        path.addLine(to: CGPoint(x: w, y: h))
        path.addLine(to: CGPoint(x: 0, y: h))
        path.close()
        return path
    }
    
    private var firstColor: UIColor = .clear
    private var secondColor: UIColor = .clear
    
    private var offset: CGFloat = 0.0
    private let twoPie: CGFloat = 2.0 * .pi
    
    var showSingleWave: Bool = false
    private var start: Bool = false
    
    private(set) var progress: CGFloat = 0.0
    
    private var waveHeight: CGFloat = 0.0
    
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupViews() {

        waveHeight = 20.0
        
        firstColor = .cyan
        secondColor = .cyan.withAlphaComponent(0.4)
        
        createStarLayer()
        
        createFirstLayer()
        
        if !showSingleWave {
            createSecondLayer()
        }
        
        createGradientLayer()
    }
    
    private func createStarLayer() {
        
        shapeLayer.path = trianglePath.cgPath
        //shapeLayer.masksToBounds = true
        
    }
    
    private func createGradientLayer() {
        gradientLayer.frame = bounds
        gradientLayer.colors = [UIColor.blue.cgColor, UIColor.red.cgColor]
        gradientLayer.startPoint = CGPoint(x: 0.5, y: 1.0)
        gradientLayer.endPoint = CGPoint(x: 0.5, y: 0.0)
        
        gradientLayer.mask = shapeLayer
        
        layer.insertSublayer(gradientLayer, at: 0)
    }
    
    private func createFirstLayer() {
        firstLayer.frame = bounds
        firstLayer.anchorPoint = .zero
        firstLayer.fillColor = firstColor.cgColor
        
        shapeLayer.addSublayer(firstLayer)

    }
    
    private func createSecondLayer() {
        secondLayer.frame = bounds
        secondLayer.anchorPoint = .zero
        secondLayer.fillColor = secondColor.cgColor
        
        shapeLayer.addSublayer(secondLayer)
    }
    
    func setProgress(_ pr: CGFloat) {
        progress = min(pr, 1.0)
        
        let top: CGFloat = progress * bounds.height
        firstLayer.setValue(bounds.width - top, forKeyPath: "position.y")
        secondLayer.setValue(bounds.width - top, forKeyPath: "position.y")

        if !start {
            DispatchQueue.main.async {
                self.startAnim()
            }
        }
    }
    
    private func startAnim() {
        start = true
        waterWaveAnim()
    }
    
    private func waterWaveAnim() {
        let w = bounds.width
        let h = bounds.height
        let W = w * 10
        
        let bezier = UIBezierPath()
        
        let startOffsetY = waveHeight * CGFloat(sinf(Float(offset * twoPie / w)))
        var originOffsetY: CGFloat = 0.0
        
        bezier.move(to: CGPoint(x: 0.0, y: startOffsetY))
        
        for i in stride(from: 0.0, to: W, by: 20.0) {
            originOffsetY = waveHeight * CGFloat(sinf(Float(twoPie / w * i + offset * twoPie / w)))
            bezier.addLine(to: CGPoint(x: i, y: originOffsetY))
        }
        
        bezier.addLine(to: CGPoint(x: W, y: originOffsetY))
        bezier.addLine(to: CGPoint(x: W, y: h))
        bezier.addLine(to: CGPoint(x: 0.0, y: h))
        bezier.addLine(to: CGPoint(x: 0.0, y: startOffsetY))
        bezier.close()
        
        let anim = CABasicAnimation(keyPath: "transform.translation.x")
        anim.duration = 1.0
        anim.fromValue = -w * 0.5
        anim.toValue = -w - w * 0.5
        anim.repeatCount = .infinity
        anim.isRemovedOnCompletion = false
        
        
        firstLayer.fillColor = firstColor.cgColor
        firstLayer.path = bezier.cgPath
        firstLayer.add(anim, forKey: nil)
        
        if !showSingleWave {
            let bezier = UIBezierPath()
            
            let startOffsetY = waveHeight * CGFloat(sinf(Float(offset * twoPie / w)))
            var originOffsetY: CGFloat = 0.0
            
            bezier.move(to: CGPoint(x: 0.0, y: startOffsetY))
            
            for i in stride(from: 0.0, to: W, by: 20.0) {
                originOffsetY = waveHeight * CGFloat(-sinf(Float(twoPie / w * i + offset * twoPie / w)))
                bezier.addLine(to: CGPoint(x: i, y: originOffsetY))
            }
            
            bezier.addLine(to: CGPoint(x: W, y: originOffsetY))
            bezier.addLine(to: CGPoint(x: W, y: h))
            bezier.addLine(to: CGPoint(x: 0.0, y: h))
            bezier.addLine(to: CGPoint(x: 0.0, y: startOffsetY))
            bezier.close()
            
            secondLayer.fillColor = secondColor.cgColor
            secondLayer.path = bezier.cgPath
            secondLayer.add(anim, forKey: nil)
        }
    }
}

使用方式

let wv = WaveView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
wv.showSingleWave = false
view.addSubview(wv)
        
wv.snp.makeConstraints { make in
    make.size.equalTo(wv.frame.size)
    make.center.equalToSuperview()
}
        
wv.setProgress(0.5)

当前效果

当前效果


解决方案

核心问题在于:你直接用三角形路径作为渐变层的遮罩,导致整个三角形被渐变填满;同时波浪路径是独立的矩形,未与三角形结合,也没有被裁剪。以下是修正后的代码:

class WaveView: UIView {
    
    private let firstLayer = CAShapeLayer()
    private let secondLayer = CAShapeLayer()
    private let maskLayer = CAShapeLayer() // 三角形遮罩,限制波浪范围
    
    private var trianglePath: UIBezierPath {
        let w = bounds.width
        let h = bounds.height
        let path = UIBezierPath()
        path.move(to: CGPoint(x: w / 2, y: 0))
        path.addLine(to: CGPoint(x: w, y: h))
        path.addLine(to: CGPoint(x: 0, y: h))
        path.close()
        return path
    }
    
    private var firstColor: UIColor = .cyan
    private var secondColor: UIColor = .cyan.withAlphaComponent(0.4)
    
    private var offset: CGFloat = 0.0
    private let twoPie: CGFloat = 2.0 * .pi
    
    var showSingleWave: Bool = false
    private var start: Bool = false
    
    private(set) var progress: CGFloat = 0.0
    
    private var waveHeight: CGFloat = 20.0
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupViews() {
        // 设置视图遮罩,所有内容仅在三角形内显示
        maskLayer.path = trianglePath.cgPath
        layer.mask = maskLayer
        
        createFirstLayer()
        if !showSingleWave {
            createSecondLayer()
        }
    }
    
    private func createFirstLayer() {
        firstLayer.frame = bounds
        firstLayer.fillColor = firstColor.cgColor
        layer.addSublayer(firstLayer)
    }
    
    private func createSecondLayer() {
        secondLayer.frame = bounds
        secondLayer.fillColor = secondColor.cgColor
        layer.addSublayer(secondLayer)
    }
    
    func setProgress(_ pr: CGFloat) {
        progress = min(max(pr, 0.0), 1.0)
        
        if !start {
            DispatchQueue.main.async {
                self.startAnim()
            }
        }
    }
    
    private func startAnim() {
        start = true
        // 用CADisplayLink实现流畅的逐帧动画
        let displayLink = CADisplayLink(target: self, selector: #selector(updateWave))
        displayLink.add(to: .main, forMode: .common)
    }
    
    @objc private func updateWave() {
        offset += 2.0
        let w = bounds.width
        let h = bounds.height
        // 进度对应的基准Y值:progress=0在底部,progress=1在顶部
        let baseY = h - progress * h
        
        // 生成第一个波浪路径
        let firstBezier = UIBezierPath()
        var startY = baseY + waveHeight * CGFloat(sinf(Float(offset * twoPie / w)))
        firstBezier.move(to: CGPoint(x: 0, y: startY))
        
        for x in stride(from: 0.0, to: w, by: 5.0) {
            let y = baseY + waveHeight * CGFloat(sinf(Float(twoPie / w * x + offset * twoPie / w)))
            firstBezier.addLine(to: CGPoint(x: x, y: y))
        }
        
        // 闭合路径:从波浪右端到视图底部,再回到左端,确保填充范围正确
        firstBezier.addLine(to: CGPoint(x: w, y: h))
        firstBezier.addLine(to: CGPoint(x: 0, y: h))
        firstBezier.close()
        firstLayer.path = firstBezier.cgPath
        
        if !showSingleWave {
            // 生成反向的第二个波浪路径
            let secondBezier = UIBezierPath()
            startY = baseY + waveHeight * CGFloat(-sinf(Float(offset * twoPie / w)))
            secondBezier.move(to: CGPoint(x: 0, y: startY))
            
            for x in stride(from: 0.0, to: w, by: 5.0) {
                let y = baseY + waveHeight * CGFloat(-sinf(Float(twoPie / w * x + offset * twoPie / w)))
                secondBezier.addLine(to: CGPoint(x: x, y: y))
            }
            
            secondBezier.addLine(to: CGPoint(x: w, y: h))
            secondBezier.addLine(to: CGPoint(x: 0, y: h))
            secondBezier.close()
            secondLayer.path = secondBezier.cgPath
        }
    }
}

若需渐变波浪

如果要给波浪添加渐变效果,修改createFirstLayer和createSecondLayer方法:

private func createFirstLayer() {
    let gradientLayer = CAGradientLayer()
    gradientLayer.frame = bounds
    gradientLayer.colors = [UIColor.blue.cgColor, UIColor.cyan.cgColor]
    gradientLayer.startPoint = CGPoint(x: 0.5, y: 1.0)
    gradientLayer.endPoint = CGPoint(x: 0.5, y: 0.0)
    
    firstLayer.frame = bounds
    gradientLayer.mask = firstLayer
    
    layer.addSublayer(gradientLayer)
}

private func createSecondLayer() {
    let gradientLayer = CAGradientLayer()
    gradientLayer.frame = bounds
    gradientLayer.colors = [UIColor.cyan.withAlphaComponent(0.4).cgColor, UIColor.blue.withAlphaComponent(0.4).cgColor]
    gradientLayer.startPoint = CGPoint(x: 0.5, y: 1.0)
    gradientLayer.endPoint = CGPoint(x: 0.5, y: 0.0)
    
    secondLayer.frame = bounds
    gradientLayer.mask = secondLayer
    
    layer.addSublayer(gradientLayer)
}

内容的提问来源于stack exchange,提问作者Asteroid

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 14:54:19