CALayer z przezroczystym otworem

112

Mam prosty widok (lewa strona obrazu) i potrzebuję stworzyć jakąś nakładkę (prawa strona obrazu) do tego widoku. Ta nakładka powinna mieć trochę nieprzezroczystości, więc poniższy widok jest nadal częściowo widoczny. Co najważniejsze, ta nakładka powinna mieć okrągły otwór pośrodku, aby nie nakładała się na środek widoku (patrz rysunek poniżej).

Mogę łatwo utworzyć taki krąg:

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;

Oraz „pełną” prostokątną nakładkę, taką jak ta:

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

Ale nie mam pojęcia, jak mogę połączyć te dwie warstwy, aby uzyskać efekt, który chcę. Ktoś? Naprawdę próbowałem wszystkiego ... Wielkie dzięki za pomoc!

Wizerunek

animal_chin
źródło
Czy potrafisz stworzyć jeden bezier, który zawiera prostokąt i okrąg, a następnie reguła nawijania używana podczas rysowania utworzy dziurę (nie próbowałem tego).
Wain
nie wiem jak to zrobić :)
animal_chin
Następnie utwórz prostokątem, a moveToPointnastępnie dodaj prostokąt zaokrąglony. Sprawdź w dokumentacji metody oferowane przez UIBezierPath.
Wain
Zobacz, czy to podobne pytanie i odpowiedź pomogą: [Wytnij przezroczystą dziurę w UIView] [1] [1]: stackoverflow.com/questions/9711248/…
dichen
Sprawdź moje rozwiązanie tutaj: stackoverflow.com/questions/14141081/ ... Mam nadzieję, że to komuś pomoże
James Laurenstin

Odpowiedzi:

218

Udało mi się to rozwiązać dzięki sugestii Jona Steinmetza. Jeśli kogoś to obchodzi, oto ostateczne rozwiązanie:

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 i 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
źródło
2
Aby zwiększyć elastyczność, utwórz podklasę widoku „IBDesignable”. To naprawdę proste! Aby rozpocząć, podłącz powyższy kod do odpowiedzi, której udzieliłem na to pytanie: stackoverflow.com/questions/14141081/ ...
clozach
2
Jako początkujący programista iOS spędziłem kilka godzin, próbując dowiedzieć się, dlaczego ten kod daje dziwne wyniki. Wreszcie odkryłem, że dodane podwarstwy muszą zostać usunięte, jeśli maska ​​nakładki zostanie ponownie obliczona w pewnym momencie. Jest to możliwe poprzez właściwość view.layer.sublayers. Bardzo dziękuję za odpowiedź!
Serzhas
Dlaczego otrzymuję dokładne przeciwieństwo tego. Przezroczysta kolorowa warstwa z czarnym półprzezroczystym kształtem?
Chanchal Warde
Jak mogę dodać przezroczysty tekst do koła za pomocą tego trybu, czy jest to możliwe? nie wiem jak
Diego Fernando Murillo Valenci
Prawie 6 lat nadal pomaga, ale należy pamiętać, że wydrążona dziura tak naprawdę nie „przebija” warstwy, w której się znajduje. Powiedz, jeśli nałóż dziurę na przycisk. Przycisk nie jest dostępny, co jest potrzebne, jeśli próbujesz stworzyć „samouczek z przewodnikiem”, taki jak ja. Biblioteka dostarczona przez @Nick Yap wykona zadanie za Ciebie, gdzie poprzez nadpisanie punktu func (punkt wewnętrzny: CGPoint, zdarzeniem: UIEvent?) -> Bool {} z UIView. Więcej informacji znajdziesz w jego bibliotece. Ale to, czego oczekujesz, to po prostu „widoczność tego, co kryje się za maską”, to ważna odpowiedź.
infinity_coding7
32

Aby stworzyć ten efekt, najłatwiej było utworzyć cały widok nakładający się na ekran, a następnie odjąć części ekranu za pomocą warstw i UIBezierPaths. Szybkie wdrożenie:

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

Przetestowałem powyższy kod i oto wynik:

wprowadź opis obrazu tutaj

Dodałem bibliotekę do CocoaPods, która usuwa wiele z powyższego kodu i umożliwia łatwe tworzenie nakładek z prostokątnymi / okrągłymi otworami, umożliwiając użytkownikowi interakcję z widokami za nakładką. Użyłem go do stworzenia tego samouczka dla jednej z naszych aplikacji:

Samouczek dotyczący korzystania z TAOverlayView

Biblioteka nazywa się TAOverlayView i jest open source pod Apache 2.0. Mam nadzieję, że okażą się przydatne!

Nick Yap
źródło
Nie wysyłaj też zduplikowanych odpowiedzi . Zamiast tego rozważ inne działania, które mogą pomóc przyszłym użytkownikom znaleźć odpowiedź, której potrzebują, zgodnie z opisem w poście, do którego prowadzi łącze. Kiedy te odpowiedzi są ledwie linkiem i zaleceniem użycia Twoich rzeczy, wydają się dość spamerskie.
Mogsdad
1
@Mogsdad Nie chciałem wyglądać na spamującego, po prostu spędziłem sporo czasu nad tą biblioteką i pomyślałem, że będzie to przydatne dla osób próbujących robić podobne rzeczy. Ale dzięki za opinię, zaktualizuję moje odpowiedzi, aby użyć przykładów kodu
Nick Yap
3
Niezła aktualizacja, Nick. Jestem w twoim kącie - sam mam opublikowane biblioteki i narzędzia i rozumiem, że umieszczanie tutaj pełnych odpowiedzi może wydawać się zbędne, gdy moja dokumentacja już je obejmuje ... jednak chodzi o to, aby odpowiedzi były samodzielne jak to możliwe. I tam ludzie delegowania nic oprócz spamu, więc raczej nie będzie jednego worka z nimi. Zakładam, że masz to samo zdanie i dlatego ci to zwróciłem. Twoje zdrowie!
Mogsdad
Użyłem kapsuły, którą stworzyłeś, dzięki za to. Ale moje widoki pod nakładką przestają oddziaływać. Co jest z tym nie tak? Mam Scrollview z widokiem obrazu w środku.
Ammar Mujeeb
@AmmarMujeeb Nakładka blokuje interakcję z wyjątkiem interakcji przez „dziury”, które tworzysz. Moją intencją w przypadku kapsuły były nakładki, które podświetlają fragmenty ekranu i pozwalają tylko na interakcję z podświetlonymi elementami.
Nick Yap
11

Zaakceptowane rozwiązanie zgodne ze 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)
Niespokojny
źródło
@Fattie: Twój link nie działa
Randy
10

Przyjąłem podobne podejście jak animal_chin, ale jestem bardziej wizualny, więc większość z nich ustawiłem w Interface Builder za pomocą gniazd i automatycznego układu.

Oto moje rozwiązanie w 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
źródło
Uwielbiam to rozwiązanie, ponieważ możesz bardzo łatwo przesuwać circlePath w czasie projektowania i uruchamiania.
Mark Moeykens
to nie zadziałało dla mnie, chociaż zmodyfikowałem go tak, aby używał normalnego prostownika zamiast owalu, ale końcowy obraz maski po prostu wychodzi źle :(
GameDev
7

Kompatybilny z Code Swift 2.0

Zaczynając od odpowiedzi @animal_inch, koduję małą klasę użytkową, mam nadzieję, że to doceni:

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

}

Następnie, gdziekolwiek w kodzie:

let circle = CircleMaskView(drawIn: <target_view>)
circle.opacity = 0.7
circle.draw()
Luca Davanzo
źródło