CALayer avec trou transparent dedans

112

J'ai une vue simple (côté gauche de l'image) et j'ai besoin de 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 ci-dessous est encore partiellement visible. Plus important encore, cette superposition doit avoir un trou circulaire au 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 ne sais pas comment combiner ces deux couches pour qu'elles créent l'effet que je veux. N'importe qui? J'ai vraiment tout essayé ... Merci beaucoup pour votre aide!

Image

animal_chin
la source
Pouvez-vous créer un bezier qui contient le rect et le cercle, puis la règle d'enroulement utilisée lors du dessin créera un trou (je ne l'ai pas essayé).
Wain
je ne sais pas comment faire :)
animal_chin
Créez avec le rect, moveToPointpuis ajoutez le rect arrondi. Consultez la documentation pour les méthodes proposées par UIBezierPath.
Wain
Voir si cette question et réponse similaire aide: [Couper le trou transparent dans UIView] [1] [1]: stackoverflow.com/questions/9711248
...
Découvrez ma solution ici: stackoverflow.com/questions/14141081/… J'espère que cela aide quelqu'un
James Laurenstin

Réponses:

218

J'ai pu résoudre ce problème avec la suggestion de Jon Steinmetz. Si quelqu'un s'en soucie, 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)
animal_chin
la source
2
Pour plus de flexibilité, créez votre sous-classe de vue "IBDesignable". C'est vraiment simple! Pour commencer, branchez le code ci-dessus dans la réponse que j'ai donnée à cette question: stackoverflow.com/questions/14141081/…
clozach
2
En tant que développeur iOS novice, j'ai passé quelques heures à essayer de comprendre pourquoi ce code produit des résultats étranges. Enfin, j'ai trouvé que les sous-couches ajoutées doivent être supprimées si le masque de superposition est recalculé à un moment donné. Cela est possible via la propriété view.layer.sublayers. Merci beaucoup pour la réponse!
Serzhas le
Pourquoi j'obtiens exactement le contraire. Couche de couleur claire avec une forme semi-transparente noire ??
Chanchal Warde
Comment puis-je ajouter un texte transparent à un cercle en utilisant ce mode, est-ce possible? Je ne trouve pas comment
Diego Fernando Murillo Valenci
Presque 6 ans, ça aide encore, mais gardez à l'esprit que le trou creux ne «perce» pas vraiment la couche qui le retient. Dites, si vous superposez le trou sur un bouton. Le bouton n'est pas accessible, ce qui est nécessaire si vous essayez de faire un «tutoriel guidé» comme moi. La bibliothèque fournie par @Nick Yap fera le travail pour vous, où en remplaçant le point func (point intérieur: CGPoint, avec event: UIEvent?) -> Bool {} de UIView. Consultez sa bibliothèque pour plus de détails. Mais, ce que vous attendez, c'est juste une «visibilité sur ce qui se cache derrière le masque», c'est une réponse valable.
infinity_coding7
32

Pour créer cet effet, j'ai trouvé qu'il était plus facile de créer une vue entière superposant l'écran, puis de soustraire des parties de l'écran à l'aide de calques et d'UIBezierPaths. Pour une implémentation 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:

entrez la description de l'image ici

J'ai ajouté une bibliothèque à CocoaPods qui résume 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é pour créer ce tutoriel pour l'une de nos applications:

Tutoriel utilisant TAOverlayView

La bibliothèque s'appelle TAOverlayView et est open source sous Apache 2.0. J'espère que tu trouves cela utile!

