向 UIBezierPath 行添加渐变

Add gradient to UIBezierPath line

我正在使用 UIBezierPathCAShapeLayer 画一条线。我需要为线条添加渐变,如顶部颜色为白色,底部颜色为红色。我遵循了 中的一些解决方案,但运气不佳。下面是我的代码。

private func drawLine(start: CGPoint, toPoint end: CGPoint) {

    //design the path
    let path = UIBezierPath()
    path.move(to: start)
    path.addLine(to: end)

    //design path in layer
    let shapeLayer = CAShapeLayer()
    shapeLayer.path = path.cgPath
    shapeLayer.strokeColor = UIColor.red.cgColor
    shapeLayer.lineWidth = 2.0
    self.layer.addSublayer(shapeLayer)
    
    let gradient = CAGradientLayer()
    gradient.frame = path.bounds
    gradient.colors = [UIColor.white.cgColor, UIColor.red.cgColor]

    let shapeMask = CAShapeLayer()
    shapeMask.path = path.cgPath
    gradient.mask = shapeMask
    
    self.layer.addSublayer(gradient)
    
}

另一个参考 Whosebug 的尝试是

private func drawLine(start: CGPoint, toPoint end: CGPoint) {

    //design the path
    let path = UIBezierPath()
    path.move(to: start)
    path.addLine(to: end)

    //design path in layer
    let shapeLayer = CAShapeLayer()
    shapeLayer.path = path.cgPath
    shapeLayer.strokeColor = UIColor.red.cgColor
    shapeLayer.lineWidth = 2.0
    self.layer.addSublayer(shapeLayer)
    
    addGradientLayer(to: shapeLayer, path: path)
}

private func addGradientLayer(to layer: CALayer, path: UIBezierPath) {

    let gradientMask = CAShapeLayer()
    gradientMask.contentsScale = UIScreen.main.scale
    gradientMask.strokeColor = UIColor.white.cgColor
    gradientMask.path = path.cgPath

    let gradientLayer = CAGradientLayer()
    gradientLayer.mask = gradientMask
    gradientLayer.frame = layer.frame
    gradientLayer.contentsScale = UIScreen.main.scale
    gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]

    layer.addSublayer(gradientLayer)
}

两者都只是画红线(描边)。

您的问题的简短答案很可能是您需要调用 cgPath.copy(strokingWithWidth: 3.0, lineCap: .butt, lineJoin: .bevel, miterLimit: 1.0) 将蒙版上的描边路径转换为填充路径。

但要逐步分解...

首先,您正在绘制一个渐变,它被一条线遮住了。不绘制已应用渐变的线。所以只需要添加一层,而且需要是渐变层。所以第一步应该画一个渐变的矩形:

func drawLine(start: CGPoint, toPoint end: CGPoint) {
    let path = UIBezierPath()
    path.move(to: start)
    path.addLine(to: end)
    
    let gradientLayer = CAGradientLayer()
    gradientLayer.frame = path.bounds
    gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
    
    self.layer.addSublayer(gradientLayer)
}

现在可以看到渐变了,让我们给它应用一些遮罩。一个圈子应该是一件容易的事:

func drawLine(start: CGPoint, toPoint end: CGPoint) {
    let path = UIBezierPath()
    path.move(to: start)
    path.addLine(to: end)
    
    let gradientLayer = CAGradientLayer()
    gradientLayer.frame = path.bounds
    gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
    gradientLayer.mask = {
        let layer = CAShapeLayer()
        layer.path = UIBezierPath(ovalIn: path.bounds).cgPath
        return layer
    }()
    
    self.layer.addSublayer(gradientLayer)
}

如果你这样做并且你使用了路径的一些非零起始坐标那么你会看到圆被切断了。被屏蔽的路径必须在渐变层坐标中,因此 path.bounds 需要更正为

    gradientLayer.mask = {
        let layer = CAShapeLayer()
        layer.path = UIBezierPath(ovalIn: CGRect(x: 0.0, y: 0.0, width: path.bounds.width, height: path.bounds.height)).cgPath
        return layer
    }()

现在坐标已经确定了,让我们把它转换成一条线。它变得有点复杂,因为我们需要减去原始路径的原点:

func drawLine(start: CGPoint, toPoint end: CGPoint) {
    let path = UIBezierPath()
    path.move(to: start)
    path.addLine(to: end)
    
    let gradientLayer = CAGradientLayer()
    gradientLayer.frame = path.bounds
    gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
    gradientLayer.mask = {
        let layer = CAShapeLayer()
        let lineStart = CGPoint(x: start.x - path.bounds.minX,
                               y: start.y - path.bounds.minY)
        let lineEnd = CGPoint(x: end.x - path.bounds.minX,
                               y: end.y - path.bounds.minY)
        let path = UIBezierPath()
        path.move(to: lineStart)
        path.addLine(to: lineEnd)
        layer.path = path.cgPath
        return layer
    }()
    
    self.layer.addSublayer(gradientLayer)
}

还有其他方法可以移动路径。也可以使用转换。所以如果这个方法对你来说不够好,请选择你想要的。

然而,这现在什么也没有画。原因是当使用 mask 时总是使用 fill 方法。填充一条线没有任何作用,因为它没有面积。例如,您可以使用多条线来绘制自定义形状。在您的情况下,您可以使用 4 条线代表一个矩形,也就是您希望绘制的线。

然而,画线是一件痛苦的事,所以幸好有一个设计不是很直观的 API 可以将描边路径转换为填充路径。因此,在您的情况下,它将一条线转换为具有已定义宽度的矩形(以及仅在更复杂的笔画中才需要的其他属性)。您可以使用

