2012-01-14 3 views
37

Utilizzando [UIBezierPath bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:], sono in grado di creare una vista arrotondata, come questa:UIBezierPath Sottrai Percorso

rounded view

Come potrei sottrarre un altro percorso da questo (o qualche altro modo), per creare un percorso come questo:

subtracted view

c'è un modo che io possa fare una cosa del genere? Pseudocodice:

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

risposta

52

Se si vuole tracciare il tracciato sottratto, si è da soli. Apple non fornisce un'API che restituisce (o semplicemente tratti) la sottrazione di un percorso da un altro.

Se si desidera solo riempire il percorso sottratto (come nell'immagine di esempio), è possibile farlo utilizzando il tracciato di ritaglio. Devi usare un trucco, però. Quando aggiungi un tracciato al tracciato di ritaglio, il nuovo tracciato di ritaglio è l'intersezione del vecchio percorso di ritaglio e il percorso aggiunto. Quindi, se si aggiunge smallMaskPath al tracciato di ritaglio, si finisce per riempire solo la regione all'interno di smallMaskPath, che è l'opposto di ciò che si desidera.

Quello che devi fare è intersecare il tracciato di ritaglio esistente con inverso di smallMaskPath. Fortunatamente, puoi farlo abbastanza facilmente usando la regola di avvolgimento pari-dispari. Puoi leggere la regola pari pari a Quartz 2D Programming Guide.

L'idea di base è creare un percorso composto con due sottotracciati: il tuo smallMaskPath e un rettangolo enorme che racchiude completamente il tuo smallMaskPath e ogni altro pixel che potresti voler riempire. A causa della regola pari, ogni pixel all'interno di smallMaskPath verrà considerato come esterno al percorso composto e ogni pixel esterno a smallMaskPath verrà considerato all'interno del percorso composto.

Quindi creiamo questo percorso composto. Inizieremo con l'enorme rettangolo.E non c'è rettangolo più grande rispetto al rettangolo di infinito:

UIBezierPath *clipPath = [UIBezierPath bezierPathWithRect:CGRectInfinite]; 

Ora facciamo in un tracciato composto con l'aggiunta di smallMaskPath ad esso:

[clipPath appendPath:smallMaskPath]; 

Poi abbiamo impostato il percorso per utilizzare il modo uguale su ogni regola dispari:

clipPath.usesEvenOddFillRule = YES; 

Prima di clip a questo percorso, dovremmo salvare lo stato grafico in modo da poter annullare la modifica al tracciato di ritaglio quando abbiamo finito:

CGContextSaveGState(UIGraphicsGetCurrentContext()); { 

Ora possiamo modificare il tracciato di ritaglio:

[clipPath addClip]; 

e possiamo riempire bigMaskPath:

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

Infine ripristinare lo stato grafico, annullare la modifica al tracciato di ritaglio:

} CGContextRestoreGState(UIGraphicsGetCurrentContext()); 

Ecco il co de tutti insieme nel caso in cui si desidera copiare/incollare:

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

CGContextSaveGState(UIGraphicsGetCurrentContext()); { 
    [clipPath addClip]; 
    [[UIColor orangeColor] setFill]; 
    [bigMaskPath fill]; 
} CGContextRestoreGState(UIGraphicsGetCurrentContext()); 
+6

Questo è il modo più lavoro di quanto è necessario. È semplice creare un tracciato composto da rotte interne ed esterne e riempirlo con la regola pari. Non è necessario fare nulla per il percorso della clip. – NSResponder

+0

La tua risposta è più semplice quando smallMaskPath è interamente contenuto da bigPathMask, come nell'esempio di Jaden10. Nel caso generale in cui smallMaskPath include anche pixel esterni a bigMaskPath, il tuo approccio più semplice non funzionerà. –

+5

Mi piace lo stile che usi per mettere il GState tra parentesi !! – To1ne

31

Questo dovrebbe farlo, regolare le dimensioni a piacere:

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 altro modo molto semplice per ottenere l'effetto che stai dopo è solo disegnare il roundrect esterno, cambiare i colori e disegnare quello interno su di esso.

+0

Questo sembra davvero promettente per quello che sto cercando di fare, tuttavia UIBezierPath e NSColor non si mescolano da quando uno è iOS e l'altro è OSX. Per caso, hai un esempio più completo che funziona su iOS? –

+0

Crea un 'CAShapeLayer' e aggiungilo come sottostrato della tua vista, quindi imposta le proprietà' path' e 'frame' del nuovo layer. È possibile impostare la proprietà 'path' sulla versione' CGPath' di 'UIBezierPath', ad es. '[uiBezierPathInstance CGPath]'. – jdc

0

Invece di sottrarre, sono stato in grado di risolvere creando CGPath utilizzando CGPathAddLineToPoint e CGPathAddArcToPoint. Questo può essere fatto anche con un UIBiezerPath con codice simile.

4

Ho battuto la testa contro il muro cercando di capire come farlo con più CGPath sovrapposti. Se si sovrappongono più di una volta, si ricarica con le soluzioni fornite sopra. Si scopre che il modo per raggiungere veramente un effetto di "sottrazione" con più percorsi sovrapposti è semplicemente impostare la modalità di fusione del contesto per cancellare.

CGContextSetBlendMode(ctx, kCGBlendModeClear);

39

in realtà c'è un modo molto più semplice per la maggior parte dei casi, ad esempio in Swift:

path.append(cutout.reversing()) 

Questo funziona perché la regola di riempimento predefinito è il non-zero winding rule.

+2

Wow! Super elegante e sorprendente! –

+0

Questo è esattamente ciò di cui avevo bisogno. Più semplice della risposta accettata :) – Orgmir

+0

NSResponder's answer was good, but this great! Grazie! – timgcarlson

0

Patrick ha fornito un miglioramento/alternativa alla risposta NSResponder dell'utente utilizzando non-zero winding rule. Ecco una implementazione completa in Swift per tutti coloro che cercano una risposta ampliata.

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

Here's a gist of this.

si può mettere questo in uno storyboard per vedere e giocare con l'uscita.

The result

1

Utilizzando @Patrick Pijnappel risposta, si può preparare un parco giochi di prova per un rapido test

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 

shapeLayer.mask = mask 

view 
PlaygroundPage.current.liveView = view 

enter image description here