Nick Yap
la source
Veuillez également ne pas publier de réponses en double . Au lieu de cela, envisagez d'autres actions qui pourraient aider les futurs utilisateurs à trouver la réponse dont ils ont besoin, comme décrit dans l'article lié. Lorsque ces réponses sont à peine plus qu'un lien et une recommandation pour utiliser votre contenu, elles semblent assez spammées.
Mogsdad
1
@Mogsdad Je ne voulais pas paraître spammé, j'ai juste passé beaucoup de temps sur cette bibliothèque et j'ai pensé que ce serait utile pour les gens qui essaient de faire des choses similaires. Mais merci pour les commentaires, je vais mettre à jour mes réponses pour utiliser des exemples de code
Nick Yap
3
Bonne mise à jour, Nick. Je suis dans votre coin - j'ai moi-même des bibliothèques et des utilitaires publiés, et je comprends que cela peut sembler redondant de mettre des réponses complètes ici alors que ma documentation le couvre déjà ... mais l'idée est de garder les réponses comme indépendantes que possible. Et il y a des gens qui ne publient que du spam, alors je préfère ne pas être mêlé à eux. Je suppose que vous êtes du même avis, c'est pourquoi je vous l'ai signalé. À votre santé!
Mogsdad
J'ai utilisé le pod que vous avez créé, merci pour cela. Mais mes vues sous la superposition cessent d'interagir. Qu'est ce qui ne va pas avec ça? J'ai un Scrollview avec imageview à l'intérieur.
Ammar Mujeeb
@AmmarMujeeb La superposition bloque l'interaction sauf à travers les "trous" que vous créez. Mon intention avec le pod était des superpositions qui mettent en évidence des parties de l'écran et vous permettent uniquement d'interagir avec les éléments en surbrillance.
Nick Yap
11

Solution acceptée Compatible Swift 3.0

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)
Excité
la source
@Fattie: votre lien est mort
Randy
10

J'ai adopté une approche similaire à animal_chin, mais je suis plus visuel, donc j'ai mis la plupart d'entre eux dans Interface Builder en utilisant des prises et une mise en page automatique.

Voici ma solution dans 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
Skwashua
la source
J'adore cette solution car vous pouvez déplacer le circlePath au moment de la conception et de l'exécution très facilement.
Mark Moeykens
cela n'a pas fonctionné pour moi, bien que je l'ai modifié pour utiliser un rect normal au lieu d'un ovale, mais l'image finale du masque sort juste mal :(
GameDev
7

Compatible Code Swift 2.0

À partir de la réponse @animal_inch, je code une petite classe utilitaire, j'espère qu'elle appréciera:

import Foundation
import UIKit
import CoreGraphics

/// Apply a circle mask on a target view. You can customize radius, color and opacity of the mask.
class CircleMaskView {

    private var fillLayer = CAShapeLayer()
    var target: UIView?

    var fillColor: UIColor = UIColor.grayColor() {
        didSet {
            self.fillLayer.fillColor = self.fillColor.CGColor
        }
    }

    var radius: CGFloat? {
        didSet {
            self.draw()
        }
    }

    var opacity: Float = 0.5 {
        didSet {
           self.fillLayer.opacity = self.opacity
        }
    }

    /**
    Constructor

    - parameter drawIn: target view

    - returns: object instance
    */
    init(drawIn: UIView) {
        self.target = drawIn
    }

    /**
    Draw a circle mask on target view
    */
    func draw() {
        guard (let target = target) else {
            print("target is nil")
            return
        }

        var rad: CGFloat = 0
        let size = target.frame.size
        if let r = self.radius {
            rad = r
        } else {
            rad = min(size.height, size.width)
        }

        let path = UIBezierPath(roundedRect: CGRectMake(0, 0, size.width, size.height), cornerRadius: 0.0)
        let circlePath = UIBezierPath(roundedRect: CGRectMake(size.width / 2.0 - rad / 2.0, 0, rad, rad), cornerRadius: rad)
        path.appendPath(circlePath)
        path.usesEvenOddFillRule = true

        fillLayer.path = path.CGPath
        fillLayer.fillRule = kCAFillRuleEvenOdd
        fillLayer.fillColor = self.fillColor.CGColor
        fillLayer.opacity = self.opacity
        self.target.layer.addSublayer(fillLayer)
    }

    /**
    Remove circle mask
    */


  func remove() {
        self.fillLayer.removeFromSuperlayer()
    }

}

Ensuite, n'importe où dans votre code:

let circle = CircleMaskView(drawIn: <target_view>)
circle.opacity = 0.7
circle.draw()
Luca Davanzo
la source