UIBezierPath Subtract Path - PullRequest
       41

UIBezierPath Subtract Path

45 голосов
/ 14 января 2012

Используя [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];

Ответы [ 7 ]

65 голосов
/ 25 августа 2015

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

path.append(cutout.reversing())

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

60 голосов
/ 14 января 2012

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

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

Что вам нужно сделать, это пересечь существующийобтравочный контур с обратным из smallMaskPath.К счастью, вы можете сделать это довольно легко, используя правило с четной и нечетной намоткой.Вы можете прочитать о четно-нечетном правиле в Quartz 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());
33 голосов
/ 14 января 2012

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

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

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

6 голосов
/ 09 марта 2017

Используя ответ @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 голосов
/ 17 апреля 2014

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

CGContextSetBlendMode(ctx, kCGBlendModeClear);

2 голосов
/ 23 апреля 2016

Патрик предоставил улучшение / альтернативу ответу пользователя 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()

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

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

The result

0 голосов
/ 15 января 2012

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

...