121 votes

CALayer avec un trou transparent

J'ai une vue simple (côté gauche de l'image) et je dois créer une sorte de superposition (côté droit de l'image) à cette vue. Cette superposition doit avoir une certaine opacité, de sorte que la vue située en dessous reste partiellement visible. Plus important encore, cette superposition doit avoir un trou circulaire en son milieu afin qu'elle ne recouvre pas le centre de la vue (voir l'image ci-dessous).

Je peux facilement créer un cercle comme celui-ci :

int radius = 20; //whatever
CAShapeLayer *circle = [CAShapeLayer layer];

circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0,radius,radius) cornerRadius:radius].CGPath;
circle.position = CGPointMake(CGRectGetMidX(view.frame)-radius,
                              CGRectGetMidY(view.frame)-radius);
circle.fillColor = [UIColor clearColor].CGColor;

Et une superposition rectangulaire "complète" comme celle-ci :

CAShapeLayer *shadow = [CAShapeLayer layer];
shadow.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, view.bounds.size.width, view.bounds.size.height) cornerRadius:0].CGPath;
shadow.position = CGPointMake(0, 0);
shadow.fillColor = [UIColor grayColor].CGColor;
shadow.lineWidth = 0;
shadow.opacity = 0.5;
[view.layer addSublayer:shadow];

Mais je n'ai aucune idée de la façon dont je peux combiner ces deux couches pour qu'elles créent l'effet que je veux. Quelqu'un ? J'ai vraiment tout essayé... Merci beaucoup pour votre aide !

Image

229voto

animal_chin Points 1090

J'ai pu résoudre ce problème grâce à la suggestion de Jon Steinmetz. Si ça intéresse quelqu'un, voici la solution finale :

int radius = myRect.size.width;
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, self.mapView.bounds.size.width, self.mapView.bounds.size.height) cornerRadius:0];
UIBezierPath *circlePath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius) cornerRadius:radius];
[path appendPath:circlePath];
[path setUsesEvenOddFillRule:YES];

CAShapeLayer *fillLayer = [CAShapeLayer layer];
fillLayer.path = path.CGPath;
fillLayer.fillRule = kCAFillRuleEvenOdd;
fillLayer.fillColor = [UIColor grayColor].CGColor;
fillLayer.opacity = 0.5;
[view.layer addSublayer:fillLayer];

Swift 3.x :

let radius = myRect.size.width
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: self.mapView.bounds.size.width, height: self.mapView.bounds.size.height), cornerRadius: 0)
let circlePath = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: 2 * radius, height: 2 * radius), cornerRadius: radius)
path.append(circlePath)
path.usesEvenOddFillRule = true

let fillLayer = CAShapeLayer()
fillLayer.path = path.cgPath
fillLayer.fillRule = kCAFillRuleEvenOdd
fillLayer.fillColor = Color.background.cgColor
fillLayer.opacity = 0.5
view.layer.addSublayer(fillLayer)

Swift 4.2 et 5 :

let radius: CGFloat = myRect.size.width
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: self.view.bounds.size.width, height: self.view.bounds.size.height), cornerRadius: 0)
let circlePath = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: 2 * radius, height: 2 * radius), cornerRadius: radius)
path.append(circlePath)
path.usesEvenOddFillRule = true

let fillLayer = CAShapeLayer()
fillLayer.path = path.cgPath
fillLayer.fillRule = .evenOdd
fillLayer.fillColor = view.backgroundColor?.cgColor
fillLayer.opacity = 0.5
view.layer.addSublayer(fillLayer)

34voto

yapster Points 85

Pour créer cet effet, j'ai trouvé que le plus simple était de créer une vue entière recouvrant l'écran, puis de soustraire des parties de l'écran à l'aide de calques et de UIBezierPaths. Pour une mise en œuvre en Swift :

// Create a view filling the screen.
let overlay = UIView(frame: CGRectMake(0, 0, 
    UIScreen.mainScreen().bounds.width,
    UIScreen.mainScreen().bounds.height))

