Mask a UIView with a cut-out circle
Asked Answered
A

3

18

I’ve created a UIView with a semi-transparent black background to sit across my main app’s view. What I’m aiming for here is to create a cut-out circle shape somewhere in that UIView where the semi-transparent black background is not seen, giving the effect of a black mask over the whole image except the circular area (this is to highlight a button in that area).

I’ve managed to use CAShapeLayer to create a circular mask, but this has had the opposite effect (i.e. the rest of the view is clear, and INSIDE the circle is the semi-transparent black. What I’d like is everything outside of the circle to keep the semi-transparent black, then what’s inside to be clear. Here’s the code I used, how would I go about making it work in the opposite way? My blackMask is the semi-transparent black view, and my buttonCircle is the circle I’d like to be kept clear.

Perhaps I need to invert the path, somehow?

CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
CGRect maskRect = buttonCircle.frame;
CGPathRef path = CGPathCreateWithEllipseInRect(maskRect, NULL);
maskLayer.path = path;
CGPathRelease(path);
blackMask.layer.mask = maskLayer;

EDIT: Trying this now, not seeing any mask at all with this one:

UIView *circularMaskView = [[UIView alloc] initWithFrame:buttonCircle.frame];
circularMaskView.backgroundColor = [UIColor greenColor];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];

CGPathRef path = CGPathCreateWithEllipseInRect(buttonCircle.frame, NULL);
maskLayer.path = path;
CGPathRelease(path);

circularMaskView.layer.mask = maskLayer;

[blackMask addSubview:circularMaskView];
Antilles answered 20/1, 2014 at 11:21 Comment(10)
try adding an extra view back of your button (same frame) and add it as subview to blackMaskEyla
This won’t work, the button itself isn’t a circle. I just want to draw a circle around it.Antilles
i mean create a new view, set background color of it to somevalue(non-transparent).set frame of this new view same as buttonCircle.frame. apply the mask to this new View (path =buttonCircle.bounds) and add it as subview to blackMask.Eyla
should i post complete answer or did you get it?Eyla
Create an inverse path like this?Dragline
This doesn’t seem to work with [UIColor clearColor]. I can make a green mask or something that’s the right colour, position, etc., but if I set the colour to clear, it doesn’t work. I’ve checked opaque on the new view.Antilles
@lukech mistake in your code. it should be CGPathCreateWithEllipseInRect(buttonCircle.bound, NULL); not CGPathCreateWithEllipseInRect(buttonCircle.frame, NULL); should be bounds not frame.Eyla
Yup, that’s right, thanks for pointing that out :) Unfortunately, it still doesn’t work with a clearColor. Works fine with anything else, but clear isn’t cutting through the black behind it at all.Antilles
Did you figure it out regarding the clearColor?Needlework
Try the answer below, worked fine for me :)Antilles
A
18

So, here’s what I did. I created a custom UIView subclass called BlackoutView, like so:

BlackoutView.h

#import <UIKit/UIKit.h>

@interface BlackoutView : UIView

@property (nonatomic, retain) UIColor *fillColor;
@property (nonatomic, retain) NSArray *framesToCutOut;

@end

BlackoutView.m

#import "BlackoutView.h"

@implementation BlackoutView

- (void)drawRect:(CGRect)rect
{
    [self.fillColor setFill];
    UIRectFill(rect);

    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetBlendMode(context, kCGBlendModeDestinationOut);

    for (NSValue *value in self.framesToCutOut) {
        CGRect pathRect = [value CGRectValue];
        UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:pathRect];
        [path fill];
    }

    CGContextSetBlendMode(context, kCGBlendModeNormal);
}

@end

I then instantiate it as normal, and set the properties to the colour of the mask I want to use, and the frames to be cut out of the mask:

[blackMask setFillColor:[UIColor colorWithWhite:0.0f alpha:0.8f]];
[blackMask setFramesToCutOut:@[[NSValue valueWithCGRect:buttonCircleA.frame],
                               [NSValue valueWithCGRect:buttonCircleB.frame]]];

This could be improved by allowing me to cut out other shapes besides ovals, but it’s fine for my purposes here and would be easily adapted to do so later. Hopefully this helps others!

Antilles answered 20/1, 2014 at 14:9 Comment(6)
Super helpful! Can you explain why you set the fillColor inside BlackoutView.m? I wasn't getting any transparency and removing those first 2 lines solved the problem for me.Stromberg
Also, do you need to add CGContextRelease(context); at the end of drawRect:rectStromberg
I've tried a lot of solutions but this really helped me! Thanks a lot :)Harpp
There was an error in the fill method which resulted in a solid black background color. Edited to fix that up.Aw
I just set the background colour using the standard backgroundColor and removed the fillColor property and first two lines in drawRect. Setting the fillColor still resulted in a solid black colour. I am working on iOS8, however I'm not sure this has anything to do with it.Premier
I also get a black screen still after removing setFill and even with setting backgroundColor standardlyCleasta
S
9

If You are using any Black Image to mask then

-(void)maskWithImage:(UIImage*)maskImage toImageView:(UIImageView*)imgView{

    CALayer *aMaskLayer=[CALayer layer];
    aMaskLayer.contents=(id)maskImage.CGImage;

    imgView.layer.mask=aMaskLayer;


}

If you have any Custom ShapeLayer to mask

-(void)maskWithShapeLayer:(CALayer*)layer toImageView:(UIImageView*)imgView{
    //Layer should be filled with Black color to mask those part of Image
    imgView.layer.mask=layer;
}

If You want to make ImageView Mask with Circle

-(void)maskWithCircle:(UIImageView*)imgView{

    CAShapeLayer *aCircle=[CAShapeLayer layer];
    aCircle.path=[UIBezierPath bezierPathWithRoundedRect:imgView.bounds cornerRadius:imgView.frame.size.height/2].CGPath; // Considering the ImageView is square in Shape

    aCircle.fillColor=[UIColor blackColor].CGColor;
    imgView.layer.mask=aCircle;

}
Sisera answered 16/6, 2014 at 12:13 Comment(0)
F
5

Here is my conversion of the accepted answer in Swift:

public class OverlayView: UIView {
let fillColor: UIColor = UIColor.grayColor()
public var framesToCutOut: Array<NSValue> = [NSValue]()

override public func drawRect(rect: CGRect) {
    super.drawRect(rect)

    fillColor.setFill()
    UIRectFill(rect)

    if let context: CGContextRef = UIGraphicsGetCurrentContext() {
        CGContextSetBlendMode(context, .DestinationOut)

        for value in framesToCutOut {
            let pathRect: CGRect = value.CGRectValue()
            let path: UIBezierPath = UIBezierPath(ovalInRect: pathRect)
            path.fill()
        }

        CGContextSetBlendMode(context, .Normal)
    }
}
Falito answered 19/3, 2016 at 11:44 Comment(1)
No need to put CGRects in NSValues if you're calling this swift code with other swift code -- just make frameToCutOut an array of [CGRect]Thetes

© 2022 - 2024 — McMap. All rights reserved.