CALayer mit transparentem Loch

112

Ich habe eine einfache Ansicht (linke Seite des Bildes) und ich muss eine Art Überlagerung (rechte Seite des Bildes) zu dieser Ansicht erstellen. Diese Überlagerung sollte eine gewisse Deckkraft haben, damit die Ansicht darunter teilweise noch sichtbar ist. Am wichtigsten ist, dass diese Überlagerung in der Mitte ein kreisförmiges Loch aufweist, damit sie nicht die Mitte der Ansicht überlagert (siehe Bild unten).

Ich kann leicht einen Kreis wie diesen erstellen:

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;

Und eine "volle" rechteckige Überlagerung wie diese:

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

Aber ich habe keine Ahnung, wie ich diese beiden Ebenen kombinieren kann, damit sie den gewünschten Effekt erzielen. Jemand? Ich habe wirklich alles versucht ... Vielen Dank für die Hilfe!

Bild

animal_chin
quelle
Können Sie einen Bezier erstellen, der das Rechteck und den Kreis enthält, und dann wird die beim Zeichnen verwendete Wicklungsregel ein Loch erzeugen (ich habe es nicht ausprobiert).
Wain
Ich weiß nicht, wie es geht :)
animal_chin
Erstellen Sie dann mit dem Rect und moveToPointfügen Sie dann das abgerundete Rect hinzu. Überprüfen Sie die Dokumente auf die von angebotenen Methoden UIBezierPath.
Wain
Sehen Sie, ob diese ähnliche Frage und Antwort hilft: [Transparentes Loch in UIView schneiden] [1] [1]: stackoverflow.com/questions/9711248/…
dichen
Schauen Sie sich meine Lösung hier an: stackoverflow.com/questions/14141081/… Hoffentlich hilft dies jemandem
James Laurenstin

Antworten:

218

Ich konnte dies mit dem Vorschlag von Jon Steinmetz lösen. Wenn es jemanden interessiert, ist hier die endgültige Lösung:

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 & 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
quelle
2
Machen Sie Ihre Ansicht für zusätzliche Flexibilität zur Unterklasse "IBDesignable". Es ist wirklich einfach! Um zu beginnen, fügen
Sie
2
Als unerfahrener iOS-Entwickler habe ich einige Stunden damit verbracht herauszufinden, warum dieser Code seltsame Ergebnisse liefert. Schließlich stellte ich fest, dass hinzugefügte Unterschichten entfernt werden müssen, wenn die Überlagerungsmaske irgendwann neu berechnet wird. Dies ist über die Eigenschaft view.layer.sublayers möglich. Vielen Dank für die Antwort!
Serzhas
Warum ich genau das Gegenteil davon bekomme. Klare farbige Schicht mit schwarzer halbtransparenter Form?
Chanchal Warde
Wie kann ich in diesem Modus einem Kreis einen transparenten Text hinzufügen? Ist das möglich? Ich finde nicht wie
Diego Fernando Murillo Valenci
Fast 6 Jahre helfen immer noch, aber denken Sie daran, dass das hohle Loch die Schicht, die es hält, nicht wirklich „durchstößt“. Sagen Sie, wenn Sie das Loch über einen Knopf legen. Die Schaltfläche ist nicht zugänglich. Dies ist erforderlich, wenn Sie versuchen, ein "geführtes Tutorial" wie mich zu erstellen. Die von @Nick Yap bereitgestellte Bibliothek erledigt die Aufgabe für Sie, indem sie den Funktionspunkt überschreibt (Innenpunkt: CGPoint, mit Ereignis: UIEvent?) -> Bool {} von UIView. Weitere Informationen finden Sie in seiner Bibliothek. Was Sie jedoch erwarten, ist nur "Sichtbarkeit dessen, was sich hinter der Maske befindet". Dies ist eine gültige Antwort.
infinity_coding7
32

Um diesen Effekt zu erzielen, war es am einfachsten, eine gesamte Ansicht über dem Bildschirm zu erstellen und dann Teile des Bildschirms mithilfe von Ebenen und UIBezierPaths zu subtrahieren. Für eine schnelle Implementierung:

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

