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
相关产品推荐
相关产品推荐