// Set a semi-transparent, black background.
overlay.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.85)

// Create the initial layer from the view bounds.
let maskLayer = CAShapeLayer()
maskLayer.frame = overlay.bounds
maskLayer.fillColor = UIColor.blackColor().CGColor

// Create the frame for the circle.
let radius: CGFloat = 50.0
let rect = CGRectMake(
        CGRectGetMidX(overlay.frame) - radius,
        CGRectGetMidY(overlay.frame) - radius,
        2 * radius,
        2 * radius)

// Create the path.
let path = UIBezierPath(rect: overlay.bounds)
maskLayer.fillRule = kCAFillRuleEvenOdd

// Append the circle to the path so that it is subtracted.
path.appendPath(UIBezierPath(ovalInRect: rect))
maskLayer.path = path.CGPath

// Set the mask of the view.
overlay.layer.mask = maskLayer

// Add the view so it is visible.
self.view.addSubview(overlay)

J'ai testé le code ci-dessus, et voici le résultat :

enter image description here

J'ai ajouté une bibliothèque à CocoaPods qui fait abstraction d'une grande partie du code ci-dessus et vous permet de créer facilement des superpositions avec des trous rectangulaires/circulaires, permettant à l'utilisateur d'interagir avec les vues derrière la superposition. Je l'ai utilisée pour créer ce tutoriel pour l'une de nos applications :

Tutorial using the TAOverlayView

La bibliothèque s'appelle TAOverlayView et est open source sous Apache 2.0. J'espère que vous le trouverez utile !

14voto

Jon Steinmetz Points 2785

Une façon d'y parvenir est d'utiliser un calque de masque. Vous pouvez trouver la documentation d'Apple aquí .

Voici quelques exemples intéressants aquí y aquí .

En fait, vous créez un deuxième CALayer dont le canal alpha agit comme un masque pour le calque auquel il est assigné comme masque. Dans votre cas, où vous voulez un trou au milieu d'un calque autrement rempli, vous pouvez toujours utiliser un calque de forme. Pour créer cette forme, vous pourriez dessiner le rectangle, puis le cercle et définir la règle d'enroulement pair-impair pour que le cercle soit un trou dans la forme. Voir ma réponse à cette question pour plus de détails. Vous pouvez définir ce paramètre sur le calque de forme utilisé comme masque à l'aide de la commande fillRule propriété de la couche de forme.

11voto

Randy Points 2169

Solution acceptée Swift 3.0 compatible

let radius = myRect.size.width
let path = UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: self.mapView.bounds.size.width, height: self.mapView.bounds.size.height), cornerRadius: 0)
let circlePath = UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: 2.0*radius, height: 2.0*radius), cornerRadius: radius)
path.append(circlePath)
path.usesEvenOddFillRule = true

let fillLayer = CAShapeLayer()
fillLayer.path = path.cgPath
fillLayer.fillRule = kCAFillRuleEvenOdd
fillLayer.fillColor = UIColor.gray.cgColor
fillLayer.opacity = 0.5
view.layer.addSublayer(fillLayer)

10voto

skwashua Points 137

J'ai adopté une approche similaire à celle d'animal_chin, mais je suis plus visuel. J'ai donc configuré la plupart des éléments dans Interface Builder en utilisant des points de vente et la mise en page automatique.

Voici ma solution en Swift

    //shadowView is a UIView of what I want to be "solid"
    var outerPath = UIBezierPath(rect: shadowView.frame)

    //croppingView is a subview of shadowView that is laid out in interface builder using auto layout
    //croppingView is hidden.
    var circlePath = UIBezierPath(ovalInRect: croppingView.frame)
    outerPath.usesEvenOddFillRule = true
    outerPath.appendPath(circlePath)

    var maskLayer = CAShapeLayer()
    maskLayer.path = outerPath.CGPath
    maskLayer.fillRule = kCAFillRuleEvenOdd
    maskLayer.fillColor = UIColor.whiteColor().CGColor

    shadowView.layer.mask = maskLayer

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X