52

通过使用[UIBezierPath bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:],我可以创建一个圆形视图,例如:

圆形视图

我怎么能从这条路径(或其他方式)中减去另一条路径,以创建这样的路径:

减法视图

有什么办法可以做这样的事情吗?伪代码:

UIBezierPath *bigMaskPath = [UIBezierPath bezierPathWithRoundedRect:bigView.bounds 
                                 byRoundingCorners:(UIRectCornerTopLeft|UIRectCornerTopRight)
                                       cornerRadii:CGSizeMake(18, 18)];
UIBezierPath *smallMaskPath = [UIBezierPath bezierPathWithRoundedRect:smalLView.bounds 
                                     byRoundingCorners:(UIRectCornerTopLeft|UIRectCornerTopRight)
                                           cornerRadii:CGSizeMake(18, 18)];

UIBezierPath *finalPath = [UIBezierPath pathBySubtractingPath:smallMaskPath fromPath:bigMaskPath];
4

9 回答 9

87

实际上,对于大多数情况,有一种更简单的方法,例如 Swift:

path.append(cutout.reversing())

这是因为默认的填充规则是非零缠绕规则

于 2015-08-25T09:21:22.953 回答
63

如果你想描边减去的路径,你就靠自己了。Apple 没有提供返回(或只是描边)一条路径与另一条路径相减的 API。

如果您只想填充减去的路径(如示例图像中所示),您可以使用剪切路径来完成。不过,你必须使用一个技巧。当您向剪切路径添加路径时,新剪切路径是旧剪切路径和添加路径的交点。因此,如果您只是添加smallMaskPath到剪切路径,您最终将只填充内部区域smallMaskPath,这与您想要的相反。

您需要做的是将现有的剪切路径smallMaskPath. 幸运的是,您可以使用奇偶缠绕规则很容易地做到这一点。您可以在Quartz 2D Programming Guide中阅读奇偶规则。

基本思想是我们创建一个包含两个子路径的复合路径:你的smallMaskPath和一个巨大的矩形,它完全包围你smallMaskPath和你可能想要填充的所有其他像素。由于奇偶规则,内部的每个像素都smallMaskPath将被视为复合路径的外部,外部的每个像素都smallMaskPath将被视为复合路径的内部。

所以让我们创建这个复合路径。我们将从巨大的矩形开始。没有比无限长方形更大的长方形了:

UIBezierPath *clipPath = [UIBezierPath bezierPathWithRect:CGRectInfinite];

现在我们通过添加它使其成为复合路径smallMaskPath

[clipPath appendPath:smallMaskPath];

接下来我们设置使用奇偶规则的路径:

clipPath.usesEvenOddFillRule = YES;

在我们剪辑到这个路径之前,我们应该保存图形状态,以便我们可以在完成后撤消对剪辑路径的更改:

CGContextSaveGState(UIGraphicsGetCurrentContext()); {

现在我们可以修改剪切路径:

    [clipPath addClip];

我们可以填写bigMaskPath

    [[UIColor orangeColor] setFill];
    [bigMaskPath fill];

最后我们恢复图形状态,撤消对剪切路径的更改:

} CGContextRestoreGState(UIGraphicsGetCurrentContext());

以下是所有代码,以防您要复制/粘贴它:

UIBezierPath *clipPath = [UIBezierPath bezierPathWithRect:CGRectInfinite];
[clipPath appendPath:smallMaskPath];
clipPath.usesEvenOddFillRule = YES;

CGContextSaveGState(UIGraphicsGetCurrentContext()); {
    [clipPath addClip];
    [[UIColor orangeColor] setFill];
    [bigMaskPath fill];
} CGContextRestoreGState(UIGraphicsGetCurrentContext());
于 2012-01-14T05:22:50.667 回答
34

应该这样做,根据需要调整大小:

CGRect outerRect = {0, 0, 200, 200};
CGRect innerRect  = CGRectInset(outerRect,  30, 30);

UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:outerRect cornerRadius:10];

[path appendPath:[UIBezierPath bezierPathWithRoundedRect:innerRect cornerRadius:5]];
path.usesEvenOddFillRule = YES;

[[UIColor orangeColor] set];
[path fill];

获得所需效果的另一种非常简单的方法是仅绘制外部圆形矩形,更改颜色,然后在其上绘制内部圆形。

于 2012-01-14T07:48:39.867 回答
13

使用@Patrick Pijnappel 答案,您可以准备测试游乐场以进行快速测试

import UIKit
import PlaygroundSupport

let view = UIView(frame: CGRect(x: 0, y: 0, width: 375, height: 647))
view.backgroundColor = UIColor.green

let someView = UIView(frame: CGRect(x:50, y: 50, width:250, height:250))
someView.backgroundColor = UIColor.red
view.addSubview(someView)

let shapeLayer = CAShapeLayer()
shapeLayer.frame = someView.bounds
shapeLayer.path = UIBezierPath(roundedRect: someView.bounds, 
                               byRoundingCorners: [UIRectCorner.bottomLeft,UIRectCorner.bottomRight] ,
                                cornerRadii: CGSize(width: 5.0, height: 5.0)).cgPath

someView.layer.mask = shapeLayer
someView.layer.masksToBounds = true

let rect = CGRect(x:0, y:0, width:200, height:100)
let cornerRadius:CGFloat = 5
let subPathSideSize:CGFloat = 25

let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
let leftSubPath = UIBezierPath(arcCenter: CGPoint(x:0, y:rect.height / 2), 
                                radius: subPathSideSize / 2, startAngle:  CGFloat(Double.pi / 2), endAngle: CGFloat(Double.pi + Double.pi / 2), clockwise: false)
