使用UIBezierPath屏蔽UIView - 仅限笔画

时间:2017-10-13 01:58:01

标签: ios swift

Update 02有一个可行的解决方案......

我正在尝试使用UIBezierPath的笔划作为另一个UIView的掩码。有很多例子,但它们都使用填充来剪辑视图。

我正在尝试仅使用路径的笔划,但它无法正确显示。

以下是我目前在Swift 3.x中的内容:

let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 0, y: 0))
bezierPath.addLine(to: CGPoint(x: 100, y: 100))
bezierPath.addLine(to: CGPoint(x: 200, y: 50))
bezierPath.addLine(to: CGPoint(x: 300, y: 100))
bezierPath.lineWidth = 5.0
bezierPath.stroke()

let gradient = Gradient(frame: self.bounds)
let mask = CAShapeLayer()

mask.path = bezierPath.cgPath
gradient.layer.mask = mask

self.addSubview(gradient)

但是,上面的代码就是这样做的。我正在寻找只使用笔画作为面具......这就是代码目前正在做的事情

Currently doing..

这是理想的结果.. Desired outcome

(此comp快照中有更多分数)

我意识到可能有更好的方式,对任何想法或替代方案持开放态度!

- 更新01 -

我最新的,但掩盖了一切:

let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 0, y: 0))
bezierPath.addLine(to: CGPoint(x: 100, y: 100))
bezierPath.addLine(to: CGPoint(x: 200, y: 50))
bezierPath.addLine(to: CGPoint(x: 300, y: 100))
bezierPath.lineWidth = 5.0
bezierPath.stroke()

let gradient = Gradient(frame: self.bounds)

UIGraphicsBeginImageContext(CGSize(width: gradient.bounds.width, height: gradient.bounds.height))
let context : CGContext = UIGraphicsGetCurrentContext()!
context.addPath(bezierPath.cgPath)

let image = UIGraphicsGetImageFromCurrentImageContext()
gradient.layer.mask?.contents = image?.cgImage

并且在尝试使用CAShapeLayer解决之后无处可去:

let mask = CAShapeLayer()
mask.fillColor = UIColor.black.cgColor
mask.strokeColor = UIColor.black.cgColor
mask.fillRule = kCAFillModeBoth
mask.path = bezierPath.cgPath
gradient.layer.mask = mask
self.addSubview(gradient)

- 更新02 - 工作解决方案 -

感谢大家的帮助!

let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 0, y: 0))
bezierPath.addLine(to: CGPoint(x: 100, y: 300))
bezierPath.addLine(to: CGPoint(x: 200, y: 50))
bezierPath.addLine(to: CGPoint(x: 300, y: 200))
bezierPath.addLine(to: CGPoint(x: 400, y: 100))
bezierPath.addLine(to: CGPoint(x: 500, y: 200))

let gradient = Gradient(frame: self.bounds) // subclass of UIView

let mask = CAShapeLayer()
mask.fillColor = nil
mask.strokeColor = UIColor.black.cgColor
mask.path = bezierPath.cgPath
mask.lineWidth = 5.0
gradient.layer.mask = mask

self.addSubview(gradient)

而且,结果是我想要的:

Expected Result

4 个答案:

答案 0 :(得分:5)

UIBezierPath有多个属性,只有在抚摸路径时才有用,包括lineWidthlineCapStylelineJoinStylemiterLimit

CGPath没有这些属性。

因此,当您设置mask.path = bezierPath.cgPath时,您在bezierPath上设置的所有笔触属性都不会延续。您只从CGPath中提取了UIBezierPath,而没有提取其他任何属性。

您需要在CAShapeLayer而不是任何路径对象上设置描边属性。因此:

mask.path = bezierPath.cgPath
mask.lineWidth = 5

您还需要设置笔触颜色,因为默认笔触颜色为nil,这意味着根本不要描边:

mask.strokeColor = UIColor.white.cgColor

并且,因为希望形状图层填充路径,您需要将填充颜色设置为nil:

mask.fillColor = nil

答案 1 :(得分:2)

确切的操作方法

进行了详细的讨论和解释:

override func layoutSubviews() {
    setup()
    super.layoutSubviews()
}

private lazy var layerToUseAsAMask: CAShapeLayer = {
    let l = CAShapeLayer()

    // Recall that often you'd just draw the path here, but, somewhat confusingly
    // we will draw the path in layout, since, we do need to know the sizes to
    // draw the path

    l.fillColor = UIColor.clear.cgColor
    l.strokeColor = UIColor.white.cgColor
    l.lineWidth = thick
    l.lineCap = .round

    // So, the bezier path is going to be on this layer. recall that (confusingly)
    // it's the >layer<, not the path, which has the line qualities. The bezier
    // path is a 100% platonic path!

    // Recall too that overall, >all< of this is merely the CAShapeLayer, which
    // will be >used as a mask< on our actual layer of interest ("ourLayer")

    return l
}()

private lazy var ourLayer: CAGradientLayer = {
    let l = CAGradientLayer()
    layer.addSublayer(l)
    l.frame = self.bounds
    l.startPoint = CGPoint(x: 0.5, y: 0)
    l.endPoint = CGPoint(x: 0.5, y: 1)
    l.colors = [topColor.cgColor, bottomColor.cgColor]

    // Recall that to round an image layer (or gradient later) you use the .mask
    // facility (>not< a path)

    l.mask = layerToUseAsAMask

    // Recall too that, somewhat bizarrely really, changes at layout to a path
    // push up to a layer (in our case our maskToUseAsMask); and indeed changes
    // to a .mask facility (in our case, the one on our main layer) push up
    // to a layer (ie, in our case to our main layer).

    return l
}()

