Инвертная маска iOS в drawRect

С помощью приведенного ниже кода я успешно маскирую часть своего рисунка, но это противоположно тому, что я хочу замаскировать. Это маскирует внутреннюю часть рисунка, где я хотел бы замаскировать внешнюю часть. Есть ли простой способ инвертировать эту маску?

myPath ниже это UIBezierPath,

CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
CGMutablePathRef maskPath = CGPathCreateMutable();
CGPathAddPath(maskPath, nil, myPath.CGPath);
[maskLayer setPath:maskPath];
CGPathRelease(maskPath);
self.layer.mask = maskLayer;

9 ответов

С четной нечетной заливкой на слое формы (maskLayer.fillRule = kCAFillRuleEvenOdd;) вы можете добавить большой прямоугольник, который покрывает весь кадр, а затем добавить форму, которую вы маскируете. Это в действительности инвертирует маску.

CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
CGMutablePathRef maskPath = CGPathCreateMutable();
CGPathAddRect(maskPath, NULL, someBigRectangle); // this line is new
CGPathAddPath(maskPath, nil, myPath.CGPath);
[maskLayer setPath:maskPath];
maskLayer.fillRule = kCAFillRuleEvenOdd;         // this line is new
CGPathRelease(maskPath);
self.layer.mask = maskLayer;

Для Swift 3.0

func mask(viewToMask: UIView, maskRect: CGRect, invert: Bool = false) {
    let maskLayer = CAShapeLayer()
    let path = CGMutablePath()
    if (invert) {
        path.addRect(viewToMask.bounds)
    }
    path.addRect(maskRect)

    maskLayer.path = path
    if (invert) {
        maskLayer.fillRule = kCAFillRuleEvenOdd
    }

    // Set the mask of the view.
    viewToMask.layer.mask = maskLayer;
}

Основываясь на принятом ответе, вот еще один гибрид в Swift. Я сделал это в функцию и сделал invert необязательный

class func mask(viewToMask: UIView, maskRect: CGRect, invert: Bool = false) {
    let maskLayer = CAShapeLayer()
    let path = CGPathCreateMutable()
    if (invert) {
        CGPathAddRect(path, nil, viewToMask.bounds)
    }
    CGPathAddRect(path, nil, maskRect)

    maskLayer.path = path
    if (invert) {
        maskLayer.fillRule = kCAFillRuleEvenOdd
    }

    // Set the mask of the view.
    viewToMask.layer.mask = maskLayer;
}

Swift 5

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let red = UIView(frame: view.bounds)
        view.addSubview(red)
        view.backgroundColor = UIColor.cyan
        red.backgroundColor = UIColor.red
        red.mask(CGRect(x: 50, y: 50, width: 50, height: 50), invert: true)
    }
}

extension UIView{

    func mask(_ rect: CGRect, invert: Bool = false) {
        let maskLayer = CAShapeLayer()
        let path = CGMutablePath()
        if (invert) {
            path.addRect(bounds)
        }
        path.addRect(rect)
        maskLayer.path = path
        if (invert) {
            maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
        }
        // Set the mask of the view.
        layer.mask = maskLayer
    }
}

Спасибо @arvidurs

Вот мое решение Swift 4.2, которое позволяет угловой радиус

extension UIView {

    func mask(withRect maskRect: CGRect, cornerRadius: CGFloat, inverse: Bool = false) {
        let maskLayer = CAShapeLayer()
        let path = CGMutablePath()
        if (inverse) {
            path.addPath(UIBezierPath(roundedRect: self.bounds, cornerRadius: cornerRadius).cgPath)
        }
        path.addPath(UIBezierPath(roundedRect: maskRect, cornerRadius: cornerRadius).cgPath)

        maskLayer.path = path
        if (inverse) {
            maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
        }

        self.layer.mask = maskLayer;
    }

}

Для Swift 4.2

func mask(viewToMask: UIView, maskRect: CGRect, invert: Bool = false) {
    let maskLayer = CAShapeLayer()
    let path = CGMutablePath()
    if (invert) {
        path.addRect(viewToMask.bounds)
    }
    path.addRect(maskRect)

    maskLayer.path = path
    if (invert) {
        maskLayer.fillRule = .evenOdd
    }

    // Set the mask of the view.
    viewToMask.layer.mask = maskLayer;
}

На 2023 год. Все ответы в настоящее время здесь кажутся неверными, поскольку они не настраивают маску при изменении макета (или - только один пример - когда вид анимируется по размеру или форме).

Это очень просто..

1. иметь слой (возможно, просто квадрат цвета, изображение, что угодно)

      lazy var examp: CALayer = {
    let l = CALayer()
    l.background = UIColor.blue.cgColor
    l.mask = shapeAsMask
    layer.addSublayer(l)
    return l
}()

