web-dev-qa-db-fra.com

UIBezierPath Subtract Path

En utilisant [UIBezierPath bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:], je peux créer une vue arrondie, telle que:

rounded view

Comment pourrais-je soustraire un autre chemin de celui-ci (ou d'une autre manière) pour créer un chemin comme celui-ci:

subtracted view

Existe-t-il un moyen de faire quelque chose comme ceci? Pseudocode:

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

Si vous voulez tracer le chemin soustrait, vous êtes seul. Apple ne fournit pas d'API qui retourne (ou juste des traits) la soustraction d'un chemin d'accès à un autre.

Si vous souhaitez simplement remplir le tracé soustrait (comme dans votre exemple d’image), vous pouvez le faire à l’aide du tracé de détourage. Vous devez utiliser un truc, cependant. Lorsque vous ajoutez un tracé au tracé de détourage, le nouveau tracé est le intersection de l'ancien tracé et le tracé ajouté. Donc, si vous ajoutez simplement smallMaskPath au tracé de détourage, vous finirez par ne remplir que la région située à l'intérieur de smallMaskPath, ce qui est le contraire de ce que vous voulez.

Ce que vous devez faire est d'intersecter le tracé de détourage existant avec le inverse de smallMaskPath. Heureusement, vous pouvez le faire assez facilement à l'aide de la règle de l'enroulement pair-impair. Vous pouvez en savoir plus sur la règle des couples pairs dans le Guide de programmation 2D Quartz .

L'idée de base est que nous créons un chemin composé avec deux sous-chemins: votre smallMaskPath et un grand rectangle qui entoure complètement votre smallMaskPath et tous les autres pixels que vous pourriez vouloir remplir. En raison de la règle paire-impaire, chaque pixel à l'intérieur de smallMaskPath sera traité comme en dehors du chemin composé, et chaque pixel à l'extérieur de smallMaskPath sera traité comme à l'intérieur du chemin composé.

Créons donc ce chemin composé. Nous allons commencer par l'énorme rectangle. Et il n'y a pas de rectangle plus énorme que le rectangle infini:

UIBezierPath *clipPath = [UIBezierPath bezierPathWithRect:CGRectInfinite];

Nous en faisons maintenant un chemin composé en y ajoutant smallMaskPath:

[clipPath appendPath:smallMaskPath];

Ensuite, nous définissons le chemin pour utiliser la règle paire-impaire:

clipPath.usesEvenOddFillRule = YES;

Avant de découper dans ce chemin, nous devrions enregistrer l'état des graphiques afin d'annuler la modification du tracé de détourage lorsque nous aurons terminé:

CGContextSaveGState(UIGraphicsGetCurrentContext()); {

Nous pouvons maintenant modifier le tracé de détourage:

    [clipPath addClip];

et nous pouvons remplir bigMaskPath:

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

Enfin, nous restaurons l'état graphique en annulant la modification du chemin de détourage:

} CGContextRestoreGState(UIGraphicsGetCurrentContext());

Voici le code dans son ensemble au cas où vous voudriez le copier/coller:

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

CGContextSaveGState(UIGraphicsGetCurrentContext()); {
    [clipPath addClip];
    [[UIColor orangeColor] setFill];
    [bigMaskPath fill];
} CGContextRestoreGState(UIGraphicsGetCurrentContext());
59
rob mayoff

En fait, il existe un moyen beaucoup plus simple dans la plupart des cas, par exemple dans Swift:

path.append(cutout.reversing())

Cela fonctionne car la règle de remplissage par défaut est la règle d'enroulement non-nulle .

60
Patrick Pijnappel

Cela devrait le faire, ajuster les tailles comme vous le souhaitez:

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

Un autre moyen très simple d’obtenir l’effet recherché est de dessiner le rectangle arrondi extérieur, de changer de couleur et d’attirer l’intérieur par dessus.

32
NSResponder

En utilisant @Patrick Pijnappel answer, vous pouvez préparer un terrain de test pour des tests rapides

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
gbk

Patrick a fourni une amélioration/alternative à la réponse de l'utilisateur NSResponder en utilisant la règle règle de non-zéro . Voici une implémentation complète dans Swift pour tous ceux qui recherchent une réponse plus complète.

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()

Voici un résumé de cela.

Vous pouvez mettre cela dans un Storyboard pour voir et jouer avec la sortie.

 The result

1
timgcarlson

Au lieu de soustraire, j'ai pu résoudre le problème en créant un CGPath en utilisant CGPathAddLineToPoint et CGPathAddArcToPoint. Cela peut aussi être fait avec une UIBiezerPath avec un code similaire.

0
jadengeller