leftSubPath.close()

let rightSubPath = UIBezierPath(arcCenter: CGPoint(x:rect.width, y:rect.height / 2), 
                               radius: subPathSideSize / 2, startAngle:  CGFloat(Double.pi / 2), endAngle: CGFloat(Double.pi + Double.pi / 2), clockwise: true)
rightSubPath.close()

path.append(leftSubPath)
path.append(rightSubPath.reversing())
path.append(path)

let mask = CAShapeLayer()
mask.frame = shapeLayer.bounds
mask.path = path.cgPath

someView.layer.mask = mask 

view
PlaygroundPage.current.liveView = view

在此处输入图像描述

在此处输入图像描述

于 2017-03-09T15:28:51.617 回答
9

2019 - 太容易了

我对其他答案感到惊讶,因为这非常容易做到。可能有一些我不明白的要求。但:

p = UIBezierPath(rect: .. )
let hole = UIBezierPath(ovalIn: ... )
p.append(hole.reversing())
p.usesEvenOddFillRule = false

无论您是否进行切割,这都可以完美地工作。

没有问题,即使它们确实重叠

在此处输入图像描述

在此处输入图像描述

(这是制作“缺口”或“压痕”的好方法。)

该技术是

  1. 外部路径顺时针,内部路径逆时针
  2. 使用计数规则,即非零规则

(在示例中,我实际上将它用作 UIView 上的掩码......)

layerToUseAsUIViewMask.path = p
layer.mask = layerToUseAsUIViewMask

为了节省任何输入孔代码的人...

let hole = UIBezierPath(ovalIn: CGRect(
    origin: CGPoint(x: 70, y: 10), .. use -10 for the second demo above.
    size: CGSize(width: 50, height: 50))
)

注意如果您不确定这两条路径的运行方式,当然只需尝试每一条

p.append(hole.reversing()) // that's acw currently on iOS

p.append(hole) // that's cw currently on iOS

直到它起作用。


类似有用的提示:

制作发光盒:

https://stackoverflow.com/a/59092828/294884

于 2019-08-15T18:16:29.750 回答
5

I've been beating my head against the wall trying to figure out how to do this with multiple overlapping CGPaths. If you overlap more than once, it refills with the solutions provided above. It turns out the way to truly attain a 'subtract' effect with multiple overlapping paths is to just set the context's blend mode to clear.

CGContextSetBlendMode(ctx, kCGBlendModeClear);

于 2014-04-16T21:51:04.220 回答
2

Patrick 通过使用非零缠绕规则为用户 NSResponder 的回答提供了改进/替代方案。这是 Swift 中的完整实现,适用于任何正在寻找扩展答案的人。

UIGraphicsBeginImageContextWithOptions(CGSize(width: 200, height: 200), false, 0.0)
let context = UIGraphicsGetCurrentContext()

let rectPath = UIBezierPath(roundedRect: CGRectMake(0, 0, 200, 200), cornerRadius: 10)
var cutoutPath = UIBezierPath(roundedRect: CGRectMake(30, 30, 140, 140), cornerRadius: 10)

rectPath.appendPath(cutoutPath.bezierPathByReversingPath())

UIColor.orangeColor().set()
outerForegroundPath.fill()

let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

这是一个要点。

您可以将其放入 Storyboard 中以查看和使用输出。

结果

于 2016-04-23T06:01:23.777 回答
1

这是视图上的两个透明孔:

在此处输入图像描述

@IBDesignable
class HoleView: UIView {
    
    @IBInspectable
    var radius1: CGFloat = 10 { didSet { updateMask() } }
    
    @IBInspectable
    var XPositionPercent1: CGFloat = 90 { didSet { updateMask() } }
    
    @IBInspectable
    var YPositionPercent1: CGFloat = 50 { didSet { updateMask() } }
    
    
    @IBInspectable
    var radius2: CGFloat = 10 { didSet { updateMask() } }
    
    @IBInspectable
    var XPositionPercent2: CGFloat = 10 { didSet { updateMask() } }
    
    @IBInspectable
    var YPositionPercent2: CGFloat = 50 { didSet { updateMask() } }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        updateMask()
    }
    
    private func updateMask() {
        
        let path = UIBezierPath(rect: bounds)
        path.usesEvenOddFillRule = false
        
        
        // MARK: Hole 1
        let XPos1 = (bounds.width * XPositionPercent1) / 100
        let YPos1 = (bounds.height * YPositionPercent1) / 100
        let center1 = CGPoint(x: XPos1, y: YPos1)
        
        let hole1 = UIBezierPath(arcCenter: center1,
                                 radius: radius1,
                                 startAngle: 0,
                                 endAngle: 2 * .pi,
                                 clockwise: true)
        path.append(hole1)
        
        // MARK: Hole 2
        let XPos2 = (bounds.width * XPositionPercent2) / 100
        let YPos2 = (bounds.height * YPositionPercent2) / 100
        let center2 = CGPoint(x: XPos2, y: YPos2)
        let hole2 = UIBezierPath(arcCenter: center2,
                                 radius: radius2,
                                 startAngle: 0,
                                 endAngle: 2 * .pi,
                                 clockwise: true)
        path.append(hole2)
        
        
        
        let mask1 = CAShapeLayer()
        mask1.fillRule = .evenOdd
        mask1.path = path.cgPath
        
        layer.mask = mask1
    }
}
于 2021-10-22T07:38:37.320 回答
-1

CGPath我可以通过创建一个using CGPathAddLineToPointand来解决,而不是减去CGPathAddArcToPoint。这也可以UIBiezerPath使用类似的代码来完成。

于 2012-01-14T23:47:38.257 回答