I'm trying to darken the surrounding area of my UIImageView, and leave a portion alone (which I define with my mask).
Right now I'm defining my mask and setting my imageView.layer.mask, however instead of darkening the rest of the image, it's completely removing it instead.
Example of the type of effect I want:
Example of what I'm getting:
The reference docs mention that the mask uses it's layer's alpha, so I've tried manipulating the mask's opacity. However, that only seems to affect the opacity of the portion I want to leave alone, while the rest of the image is still being cut out completely.
Can anyone point out what I'm doing wrong? Thanks.
Here's my code:
CAShapeLayer *mask = [CAShapeLayer layer];
GMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, nil, 1052, 448);
CGPathAddLineToPoint(path, nil, 2, 484);
CGPathAddLineToPoint(path, nil, 54, 1263);
CGPathAddLineToPoint(path, nil, 56, 1305);
CGPathAddLineToPoint(path, nil, 380, 1304);
CGPathAddLineToPoint(path, nil, 1050, 1311);
CGPathCloseSubpath(path);
mask.path = path;
CGPathRelease(path);
//mask.opacity = 0.5; //doesn't affect the surrounding portion, only the cut out area.
self.imageView.layer.mask = mask;
What you're doing wrong is using a layer mask in the first place. You are trying to shade or darken an area of your image. That's not what a layer mask does at all! Basically a layer mask punches through in an existing layer, causing whatever is behind it to show through. That's exactly what you discovered:
it's completely removing it instead
Yes, because that's what layer masks do! If you don't want that, why are you using a layer mask?
What you want is just to lay a second image view (or just a sublayer) over the first one. It contains an image that you draw. It is transparent except where it has a semi-transparent dark color fill. That will darken what's behind it. You use a clipping path to define the area that doesn't get the dark color fill.
Alternatively, alter the image in your image view by drawing on top of it with compositing or possibly using a CIFilter.
In case anyone else has the same issue: if the goal is to darken the layer, the mask is not suitable. But if you need to make a part of the image transparent or semi-transparent, it is definitely a great way to go.
For example, this code could live in an NSView subclass:
let imageLayer = CALayer()
let maskLayer = CAShapeLayer()
let ovalPath = NSBezierPath(ovalIn: bounds)
maskLayer.path = ovalPath
maskLayer.autoresizingMask = [.layerWidthSizable, .layerHeightSizable]
maskLayer.frame = bounds
maskLayer.backgroundColor = NSColor.black.withAlphaComponent(0.2).cgColor
imageLayer.contents = NSImage(named: "sampleImage")
imageLayer.autoresizingMask = [.layerWidthSizable, .layerHeightSizable]
imageLayer.frame = bounds
imageLayer.mask = maskLayer
This gives 0.2 opacity to an oval 'hole' in the imageLayer.
Related
I tried to add a mask layer on a UIImageView. This imageview hold an image and the mask layer hold another image by setting its contents. But when I add the mask layer onto the layer of imageview, the mask layer doesn't hold any image. The mask layer had been initialized by getter. The method adding mask layer is below:
- (void)circleButtonClicked{
self.maskLayer.contents = (id)[UIImage imageNamed:#"mask.jpg"].CGImage;
self.maskLayer.frame = CGRectMake(0, 0, kSCREEN_WIDTH, kSCREEN_WIDTH);
self.maskLayer.opacity = 0.5;
[self.imageView.layer addSublayer:self.maskLayer];
UIBezierPath * pPath = [UIBezierPath bezierPathWithArcCenter:self.imageView.center radius:(kSCREEN_WIDTH-100)/2 startAngle:0.0 endAngle:M_PI*2 clockwise:YES];
self.maskLayer.path = pPath.CGPath;
UIBezierPath * otherPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, kSCREEN_WIDTH, kSCREEN_WIDTH)];
self.maskLayer.path = otherPath.CGPath;
[otherPath appendPath:pPath];
self.maskLayer.path = otherPath.CGPath;
self.maskLayer.fillRule = kCAFillRuleEvenOdd;
}
Shape layers are used for displaying paths not images. There are three different ways to provide layer content (see Core Animation Programming Guide) but only one at a time.
There are some things in your code which I don't understand:
Your maskLayer is just a normal layer and not a mask. When you want to use it as mask, you should use the mask property of CALayer and not add it as sublayer.
Your layer should contain an image and a path, and should be a sublayer in an image view. I think its easier to use a image view and add it as subview to imageView.
If you want to use an image as mask, which should be possible, you shouldn't use a JPEG image as source. Only the alpha values of a mask layer are considered, but the alpha values of the whole JPEG image are one. Thus, using a JPEG image is equivalent to using a filled square, or applying no mask.
In simple words only part of my icon must change color at some event.
I drawing an icon with curves inside an UIView. The drawing boilds
down to combining several paths together like this (swift code):
let combinedPath = CGPathCreateMutableCopy(bezierPath.CGPath)
CGPathAddPath(combinedPath, nil, bezier2Path.CGPath);
CGPathAddPath(combinedPath, nil, bezier3Path.CGPath);
Then I'm adding the combined path to the UIView like this (obj-c, sorry for
the mix):
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.frame = self.bounds;
shapeLayer.path = combinedPath;
self.layer.mask = shapeLayer;
And now at some point in time I want to change the fill color of
only one bezierPath with animation. How can I do this?
You can clean the current mask, and redraw any of the bezierpath with stroke/fill color and combine them together, and set it again. And for drawing animation, you can refer to How to Animate CoreGraphics Drawing of Shape Using CAKeyframeAnimation
We have a CALayer which shows image with edit information. Such as rotate, scale and translate. I want to make a CGPath which exactly match the layer bounds. When the layer rotate, my path should also rotate and its four corners should match the CALayer's four corners. The CGPath is actually used as a gray mask to show the clipped area of the image.
I try following code, but it does not work.
f = self.imageLayer.frame;
t = self.imageLayer.affineTransform;
CGPathAddRect(path, &t, f);
The CALayer has its own CGAffineTransform. All edit information are applied via the CGAffineTransform.
Any hint will be appreciated, Thanks a lot.
If I got your question right you could be using UIBezierPath's usesEvenOddFillRule to cut around your image layer bounds dimming the rest of the visible screen. Basically you create a very large rectangular path (for some reasons CGRectInfinite doesn't work here) and cut out the area around your image layer. The trick is to use kCAFillRuleEvenOdd which flips what is considered inside and outside.
Something like should work:
let cutPath = UIBezierPath(roundedRect: imageLayer.bounds, cornerRadius: 0)
let clipPath = UIBezierPath(rect: CGRectMake(-10e6, -10e6, 10e7, 10e7)) // CGRectInfinite isn't working here ?
clipPath.appendPath(cutPath)
clipPath.usesEvenOddFillRule = true
let shape = CAShapeLayer()
shape.contentsScale = UIScreen.mainScreen().scale
shape.lineWidth = 2
shape.fillColor = UIColor(red:0.1, green:0.1, blue:0.1, alpha:0.5).CGColor
shape.fillRule = kCAFillRuleEvenOdd
shape.strokeColor = UIColor.whiteColor().CGColor
shape.path = clipPath.CGPath
imageLayer.addSublayer(shape)
// do a transformations here
imageLayer.transform = CATransform3DMakeRotation(10.0, 1.0, 1.0, 0.8)
Which results
I am doing some custom download progresses bar in side the (subclassed) UIButton (or) i can also do that in UIView, every thing is fine, now i want change the colour of the title as the progress the proceeds, for example as u can see in the below image shows what is now i am getting,
what i want is like below image
i want change the color of the title to white or some other color as download proceeds, is there any way i can do, any sample code for this.
i am achieving this by using below code
- (void)drawInContext:(CGContextRef)progressContext
{
CGContextSetFillColorWithColor(progressContext, self.progressLayerColor.CGColor);
CGMutablePathRef Path = CGPathCreateMutable();
CGRect boundsRect = self.bounds;
CGPathMoveToPoint(Path, NULL, boundsRect.origin.x, boundsRect.origin.y);
CGPathAddRect(Path, NULL, CGRectMake(0, 0, self.progress, 35));
CGPathCloseSubpath(Path);
CGContextAddPath(progressContext, Path);
CGContextFillPath(progressContext);
CGContextSetBlendMode(progressContext, kCGBlendModeClear);
CGContextFillPath(progressContext);
CGPathRelease(Path);
}
I'd do this with two UILabels, one with a green background and white text, one with a white background and black text, with the green one one on top of the other with the same frame.
The topmost label has a CAShapeLayer as it's layers mask. The CAShapeLayer has a single, wide (at least the height of the label) black stroke across its whole length, from left to right.
By default this would make the whole view look green. So far, so useless.
However, a CAShapeLayer has an (animatable) strokeEnd property. If you set this to 0.0, the green label will be invisible. Set it to 0.5, you see half and half.
You now have a method to animate a progress meter, just by updating the strokeEnd property of this mask layer.
With the help of what #jrturton suggested i am able to make this kind of progress bar,
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = _labelGreen.bounds;//it the label on top with green background and white text color
CGMutablePathRef mutPath = CGPathCreateMutable();
CGPathMoveToPoint(mutPath, NULL, 0, 0);
CGPathAddRect(mutPath, NULL, CGRectMake( 0, 0, progress, _labelGreen.frame.size.height)); //as the progress cganges the width of mask layer also changes
maskLayer.path = mutPath;//set the path
_labelGreen.layer.mask = maskLayer;
the resulted output will be like below image
A trivial solution would be to use 2 UIImageViews. The one below would have an image with white background and black text, and the one above would have green background with white text. The image below would always have the same frame, but the other would start with a width 0. As the download progresses you would increase it's width and it would start to cover the image that is below (with the black font). And of course, you would use the View Mode: left on your expanding image view, to always have the image positioned on the left with no resizing.
I am wondering if it is possible to clip a view to a Bezier Path. What I mean is that I want to be able to see the view only in the region within the closed Bezier Path. The reason for this is that I have the outline of an irregular shape, and I want to fill in the shape gradually with a solid color from top to bottom. If I could make it so that a certain view is only visible within the path then I could simply create a UIView of the color I want and then change the y coordinate of its frame as I please, effectively filling in the shape. If anyone has any better ideas for how to implement this that would be greatly appreciated. For the record the filling of the shape will match the y value of the users finger, so it can't be a continuous animation. Thanks.
Update (a very long time later):
I tried your answer, Rob, and it works great except for one thing. My intention was to move the view being masked while the mask remains in the same place on screen. This is so that I can give the impression of the mask being "filled up" by the view. The problem is that with the code I have written based on your answer, when I move the view the mask moves with it. I understand that that is to be expected because all I did was add it as the mask of the view so it stands to reason that it will move if the thing it's tied to moves. I tried adding the mask as a sublayer of the superview so that it stays put, but that had very weird results. Here is my code:
self.test = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
self.test.backgroundColor = [UIColor greenColor];
[self.view addSubview:self.test];
UIBezierPath *myClippingPath = [UIBezierPath bezierPath];
[myClippingPath moveToPoint:CGPointMake(100, 100)];
[myClippingPath addCurveToPoint:CGPointMake(200, 200) controlPoint1:CGPointMake(self.screenWidth, 0) controlPoint2:CGPointMake(self.screenWidth, 50)];
[myClippingPath closePath];
CAShapeLayer *mask = [CAShapeLayer layer];
mask.path = myClippingPath.CGPath;
self.test.layer.mask = mask;
CGRect firstFrame = self.test.frame;
firstFrame.origin.x += 100;
[UIView animateWithDuration:3 animations:^{
self.test.frame = firstFrame;
}];
Thanks for the help already.
You can do this easily by setting your view's layer mask to a CAShapeLayer.
UIBezierPath *myClippingPath = ...
CAShapeLayer *mask = [CAShapeLayer layer];
mask.path = myClippingPath.CGPath;
myView.layer.mask = mask;
You will need to add the QuartzCore framework to your target if you haven't already.
In Swift ...
let yourCarefullyDrawnPath = UIBezierPath( .. blah blah
let maskForYourPath = CAShapeLayer()
maskForYourPath.path = carefullyRoundedBox.CGPath
layer.mask = maskForYourPath
Just an example of Rob's solution, there's a UIWebView sitting as a subview of a UIView called smoothView. smoothView uses bezierPathWithRoundedRect to make a rounded gray background (notice on right). That works fine.
But if smoothView has only normal clip-subviews, you get this:
If you do what Rob says, you get the rounded corners in smoothView and all subviews ...
Great stuff.