Обратите внимание, чтоexampуже имеет установленную маску.

2. Всякий раз, когда вы хотите замаскировать, вам, конечно, нужен маскирующий слой под рукой.

      lazy var shapeAsMask: CAShapeLayer = {
    let s = CAShapeLayer()
    s.fillRule = .evenOdd
    layer.addSublayer(s)
    layer.mask = s
    return s
}()

Обратите внимание, что fillrule устанавливается по мере необходимости.

3. Теперь создайте фигуру с кривой Безье. Давайте просто сделаем круг:

      override func layoutSubviews() {
    super.layoutSubviews()
    
    examp.frame = bounds

    let i = bounds.width * 0.30
    let thing = UIBezierPath(ovalIn: bounds.insetBy(dx: i, dy: i))
    ...
}

Итак, это маленький кружок в середине изображения.

Если вы хотите «иметь форму», это просто

      override func layoutSubviews() {
    super.layoutSubviews()
    
    fill.frame = bounds

    let i = bounds.width * 0.30
    let thing = UIBezierPath(ovalIn: bounds.insetBy(dx: i, dy: i))
    
    shapeAsMask.path = thing.cgPath
}

Если вы хотите «иметь ОБРАТНУЮ форму», это просто

      override func layoutSubviews() {
    super.layoutSubviews()
    
    fill.frame = bounds

    let i = bounds.width * 0.30
    let thing = UIBezierPath(ovalIn: bounds.insetBy(dx: i, dy: i))

    let p = CGMutablePath()
    p.addRect(bounds)
    p.addPath(thing.cgPath)

    shapeAsMask.path = p
}

Короче "минус" в этом..

      let thing = UIBezierPath( ... some path

просто это:

      let neg = CGMutablePath()
neg.addRect(bounds)
neg.addPath(thing.cgPath)

Итак, это все.

Не забывайте, что ...

Каждый раз, когда у вас есть маска (или слой!), вы должны установить ее вlayoutSubviews. (Вот почему layoutSubviews называется layoutSubviews!)

Есть много отличных ответов, все они используют правило чет-нечет для разрешения внутренних и внешних поверхностей. Вот подход Swift 5 с ненулевым правилом:

      extension UIView {
    func mask(_ rect: CGRect, cornerRadius: CGFloat, invert: Bool = false) {
        let maskLayer = CAShapeLayer()
        let path = CGMutablePath()
        
        if (invert) {
            path.move(to: .zero)
            path.addLine(to: CGPoint(x: 0, y: bounds.height))
            path.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
            path.addLine(to: CGPoint(x: bounds.width, y: 0))
            path.addLine(to: .zero)
        }
        
        path.addPath(UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath)
        maskLayer.path = path
        layer.mask = maskLayer
    }
}

Для операции маскирования это довольно прямолинейно. Мы рисуем прямоугольник и используем его как маскирующий путь.

Для инвертированной маскировки мы используем границы представления, которое будет маскироваться. Затем рисуем его против часовой стрелки. После этого добавляемUIBezierPathrect, который по умолчанию рисуется по часовой стрелке. Таким образом, любая точка p внутри прямоугольника будет иметь одно пересечение по часовой стрелке и одно пересечение против часовой стрелки, что приведет к нулевому общему числу витков, что сделает эту точку внешней точкой.

Чтобы инвертировать маску, вы можете примерно так

  1. Вот маска из скрещенных прямоугольников вроде

     let crossPath =  UIBezierPath(rect: cutout.insetBy(dx: 30, dy: -5))
              crossPath.append(UIBezierPath(rect: cutout.insetBy(dx: -5, dy: 30)))
                let crossMask = CAShapeLayer()
                crossMask.path = crossPath.cgPath
                crossMask.backgroundColor = UIColor.clear.cgColor
                crossMask.fillRule = .evenOdd
    
  2. И здесь я добавляю 3-й прямоугольник вокруг моих скрещенных прямоугольников, поэтому, используя.evenOdd, он занимает площадь, равную (New Rectancle - Old Crossed Rectangle), другими словами, за пределами области скрещенных прямоугольников

      let crossPath = UIBezierPath(rect: cutout.insetBy(dx: -5, dy: -5) )
        crossPath.append(UIBezierPath(rect: cutout.insetBy(dx: 30, dy: -5)))
        crossPath.append(UIBezierPath(rect: cutout.insetBy(dx: -5, dy: 30)))
        let crossMask = CAShapeLayer()
        crossMask.path = crossPath.cgPath
        crossMask.backgroundColor = UIColor.clear.cgColor
        crossMask.fillRule = .evenOdd
    

Другие вопросы по тегам