使用UIBezierPath对UIView进行遮罩-仅描边

6

更新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..

这是期望的结果.. 期望的结果 (在此竞赛快照中有更多要点)
我意识到可能有更好的方法,欢迎任何想法或替代方案!

--更新 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


2
你可能需要将描边路径渲染成UIImage,然后使用该UIImage作为蒙版。 - ekscrypto
2
  1. 你说你只想要路径的描边,但你没有设置描边颜色。
  2. 你需要下面这行代码吗?gradient.layer.mask = mask。
- El Tomato
2
不,UIBezierPath并不支持。但是CAShapeLayer支持。 - El Tomato
1
你的问题有进展吗? - ekscrypto
2
我不明白这个问题。使用笔画作为遮罩是什么意思?你能解释一下或者画出你想要产生的效果吗? - matt
显示剩余8条评论
4个回答

9

UIBezierPath具有几个属性,只在描边路径时才起作用,包括lineWidthlineCapStylelineJoinStylemiterLimit

CGPath没有这些属性。

因此,当您设置mask.path = bezierPath.cgPath时,您在bezierPath上设置的任何描边属性都不会传递。您仅从UIBezierPath中提取了CGPath,而没有这些其他属性。

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

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

您还需要设置描边颜色,因为默认的描边颜色是nil,这意味着根本不描边:
mask.strokeColor = UIColor.white.cgColor

同时,由于您不想让形状图层填充路径,因此需要将填充颜色设置为nil:

mask.fillColor = nil

谢谢你的帮助!这解决了问题...我会更新我的问题并附上可行的解决方案。 - August

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中,苹果将遮罩称为“层”!因此,“CAShapeLayer”可以是图像的一层(即图像的一部分),也可以是完全不同用途的遮罩。"最初的回答"
let someMaskIAmAMaking = CAMask() ... no! 

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

请记住,在iOS中,“CAMask”不存在 - 您需要使用“CALayer”来处理“CAMask”和“CALayer”。
为了形成您添加的自定义层(无论是渐变,猫的照片还是其他任何内容),您需要使用掩码。那就是一个掩码..不是路径。
我们将要形成一个层,所以我们将使用一个掩码。(这意味着 - 什么鬼 - 一个CAShapeLayer。)
let ourMask = CAShapeLayer( ...

1. 创建一个仅用于掩码的图层

通常情况下,您只需在此处绘制路径,但我们将在布局中绘制路径,因为我们需要知道尺寸来绘制路径。

因此,这个贝塞尔路径将位于该层上。请记住,(令人困惑的是)具有线条特性的不是路径,而是 >图层<! 贝塞尔路径是100%完美的路径!

请还记得,整体而言,所有这些都只是CAShapeLayer,它将作为掩码使用在我们实际感兴趣的图层(“ourLayer”)上

2. 创建实际的新特殊图层(这里是渐变)

请记住,要使图像层(或渐变)圆形,您需要使用.mask功能(而不是路径)

还请记住,有点奇怪的是,布局上对路径的更改会推到图层(在我们的情况下是我们要用作掩码的maskToUseAsMask图层),确实更改了.mask功能(在我们的情况下是我们的ourLayer上的mask),会向上推到图层(在我们的情况下是我们的ourLayer)。

3. 在布局时实际绘制路径

请如上所述,每次布局时您都必须再次绘制路径。它被“推送”到maskToUseAsMask,最终到我们ourLayer的.mask。

4. 最后在视图上设置普通阴影

这样,

Hence.

Original Answer翻译成:最初的回答。

    layer.shadowOpacity = 0.20 etc ...

请注意,在视图上的阴影是内置于“ .layer”中的阴影,这是视图的“主要和基本层”。您只需将“ .layer”的 .shadowOpacity 设置为一个值即可打开它。
这与在您添加的其他层上添加阴影非常不同。要在您添加的其他层上添加阴影,例如 catLayer,您需要使用 catLayer.path。(该过程完全不同,例如在此处进行了解释:https://dev59.com/IFgR5IYBdhLWcg3wUL4J#57465440

很棒的东西,希望能够尽快使用!谢谢。 - August

1
解决方案是在图像中绘制贝塞尔路径,然后使用生成的图像作为掩码。您代码的问题在于,虽然将贝塞尔路径添加到上下文中,但从未对其进行描边(stroke())。我想向您介绍这个Swift函数,它可以将路径数组绘制到图像中:使用Swift从Uibezierpath数组创建UIImage 您可以通过将其显示到UIImageView中来确认图像是否正确渲染。一旦确认了这一点,应该很容易将其用作掩码:iOS中从UIImage / CGImage创建层蒙版? 干杯!

谢谢你的帮助,伙计。我已经能够在图像中呈现路径,但是对于遮罩无法理解。非常感谢你提供的好主意和链接! - August

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: /* ... */)

非常有趣,谢谢分享!我希望不久的将来能够尝试一下 :-) - August

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接