Подтвердить что ты не робот

Путь вычитания UIBezierPath

Используя [UIBezierPath bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:], я могу создать округленное представление, например:

rounded view

Как я мог вычесть другой путь из этого (или каким-либо другим способом), чтобы создать такой путь:

subtracted view

Есть ли способ сделать что-то подобное? Псевдокод:

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];
4b9b3361

Ответ 1

Если вы хотите погладить вычитаемый путь, вы сами по себе. Apple не предоставляет API, который возвращает (или просто штрихи) вычитание одного пути из другого.

Если вы просто хотите заполнить вычитаемый путь (как в вашем примере изображения), вы можете сделать это, используя обтравочный контур. Однако вы должны использовать трюк. Когда вы добавляете путь к обтравочному контуру, новый обтравочный контур является пересечением старого обтравочного контура и добавленного пути. Поэтому, если вы просто добавите smallMaskPath к обтравочному контуру, вы получите заполнение только области внутри smallMaskPath, которая противоположна тому, что вы хотите.

Что вам нужно сделать, это пересечь существующий обтравочный путь с инверсией smallMaskPath. К счастью, вы можете сделать это довольно легко, используя правило четной обмотки. Вы можете прочитать о четном правиле в Кварцевом 2D-руководстве по программированию.

Основная идея состоит в том, что мы создаем составной путь с двумя подпутами: ваш 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());

Ответ 2

На самом деле для большинства случаев гораздо проще, например в Swift:

path.append(cutout.reversing())

Это работает, потому что правило заполнения по умолчанию - это правило ненулевой обмотки.

Ответ 3

Это должно сделать это, отрегулируйте размеры по своему усмотрению:

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];

Еще один очень простой способ получить эффект, который вам нужен, - просто нарисовать внешний округлый, изменить цвета и нарисовать внутреннюю над ним.

Ответ 4

Используя ответ @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(M_PI_2), endAngle: CGFloat(M_PI + M_PI_2), clockwise: false)
leftSubPath.close()

let rightSubPath = UIBezierPath(arcCenter: CGPoint(x:rect.width, y:rect.height / 2), 
                               radius: subPathSideSize / 2, startAngle: CGFloat(M_PI_2), endAngle: CGFloat(M_PI + M_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

enter image description here

enter image description here

Ответ 5

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

CGContextSetBlendMode(ctx, kCGBlendModeClear);

Ответ 6

Патрик предоставил альтернативу ответам пользователя NSRsponder, используя правило ненулевой обмотки. Вот полная реализация в 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()

Вот суть этого.

Вы можете поместить это в Раскадровку, чтобы видеть и играть с выходом.

Результат

Ответ 7

2019 - слишком просто

Я удивлен другими ответами, потому что это чрезвычайно легко сделать. Могут ли быть некоторые требования, которые я не понимаю. Но:

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

Это отлично работает, независимо от того, есть ли у вас резка.

enter image description here

и

enter image description here

(Это отличный способ сделать надрез или углубление.)

Техника

  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 acw currently on iOS

и

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

пока это не работает.

Ответ 8

Вместо вычитания я смог решить, создав CGPath с помощью CGPathAddLineToPoint и CGPathAddArcToPoint. Это также можно сделать с помощью UIBiezerPath с похожим кодом.