public func copy(strokingWithWidth lineWidth: CGFloat, lineCap: CGLineCap, lineJoin: CGLineJoin, miterLimit: CGFloat, transform: CGAffineTransform = .identity) -> CGPath

该代码未记录在案,但在 online documentation 您可能会找到

Discussion The new path is created so that filling the new path draws the same pixels as stroking the original path with the specified line style.

现在最终结果应该是这样的:

func drawLine(start: CGPoint, toPoint end: CGPoint) {
    let path = UIBezierPath()
    path.move(to: start)
    path.addLine(to: end)
    
    let gradientLayer = CAGradientLayer()
    gradientLayer.frame = path.bounds
    gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
    gradientLayer.mask = {
        let layer = CAShapeLayer()
        let lineStart = CGPoint(x: start.x - path.bounds.minX,
                               y: start.y - path.bounds.minY)
        let lineEnd = CGPoint(x: end.x - path.bounds.minX,
                               y: end.y - path.bounds.minY)
        let path = UIBezierPath()
        path.move(to: lineStart)
        path.addLine(to: lineEnd)
        layer.path = path.cgPath.copy(strokingWithWidth: 3.0, lineCap: .butt, lineJoin: .bevel, miterLimit: 1.0)
        return layer
    }()
    
    self.layer.addSublayer(gradientLayer)
}

我将线宽硬编码为 3.0 但您最好将其添加为方法参数或视图 属性.

要尝试不同的设置并快速查看此答案的结果,您可以创建一个新项目并将视图控制器代码替换为以下代码:

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
        view.addSubview({
            let label = UILabel(frame: CGRect(x: 20.0, y: 70.0, width: 300.0, height: 20.0))
            label.text = "Tap to toggle through drawing types"
            return label
        }())
    }
    
    private var instance: Int = 0
    private var currentView: UIView?
    
    @objc private func onTap() {
        currentView?.removeFromSuperview()
        let view = MyView(frame: CGRect(x: 30.0, y: 100.0, width: 100.0, height: 200.0))
        currentView = view
        view.backgroundColor = .lightGray
        self.view.addSubview(view)
        view.drawLine(type: instance, start: CGPoint(x: 90.0, y: 10.0), toPoint: .init(x: 10.0, y: 150.0))
        instance += 1
    }
    
}

private extension ViewController {

    class MyView: UIView {
        
        func drawLine(type: Int, start: CGPoint, toPoint end: CGPoint) {
            let methods = [drawLine1, drawLine2, drawLine3, drawLine4, drawLine5]
            methods[type%methods.count](start, end)
        }
        
        private func drawLine1(start: CGPoint, toPoint end: CGPoint) {
            let path = UIBezierPath()
            path.move(to: start)
            path.addLine(to: end)
            
            let gradientLayer = CAGradientLayer()
            gradientLayer.frame = path.bounds
            gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
            
            self.layer.addSublayer(gradientLayer)
        }
        
        private func drawLine2(start: CGPoint, toPoint end: CGPoint) {
            let path = UIBezierPath()
            path.move(to: start)
            path.addLine(to: end)
            
            let gradientLayer = CAGradientLayer()
            gradientLayer.frame = path.bounds
            gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
            gradientLayer.mask = {
                let layer = CAShapeLayer()
                layer.path = UIBezierPath(ovalIn: path.bounds).cgPath
                return layer
            }()
            
            self.layer.addSublayer(gradientLayer)
        }
        
        private func drawLine3(start: CGPoint, toPoint end: CGPoint) {
            let path = UIBezierPath()
            path.move(to: start)
            path.addLine(to: end)
            
            let gradientLayer = CAGradientLayer()
            gradientLayer.frame = path.bounds
            gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
            gradientLayer.mask = {
                let layer = CAShapeLayer()
                layer.path = UIBezierPath(ovalIn: CGRect(x: 0.0, y: 0.0, width: path.bounds.width, height: path.bounds.height)).cgPath
                return layer
            }()
            
            self.layer.addSublayer(gradientLayer)
        }
        
        private func drawLine4(start: CGPoint, toPoint end: CGPoint) {
            let path = UIBezierPath()
            path.move(to: start)
            path.addLine(to: end)
            
            let gradientLayer = CAGradientLayer()
            gradientLayer.frame = path.bounds
            gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
            gradientLayer.mask = {
                let layer = CAShapeLayer()
                let lineStart = CGPoint(x: start.x - path.bounds.minX,
                                        y: start.y - path.bounds.minY)
                let lineEnd = CGPoint(x: end.x - path.bounds.minX,
                                      y: end.y - path.bounds.minY)
                let path = UIBezierPath()
                path.move(to: lineStart)
                path.addLine(to: lineEnd)
                layer.path = path.cgPath
                return layer
            }()
            
            self.layer.addSublayer(gradientLayer)
        }
        
        private func drawLine5(start: CGPoint, toPoint end: CGPoint) {
            let path = UIBezierPath()
            path.move(to: start)
            path.addLine(to: end)
            
            let gradientLayer = CAGradientLayer()
            gradientLayer.frame = path.bounds
            gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
            gradientLayer.mask = {
                let layer = CAShapeLayer()
                let lineStart = CGPoint(x: start.x - path.bounds.minX,
                                        y: start.y - path.bounds.minY)
                let lineEnd = CGPoint(x: end.x - path.bounds.minX,
                                      y: end.y - path.bounds.minY)
                let path = UIBezierPath()
                path.move(to: lineStart)
                path.addLine(to: lineEnd)
                layer.path = path.cgPath.copy(strokingWithWidth: 3.0, lineCap: .butt, lineJoin: .bevel, miterLimit: 1.0)
                return layer
            }()
            
            self.layer.addSublayer(gradientLayer)
        }
        
    }
    
}