Reputation: 5032
Looking to create a view that has a transparent frame inside of it so that the views behind the view can be seen through this transparent frame, but areas outside of this will not show through. So essentially a window within the view.
Hoping to be able to do something like this:
CGRect hole = CGRectMake(100, 100, 250, 250);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [UIColor blackColor].CGColor);
CGContextFillRect(context, rect);
CGContextAddRect(context, hole);
CGContextClip(context);
CGContextSetFillColorWithColor(context, [UIColor clearColor].CGColor);
CGContextFillRect(context, rect);
but the clear does not override the black so whole background is black. Any ideas along these lines?
Upvotes: 53
Views: 38952
Reputation: 34331
iOS UIView add a hole
Key point us evenOdd
let view = self.imageView!
let horizontalPerc = 0.8
let verticalPerc = 0.2
let bigFrame = view.frame
let bigFrameSize = bigFrame.size
let smallFrameSizeWidth = bigFrameSize.width * horizontalPerc
let smallFrameSizeHeight = bigFrameSize.height * verticalPerc
let smallFrameCenterX = view.center.x - smallFrameSizeWidth / 2
let smallFrameCenterY = view.center.y - smallFrameSizeHeight / 2
var smallRect = CGRect(
origin: CGPoint(
x: smallFrameCenterX,
y: smallFrameCenterY
),
size: CGSize(
width: smallFrameSizeWidth,
height: smallFrameSizeHeight
)
)
let bigPath = UIBezierPath(rect: bigFrame)
let smallPath = UIBezierPath(roundedRect: smallRect, cornerRadius: 14)
bigPath.append(smallPath)
bigPath.usesEvenOddFillRule = true
let fillLayer = CAShapeLayer()
fillLayer.path = bigPath.cgPath
fillLayer.fillRule = .evenOdd
fillLayer.fillColor = UIColor.black.cgColor
fillLayer.opacity = 0.7
self.view.layer.addSublayer(fillLayer)
Upvotes: 0
Reputation: 434
Another solution: The Big rect is all the view (yellow color) and the small is the transparent rect. The color opacity is settable.
let pathBigRect = UIBezierPath(rect: bigRect)
let pathSmallRect = UIBezierPath(rect: smallRect)
pathBigRect.appendPath(pathSmallRect)
pathBigRect.usesEvenOddFillRule = true
let fillLayer = CAShapeLayer()
fillLayer.path = pathBigRect.CGPath
fillLayer.fillRule = kCAFillRuleEvenOdd
fillLayer.fillColor = UIColor.yellowColor().CGColor
//fillLayer.opacity = 0.4
view.layer.addSublayer(fillLayer)
Upvotes: 35
Reputation: 1189
I used the answer from Bushra Shahid and it worked good, but it has a problem if the circles overlap each other.
I used this different approach which works well in such case:
class HoleView: UIView {
var holes: [CGRect] = [] {
didSet {
lastProcessedSize = .zero
createMask()
}
}
private var lastProcessedSize = CGSize.zero
override func layoutSubviews() {
super.layoutSubviews()
createMask()
}
private func createMask() {
guard lastProcessedSize != frame.size,
holes.count > 0
else { return }
let size = frame.size
// create image
UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)
guard let context = UIGraphicsGetCurrentContext()
else { return }
UIColor.white.setFill()
context.fill(CGRect(origin: .zero, size: size))
UIColor.black.setFill()
holes.forEach { context.fillEllipse(in: $0) }
// apply filter to convert black to transparent
guard let image = UIGraphicsGetImageFromCurrentImageContext(),
let cgImage = image.cgImage,
let filter = CIFilter(name: "CIMaskToAlpha")
else { return }
filter.setDefaults()
filter.setValue(CIImage(cgImage: cgImage), forKey: kCIInputImageKey)
guard let result = filter.outputImage,
let cgMaskImage = CIContext().createCGImage(result, from: result.extent)
else { return }
// Create mask
let mask = CALayer()
mask.frame = bounds
mask.contents = cgMaskImage
layer.mask = mask
}
}
In summary:
UIImage
mask in black and white instead of with/transparent.CIMaskToAlpha
CIFilter
to convert it to a transparent/white mask.CGImage
as content of a CALayer
CALayer
as view mask.Upvotes: 1
Reputation: 4747
You can achieve this by giving the view's layer a border.
class HollowSquareView: UIView {
override func awakeFromNib() {
super.awakeFromNib()
self.backgroundColor = UIColor.clear
self.layer.masksToBounds = true
self.layer.borderColor = UIColor.black.cgColor
self.layer.borderWidth = 10.0
}
}
This will give you a square frame of width 10 and a transparent core.
You can also set the layer's cornerRadius
to half the view's width and this will give you a hollow circle.
Upvotes: 1
Reputation: 449
Implementation of the @Lefteris answer on Swift 4:
import UIKit
class PartialTransparentView: UIView {
var rectsArray: [CGRect]?
convenience init(rectsArray: [CGRect]) {
self.init()
self.rectsArray = rectsArray
backgroundColor = UIColor.black.withAlphaComponent(0.6)
isOpaque = false
}
override func draw(_ rect: CGRect) {
backgroundColor?.setFill()
UIRectFill(rect)
guard let rectsArray = rectsArray else {
return
}
for holeRect in rectsArray {
let holeRectIntersection = rect.intersection(holeRect)
UIColor.clear.setFill()
UIRectFill(holeRectIntersection)
}
}
}
Upvotes: 5
Reputation: 2269
Including an answer for Xamarin Studio iOS using C#. This draws a single rounded rectangle with 60% Alpha. Mostly taken from the answer from @mikeho
public override void Draw(CGRect rect)
{
base.Draw(rect);
//Allows us to draw a nice clear rounded rect cutout
CGContext context = UIGraphics.GetCurrentContext();
// Create a path around the entire view
UIBezierPath clipPath = UIBezierPath.FromRect(rect);
// Add the transparent window to a sample rectangle
CGRect sampleRect = new CGRect(0f, 0f, rect.Width * 0.5f, rect.Height * 0.5f);
UIBezierPath path = UIBezierPath.FromRoundedRect(sampleRect, sampleRect.Height * 0.25f);
clipPath.AppendPath(path);
// This sets the algorithm used to determine what gets filled and what doesn't
clipPath.UsesEvenOddFillRule = true;
context.SetFillColor(UIColor.Black.CGColor);
context.SetAlpha(0.6f);
clipPath.Fill();
}
Upvotes: 0
Reputation: 1821
in this code create more than circle
- (void)drawRect:(CGRect)rect {
// Drawing code
UIColor *bgcolor=[UIColor colorWithRed:0.85 green:0.85 blue:0.85 alpha:1.0f];//Grey
[bgcolor setFill];
UIRectFill(rect);
if(!self.initialLoad){//If the view has been loaded from next time we will try to clear area where required..
// clear the background in the given rectangles
for (NSValue *holeRectValue in _rectArray) {
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect holeRect = [holeRectValue CGRectValue];
[[UIColor clearColor] setFill];
CGRect holeRectIntersection = CGRectIntersection( holeRect, rect );
CGContextSetFillColorWithColor( context, [UIColor clearColor].CGColor );
CGContextSetBlendMode(context, kCGBlendModeClear);
CGContextFillEllipseInRect( context, holeRectIntersection );
}
}
self.initialLoad=NO;
}
Upvotes: 0
Reputation: 887
If you want something quick and effective, I added a library (TAOverlayView) to CocoaPods that allows you to 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:
You can change the background by setting the backgroundColor
of the overlay with something like UIColor(red: 0, green: 0, blue: 0, alpha: 0.85)
, depending on your color and opaqueness needs.
Upvotes: 1
Reputation: 791
Here is my general swift implementation.
{someViewArray.map{($0,false)}} // array of views, not round
Hope it helps someone, thanks to the other contributors
public class HolyView : UIView {
public var holeViews = [(UIView,Bool)]()
public var holeViewsGenerator:(()->[(UIView,Bool)])?
internal var _backgroundColor : UIColor?
public override var backgroundColor : UIColor? {
get {return _backgroundColor}
set {_backgroundColor = newValue}
}
public override func drawRect(rect: CGRect) {
if (backgroundColor == nil) {return}
let ctxt = UIGraphicsGetCurrentContext()
backgroundColor?.setFill()
UIRectFill(rect)
UIColor.whiteColor().setFill()
UIRectClip(rect)
let views = (holeViewsGenerator == nil ? holeViews : holeViewsGenerator!())
for (view,isRound) in views {
let r = convertRect(view.bounds, fromView: view)
if (CGRectIntersectsRect(rect, r)) {
let radius = view.layer.cornerRadius
if (isRound || radius > 0) {
CGContextSetBlendMode(ctxt, kCGBlendModeDestinationOut);
UIBezierPath(roundedRect: r,
byRoundingCorners: .AllCorners,
cornerRadii: (isRound ? CGSizeMake(r.size.width/2, r.size.height/2) : CGSizeMake(radius,radius))
).fillWithBlendMode(kCGBlendModeDestinationOut, alpha: 1)
}
else {
UIRectFillUsingBlendMode(r, kCGBlendModeDestinationOut)
}
}
}
}
}
Upvotes: 1
Reputation: 3579
@mosib's answer was a lot of help for me until I wanted to draw more than one circular cutouts in my view. After struggling a little bit, I updated my drawRect like this (code in swift...sorry bad editing):
override func drawRect(rect: CGRect)
{
backgroundColor.setFill()
UIRectFill(rect)
let layer = CAShapeLayer()
let path = CGPathCreateMutable()
for aRect in self.rects
{
let holeEnclosingRect = aRect
CGPathAddEllipseInRect(path, nil, holeEnclosingRect) // use CGPathAddRect() for rectangular hole
/*
// Draws only one circular hole
let holeRectIntersection = CGRectIntersection(holeRect, rect)
let context = UIGraphicsGetCurrentContext()
if( CGRectIntersectsRect(holeRectIntersection, rect))
{
CGContextBeginPath(context);
CGContextAddEllipseInRect(context, holeRectIntersection)
//CGContextDrawPath(context, kCGPathFillStroke)
CGContextClip(context)
//CGContextClearRect(context, holeRectIntersection)
CGContextSetFillColorWithColor(context, UIColor.clearColor().CGColor)
CGContextFillRect(context, holeRectIntersection)
CGContextClearRect(context, holeRectIntersection)
}*/
}
CGPathAddRect(path, nil, self.bounds)
layer.path = path
layer.fillRule = kCAFillRuleEvenOdd
self.layer.mask = layer
}
Upvotes: 8
Reputation: 1633
This implementation supports Rectangles and Circles, written in swift: PartialTransparentMaskView
class PartialTransparentMaskView: UIView{
var transparentRects: Array<CGRect>?
var transparentCircles: Array<CGRect>?
weak var targetView: UIView?
init(frame: CGRect, backgroundColor: UIColor?, transparentRects: Array<CGRect>?, transparentCircles: Array<CGRect>?, targetView: UIView?) {
super.init(frame: frame)
if((backgroundColor) != nil){
self.backgroundColor = backgroundColor
}
self.transparentRects = transparentRects
self.transparentCircles = transparentCircles
self.targetView = targetView
self.opaque = false
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func drawRect(rect: CGRect) {
backgroundColor?.setFill()
UIRectFill(rect)
// clear the background in the given rectangles
if let rects = transparentRects {
for aRect in rects {
var holeRectIntersection = CGRectIntersection( aRect, rect )
UIColor.clearColor().setFill();
UIRectFill(holeRectIntersection);
}
}
if let circles = transparentCircles {
for aRect in circles {
var holeRectIntersection = aRect
let context = UIGraphicsGetCurrentContext();
if( CGRectIntersectsRect( holeRectIntersection, rect ) )
{
CGContextAddEllipseInRect(context, holeRectIntersection);
CGContextClip(context);
CGContextClearRect(context, holeRectIntersection);
CGContextSetFillColorWithColor( context, UIColor.clearColor().CGColor)
CGContextFillRect( context, holeRectIntersection);
}
}
}
}
}
Upvotes: 1
Reputation: 7000
I used UIBezierPath
to handle cutting out the transparent hole.
The following code goes into a subclass of the UIView
that you want to draw a transparent hole:
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
CGContextRef context = UIGraphicsGetCurrentContext();
// Clear any existing drawing on this view
// Remove this if the hole never changes on redraws of the UIView
CGContextClearRect(context, self.bounds);
// Create a path around the entire view
UIBezierPath *clipPath = [UIBezierPath bezierPathWithRect:self.bounds];
// Your transparent window. This is for reference, but set this either as a property of the class or some other way
CGRect transparentFrame;
// Add the transparent window
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:transparentFrame cornerRadius:5.0f];
[clipPath appendPath:path];
// NOTE: If you want to add more holes, simply create another UIBezierPath and call [clipPath appendPath:anotherPath];
// This sets the algorithm used to determine what gets filled and what doesn't
clipPath.usesEvenOddFillRule = YES;
// Add the clipping to the graphics context
[clipPath addClip];
// set your color
UIColor *tintColor = [UIColor blackColor];
// (optional) set transparency alpha
CGContextSetAlpha(context, 0.7f);
// tell the color to be a fill color
[tintColor setFill];
// fill the path
[clipPath fill];
}
Upvotes: 12
Reputation: 1013
Lefteris Answer is absolutely right, however, it creates transparent Rects. For CIRCULAR transparent layer, modify draw rect as
- (void)drawRect:(CGRect)rect {
[backgroundColor setFill];
UIRectFill(rect);
for (NSValue *holeRectValue in rectsArray) {
CGRect holeRect = [holeRectValue CGRectValue];
CGRect holeRectIntersection = CGRectIntersection( holeRect, rect );
CGContextRef context = UIGraphicsGetCurrentContext();
if( CGRectIntersectsRect( holeRectIntersection, rect ) )
{
CGContextAddEllipseInRect(context, holeRectIntersection);
CGContextClip(context);
CGContextClearRect(context, holeRectIntersection);
CGContextSetFillColorWithColor( context, [UIColor clearColor].CGColor );
CGContextFillRect( context, holeRectIntersection);
}
}
}
Upvotes: 21
Reputation: 14687
This is my implementation (as I did needed a view with transparent parts):
Header (.h) file:
// Subclasses UIview to draw transparent rects inside the view
#import <UIKit/UIKit.h>
@interface PartialTransparentView : UIView {
NSArray *rectsArray;
UIColor *backgroundColor;
}
- (id)initWithFrame:(CGRect)frame backgroundColor:(UIColor*)color andTransparentRects:(NSArray*)rects;
@end
Implementation (.m) file:
#import "PartialTransparentView.h"
#import <QuartzCore/QuartzCore.h>
@implementation PartialTransparentView
- (id)initWithFrame:(CGRect)frame backgroundColor:(UIColor*)color andTransparentRects:(NSArray*)rects
{
backgroundColor = color;
rectsArray = rects;
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.opaque = NO;
}
return self;
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
// Drawing code
[backgroundColor setFill];
UIRectFill(rect);
// clear the background in the given rectangles
for (NSValue *holeRectValue in rectsArray) {
CGRect holeRect = [holeRectValue CGRectValue];
CGRect holeRectIntersection = CGRectIntersection( holeRect, rect );
[[UIColor clearColor] setFill];
UIRectFill(holeRectIntersection);
}
}
@end
Now to add a view with partial transparency, you need to import the PartialTransparentView custom UIView subclass, then use it as follows:
NSArray *transparentRects = [[NSArray alloc] initWithObjects:[NSValue valueWithCGRect:CGRectMake(0, 50, 100, 20)],[NSValue valueWithCGRect:CGRectMake(0, 150, 10, 20)], nil];
PartialTransparentView *transparentView = [[PartialTransparentView alloc] initWithFrame:CGRectMake(0,0,200,400) backgroundColor:[UIColor colorWithWhite:1 alpha:0.75] andTransparentRects:rects];
[self.view addSubview:backgroundView];
This will create a view with 2 transparent rects. Of course you can add as many rects as you wish, or just use one. The above code is only handling rectangles, so if you wish to use circles, you will have to modify it.
Upvotes: 47
Reputation: 1494
This will do the clipping:
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor( context, [UIColor blueColor].CGColor );
CGContextFillRect( context, rect );
CGRect holeRectIntersection = CGRectIntersection( CGRectMake(50, 50, 50, 50), rect );
if( CGRectIntersectsRect( holeRectIntersection, rect ) )
{
CGContextAddEllipseInRect(context, holeRectIntersection);
CGContextClip(context);
CGContextClearRect(context, holeRectIntersection);
CGContextSetFillColorWithColor( context, [UIColor clearColor].CGColor );
CGContextFillRect( context, holeRectIntersection);
}
Upvotes: 6
Reputation: 5032
Ended up "Faking" it
windowFrame is a property
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [UIColor clearColor].CGColor);
CGContextFillRect(context, rect);
CGRect rootFrame = [[Navigation rootController] view].frame;
CGSize deviceSize = CGSizeMake(rootFrame.size.width, rootFrame.size.height);
CGRect topRect = CGRectMake(0, 0, deviceSize.width, windowFrame.origin.y);
CGRect leftRect = CGRectMake(0, topRect.size.height, windowFrame.origin.x, windowFrame.size.height);
CGRect rightRect = CGRectMake(windowFrame.size.width+windowFrame.origin.x, topRect.size.height, deviceSize.width-windowFrame.size.width+windowFrame.origin.x, windowFrame.size.height);
CGRect bottomRect = CGRectMake(0, windowFrame.origin.y+windowFrame.size.height, deviceSize.width, deviceSize.height-windowFrame.origin.y+windowFrame.size.height);
CGContextSetFillColorWithColor(context, [UIColor blackColor].CGColor);
CGContextFillRect(context, topRect);
CGContextFillRect(context, leftRect);
CGContextFillRect(context, rightRect);
CGContextFillRect(context, bottomRect);
Upvotes: 0
Reputation: 8944
Well i'll have to answer as missed the comment and filled an answer form :) I really'd like Carsten to provide more information about the best way to do what he propose.
You could use
+ (UIColor *)colorWithPatternImage:(UIImage *)image
to create a background 'color' image of any complexity. An image could be either created programmatically if you are familiar with drawing classes or statically if the windows frames are predefined.
Upvotes: 0
Reputation: 137
Do it the other way around! Place those views you would like to see through the "hole" in a separate view of the right size. Then set "clipsToBounds" to YES and put that view on top. The view with the "transparent" frame is the undermost then. "clipsToBounds" means everything outside of the box/hole is cut off.
Then you might have to deal how touches are handled. But that's another question. Maybe setting userInteractionEnabled on the according views is enough.
Upvotes: -3