问题
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?
回答1:
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.
回答2:
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);
}
}
}
回答3:
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];
}
回答4:
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)
回答5:
@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
}
回答6:
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);
}
回答7:
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);
}
}
}
}
}
回答8:
Here is my general swift implementation.
- For static views add tuples to the holeViews array as (theView, isRound)
- If you want to dynamically assign the views as I needed to, set the generator to something, say maybe
{someViewArray.map{($0,false)}} // array of views, not round
- Use the view's corner radius instead of the isRound flag if you want, isRound is just easier for making circles.
- Note that isRound is really isEllipseThatWillBeRoundIfTheViewIsSquare
- Most code won't need the public's/internal's.
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)
}
}
}
}
}
回答9:
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.
回答10:
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)
}
}
}
回答11:
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.
回答12:
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);
回答13:
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;
}
回答14:
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();
}
回答15:
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.
回答16:
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:
- You create a
UIImage
mask in black and white instead of with/transparent. - Use CIMaskToAlpha
CIFilter
to convert it to a transparent/white mask. - Use the generated
CGImage
as content of aCALayer
- User tht
CALayer
as view mask.
回答17:
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.
来源:https://stackoverflow.com/questions/9711248/cut-transparent-hole-in-uiview