override func layoutSubviews() {
    common()
    super.layoutSubviews()
}

func common() {

    ourLayer.frame = bounds

    layerToUseAsAMask.frame = bounds

    // Recall as explained above, every single time we layout, you have to
    // actually draw the path again.  It is "pushed up" to maskToUseAsMask
    // and ultimately to the .mask on our ourLayer.

    let pBar = UIBezierPath()
    pBar.move(to: ...)
    pBar.addLine(to: ...)
    maskToUseAsMask.path = pBar.cgPath

    layer.shadowOffset = CGSize(width: 5, height: 5)
    layer.shadowColor = UIColor.green.cgColor
    layer.shadowOpacity = 0.20
    layer.shadowRadius = 2
}

在iOS中,Apple已将面具命名为..一层!因此,“ CAShapeLayer”可以是一层(图像的一层),或者在完全不同的用法中可以是...蒙版。

let someMaskIAmAMaking = CAMask() ... no! 

let someMaskIAmAMaking = CAShapeLayer() ... strange but true. Bad Apple.

请始终记住,在iOS中,“ CAMask”不存在-您对“ CAMask”和“ CALayer”都使用“ CALayer”。

要成形已添加的自定义图层(渐变色,猫的照片或其他任何形状),您需要使用蒙版。那是面具。. 不是路径

我们将要成形一个图层,因此我们将使用蒙版。 (这意味着-WTF-CAShapeLayer。)

let ourMask = CAShapeLayer( ...

因此。

1。制作一层,其唯一目的是用作蒙版

回想一下,通常您只是在这里画出了一条路,但是有些混乱 我们将在布局中绘制路径,因为我们确实需要知道      绘制路径。

因此,贝塞尔曲线路径将在此层上。回想一下(令人困惑) 是> layer <,而不是具有线条质量的路径。贝塞尔曲线 路径是100%柏拉图式的路径!

也请回想一下,总体而言,所有这一切仅仅是CAShapeLayer, 将>用作我们的实际关注图层(“ ourLayer”)上的遮罩

2。制作实际的新特殊层(此处为渐变)

回想一下,要使图像层(或以后的渐变)变圆,请使用.mask 设施(不是路径)

也请回想一下,实际上有点奇怪,它在布局上更改为路径      向上推一层(在我们的例子中是maskToUseAsMask);确实发生了变化     到一个.mask设施(在我们的示例中,是我们ourLayer上的一个)上推  一层(例如,在我们的OurLayer中)。

3。在布局时,实际绘制路径

回想一下,如上所述,我们每次布局时,都必须 真正地画出了道路。它被“推”到maskToUseAsMask 并最终到达ourLayer上的.mask。

4。最后在“视图”上设置一个普通阴影

所以

    layer.shadowOpacity = 0.20 etc ...

回想一下,“视图上”的阴影是视图“主层和基本层”“ .layer”的内置阴影。您只需将.layer的.shadowOpacity设置为一个值即可将其打开。

这与在添加的其他图层上添加阴影 完全不同。要在添加的其他某些层上添加阴影 ,例如catLayer,请使用catLayer.path。 (该过程完全不同,在这里举例说明:https://stackoverflow.com/a/57465440/294884

答案 2 :(得分:1)

解决方案是将bezierPath绘制到图像中,然后使用生成的图像作为蒙版。您的代码的问题是,当您将bezier路径添加到上下文时,您永远不会描述()它。我想向您推荐这个快速函数,它可以将一系列路径绘制到图像中:Create an UIImage from Uibezierpath Array in Swift

您可以通过将图像显示到UIImageView来正确确认图像。确认后,应该很容易使用掩码:iOS layer mask from UIImage/CGImage?

干杯!

答案 3 :(得分:1)

您可以直接在Core Graphics中执行此操作,而无需使用CALayer类,这在绘制图像时非常有用。

您需要先将贝塞尔曲线路径转换为本身的“描边”版本,然后使用渐变剪切并填充生成的路径。

我使用以下包装函数:

extension UIBezierPath {
    
    // Return a path that represents the outline of the receiver when stroked
    public func strokedPath() -> UIBezierPath?
    {
        guard let ctx = UIGraphicsGetCurrentContext() else { return nil }
        
        ctx.addPath(self.cgPath)
        ctx.setLineWidth(self.lineWidth)
        ctx.replacePathWithStrokedPath()
        
        guard let path = ctx.path  else { return nil }
        return UIBezierPath(cgPath: path)
    }
    
    /// Fills the receiving bezier path with a gradient
    public func fillWithLinearGradient(start:CGPoint, end:CGPoint, colors:[UIColor], fractions:[CGFloat]? = nil)
    {
        guard let ctx = UIGraphicsGetCurrentContext() else { return }
        
        ctx.saveGState()
        defer { ctx.restoreGState() } // clean up graphics state changes when the method returns
        
        self.addClip() // use the path as the clipping region
        
        let cgColors = colors.map({ $0.cgColor })
        let locations = (fractions?.count == colors.count) ? fractions : nil
        guard let gradient = CGGradient(colorsSpace: nil, colors: cgColors as CFArray, locations: locations) else { return }
        
        ctx.drawLinearGradient(gradient, start: start, end: end, options: [])
    }

}

用法:

myBezierPath.strokedPath()?.fillWithLinearGradient(start: /* ... */)