CALayer with transparent hole in it
Asked Answered
W

5

121

I have a simple view (left side of the picture) and i need to create some kind of overlay (right side of the picture) to this view. This overlay should have some opacity, so the view bellow it is still partly visible. Most importantly this overlay should have a circular hole in the middle of it so it doesn't overlay the center of the view (see picture bellow).

I can easily create a circle like this :

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;

And a "full" rectangular overlay like this :

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

But I have no idea how can I combine these two layers so they create effect I want. Anyone? I've tried really everything... Thanks a lot for help!

Image

Widget answered 12/5, 2013 at 22:39 Comment(5)
Can you create one bezier which contains the rect and the circle and then the winding rule used during drawing will create a hole (I haven't tried it).Preponderance
i dont know how to do it :)Widget
Create with the rect, then moveToPoint, then add the rounded rect. Check the docs for the methods offered by UIBezierPath.Preponderance
See if this similar question and answer help: [Cut transparent hole in UIView][1] [1]: #9711748Tewell
Check out my solution here: #14141581 Hopefully this helps someonePipette
W
229

I was able to solve this with Jon Steinmetz suggestion. If any one cares, here's the final solution:

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)
Widget answered 13/5, 2013 at 9:27 Comment(8)
For added flexibility, make your view subclass "IBDesignable". It's really easy! To get started, plug the above code into the answer I gave to this question: #14141581Adenosine
As a novice iOS developer I've spent few hours trying to figure out, why this code produces weird results. Finally I found, that added sublayers must be removed if overlay mask is recalculated at some point. This is possible via view.layer.sublayers property. Thank you very much for answer!Sundae
Why I am getting the exact opposite of it. Clear colored layer with black semi transparent shape??Casares
How i can add a transparent text to a circle using this mode, is possible? i don't find howLaundress
Almost 6 years, still helps, but keep in mind that the hollow hole does not really 'punch through' the layer that holds it. Say, if overlay the hole over a button. The button is not accessible, which is needed if you are trying to make a 'guided tutorial' like me. The library provided by @Nick Yap will do the job for you, where by overriding func point(inside point: CGPoint, with event: UIEvent?) -> Bool {} of UIView. Check out his library for more details. But, what you expect is just 'visibility to what is behind the mask', this is a valid answer.Nonentity
Can any one provide any documentation for this on how its achieved, I'm getting weird(connected) paths if I add three or more exclusions for the layer path.Holmann
this is a much preferred and better approach than doing it with masking.Pluckless
Yeah but how would you do this if you didn't just want a solid grey circle or constant 0.5 fillOpacity, but one with varying alpha across the mask, like a fading circle? (say your CAShapeLayer was masked with a radial CAGradientLayer)Levitan
S
34

To create this effect, I found it easiest to create an entire view overlaying the screen, then subtracting portions of the screen using layers and UIBezierPaths. For a Swift implementation:

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

I tested the code above, and here is the result:

enter image description here

I added a library to CocoaPods that abstracts away a lot of the above code and allows you to easily create overlays with rectangular/circular holes, allowing the user to interact with views behind the overlay. I used it to create this tutorial for one of our apps:

Tutorial using the TAOverlayView

The library is called TAOverlayView, and is open source under Apache 2.0. I hope you find it useful!

Saree answered 15/3, 2016 at 15:13 Comment(7)
Also, please do not post duplicate answers. Instead, consider other actions that could help future users find the answer they need, as described in the linked post. When those answers are barely more than a link & recommendation to use your stuff, they appear pretty spammy.Blindfish
@Blindfish I didn't want to appear spammy, I just spent a good amount of time on this library and I figured it would be useful to people trying to do similar things. But thanks for the feedback, I'll update my answers to use code examplesSaree
Nice update, Nick. I'm in your corner - I've got published libraries and utilities myself, and I understand that it can seem redundant to put complete answers here when my documentation already covers it... however the idea is to keep answers as self-contained as possible. And there are people posting nothing but spam, so I'd rather not be lumped in with them. I assume you're of the same mind, which is why I pointed it out to you. Cheers!Blindfish
I used the pod you created, thanks for it. But my views beneath the overlay stop interacting. Whats wrong with it? I have a Scrollview with imageview inside it.Veliger
@AmmarMujeeb The overlay blocks interaction except through the "holes" that you create. My intention with the pod was overlays that highlight portions of the screen, and only allow you to interact with the highlighted elements.Saree
Thanks, man! Your answer's helped me to make a rect with cornered angles via Objective-C.Shanklin
Thanks Nick for the answer. But keep in mind that if you use the code provided above, the hollow hole does not really 'punch through' the layer that holds it. Say, if overlay the hole over a button. The button is not accessible, which is needed if you are trying to make a 'guided tutorial' like me. But the library provided by Nick will do the job for you. The difference is overriding 'func point(inside point: CGPoint, with event: UIEvent?) -> Bool {}' of UIView.Nonentity
S
11

Accepted solution 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)
Siclari answered 6/10, 2016 at 13:59 Comment(1)
@Fattie: your link is deadSiclari
M
10

I took a similar approach as animal_chin, but I'm more visual, so I set most of it up in Interface Builder using outlets and auto layout.

Here is my solution 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
Mario answered 7/7, 2015 at 17:29 Comment(2)
I love this solution because you can move the circlePath around at design time and run time very easily.Botts
this didn't work for me, although i modified it to use a normal rect instead of oval, but the final mask image is just coming out wrong :(Brucie
G
7

Code Swift 2.0 compatible

Starting from @animal_inch answer, I code a little utility-class, hope it will appreciate:

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

}

Then, wherever in your code:

let circle = CircleMaskView(drawIn: <target_view>)
circle.opacity = 0.7
circle.draw()
Gefen answered 12/10, 2015 at 8:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.