Ich habe den obigen Code getestet und hier ist das Ergebnis:

Geben Sie hier die Bildbeschreibung ein

Ich habe CocoaPods eine Bibliothek hinzugefügt, die einen Großteil des obigen Codes abstrahiert und es Ihnen ermöglicht, auf einfache Weise Überlagerungen mit rechteckigen / kreisförmigen Löchern zu erstellen, sodass der Benutzer mit Ansichten hinter der Überlagerung interagieren kann. Ich habe es verwendet, um dieses Tutorial für eine unserer Apps zu erstellen:

Tutorial mit TAOverlayView

Die Bibliothek heißt TAOverlayView und ist unter Apache 2.0 Open Source. Ich hoffe du findest es nützlich!

Nick Yap
quelle
Bitte posten Sie auch keine doppelten Antworten . Ziehen Sie stattdessen andere Aktionen in Betracht, die zukünftigen Benutzern helfen könnten, die Antwort zu finden, die sie benötigen, wie im verlinkten Beitrag beschrieben. Wenn diese Antworten kaum mehr als ein Link und eine Empfehlung zur Verwendung Ihrer Inhalte sind, erscheinen sie ziemlich spammig.
Mogsdad
1
@Mogsdad Ich wollte nicht spammig erscheinen, ich habe nur viel Zeit in dieser Bibliothek verbracht und dachte, es wäre nützlich für Leute, die versuchen, ähnliche Dinge zu tun. Aber danke für das Feedback, ich werde meine Antworten aktualisieren, um Codebeispiele zu verwenden
Nick Yap
3
Schönes Update, Nick. Ich bin in Ihrer Ecke - ich habe selbst Bibliotheken und Dienstprogramme veröffentlicht, und ich verstehe, dass es überflüssig erscheinen kann, hier vollständige Antworten zu platzieren, wenn meine Dokumentation dies bereits abdeckt. Die Idee ist jedoch, die Antworten als in sich geschlossen zu halten wie möglich. Und es gibt Leute, die nur Spam posten, also möchte ich lieber nicht mit ihnen verwechselt werden. Ich nehme an, Sie sind sich einig, weshalb ich Sie darauf hingewiesen habe. Prost!
Mogsdad
Ich habe den von Ihnen erstellten Pod verwendet, danke dafür. Aber meine Ansichten unter der Überlagerung hören auf zu interagieren. Was stimmt damit nicht? Ich habe eine Bildlaufansicht mit Bildansicht.
Ammar Mujeeb
@AmmarMujeeb Das Overlay blockiert die Interaktion außer durch die von Ihnen erstellten "Löcher". Meine Absicht mit dem Pod waren Überlagerungen, die Teile des Bildschirms hervorheben und nur die Interaktion mit den hervorgehobenen Elementen ermöglichen.
Nick Yap
11

Akzeptierte Lösung Swift 3.0 kompatibel

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)
Geil
quelle
@ Fattie: Ihr Link ist tot
Randy
10

Ich habe einen ähnlichen Ansatz wie animal_chin gewählt, bin aber visueller, daher habe ich das meiste davon in Interface Builder mithilfe von Outlets und automatischem Layout eingerichtet.

Hier ist meine Lösung in 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
quelle
Ich liebe diese Lösung, weil Sie den circlePath zur Entwurfszeit und zur Laufzeit sehr einfach verschieben können.
Mark Moeykens
Dies hat bei mir nicht funktioniert, obwohl ich es so modifiziert habe, dass ein normales Rect anstelle eines Ovals verwendet wird, aber das endgültige Maskenbild ist einfach falsch :(
GameDev
7

Code Swift 2.0 kompatibel

Ausgehend von der Antwort @animal_inch codiere ich eine kleine Utility-Klasse und hoffe, dass sie Folgendes zu schätzen weiß:

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

}

Dann, wo immer in Ihrem Code:

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