I am trying to create a circle which is filled depending upon a certain percentage. Here is pic of the effect I am going for:
I have a UIView and to this I add a CAShapeLayer which draws the circle. I am then creating another UIShapeLayer as a square to match the UIView containing the circle. Then setting the height of this depending on the figure. So if the square is 100px high and the value is 10% then I set the square to 10px so that it fills 10% of circle.
The image below shows 50% of the circle being filled. As you can see it covers the UIView as well as the circle. However when I try to set the CAShapeLayer to mask to bounds ( circle.maskToBounds) the circle complete disappears along with the square that I am adding as it's subview.
Here is my code:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
[self drawCircleInView:view];
[self.view addSubview:view];
}
- (void)drawCircleInView:(UIView *)v
{
// Set up the shape of the circle
int radius = v.frame.size.height / 2;
CAShapeLayer *circle = [CAShapeLayer layer];
// Make a circular shape
circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2*radius, 2*radius)
cornerRadius:radius].CGPath;
// Center the shape in self.view
circle.position = CGPointMake(CGRectGetMidX(v.bounds)-radius,
CGRectGetMidY(v.bounds)-radius);
// Configure the apperence of the circle
circle.fillColor = [UIColor blueColor].CGColor;
circle.strokeColor = [UIColor clearColor].CGColor;
circle.lineWidth = 5;
circle.masksToBounds = YES; // HERE IS THE LINE OF CODE THAT MAKES THE CIRCLE DISAPPEAR
CAShapeLayer *sublayer = [CAShapeLayer layer];
sublayer.backgroundColor = [UIColor orangeColor].CGColor;
sublayer.opacity = .5f;
sublayer.frame = CGRectMake(0, 100, 200, 100);
[circle addSublayer:sublayer];
// Add to parent layer
[v.layer addSublayer:circle];
}
I am wondering why setting circle.masksToBounds = YES is making the circle and the sublayer disappear completely. My understanding is that by setting this it should only show the sublayer over the circle.
Many thanks in advance.
Try this:
- (void)drawCircleInView:(UIView *)v {
// Set up the shape of the circle
CGSize size = v.bounds.size;
CALayer *layer = [CALayer layer];
layer.frame = v.bounds;
layer.backgroundColor = [UIColor blueColor].CGColor;
CALayer *sublayer = [CALayer layer];
sublayer.frame = CGRectMake(0, size.height/2, size.width, size.height/2);
sublayer.backgroundColor = [UIColor orangeColor].CGColor;
sublayer.opacity = .5f;
CAShapeLayer *mask = [CAShapeLayer layer];
mask.frame = v.bounds;
mask.path = [UIBezierPath bezierPathWithOvalInRect:v.bounds].CGPath;
mask.fillColor = [UIColor blackColor].CGColor;
mask.strokeColor = [UIColor clearColor].CGColor;
[layer addSublayer:sublayer];
[layer setMask:mask];
// Add to parent layer
[v.layer addSublayer:layer];
}
maskToBounds is "to bounds" you are not setting bounds nor frame.
It's not necessary, but you'd better to set frame of CAShapeLayer, to prevent unnecessary confusion.
bounds is rectangle, not CAShapeLayer's shape. Use mask layer to mask by shape instead.
circle and sublayer is not need to be CAShapeLayer because it will be masked by shaped mask layer.
To achieve this use a square image with its center circle as transparent. like this image
How to use this image for this effect -
Add a square layer (with lighter color/ or non fill color) first. above that add a layer with the fill color with sane rect. and above all place this image. and change the frame of middle layer that has the fill color to achieve desired effect
Related
My problem is following: I'm trying to cut out a segment out of custom UIView and apply a shadow effect to this view.
In a custom UIView class, I did this:
Created two layers - shadow and mask. Added a shadow layer as a sublayer for this custom view. Then I created a new view, set its mask as the mask layer and added it as a subview to the custom view.
self.backgroundColor = [UIColor clearColor];
CGFloat radius = 40;
float startAngle = -M_PI;
float endAngle = 0;
UIBezierPath *path = [UIBezierPath bezierPathWithRect:self.bounds];
[path moveToPoint:CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMaxY(self.bounds)+radius/1.8)];
[path addArcWithCenter:CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMaxY(self.bounds)+radius/1.8) radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
CAShapeLayer *shadowLayer = [[CAShapeLayer alloc] init];
shadowLayer.frame = self.bounds;
shadowLayer.path = path.CGPath;
shadowLayer.shadowColor = [UIColor grayColor].CGColor;
shadowLayer.shadowOpacity = 0.5;
shadowLayer.shadowRadius = 2;
shadowLayer.masksToBounds = NO;
shadowLayer.shadowOffset = CGSizeMake(2.0, 2.0);
shadowLayer.fillRule = kCAFillRuleEvenOdd;
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = self.bounds;
maskLayer.masksToBounds = NO;
maskLayer.path = path.CGPath;
maskLayer.fillRule = kCAFillRuleEvenOdd;
maskLayer.fillColor = [UIColor whiteColor].CGColor;
[self.layer addSublayer:shadowLayer];
UIView *view = [[UIView alloc] initWithFrame:self.bounds];
view.backgroundColor = [UIColor whiteColor];
view.layer.mask = maskLayer;
[self addSubview:view];
That is what I want to achieve -
and this is what I actually get
If I set clipToBounds = YES, it will cut off the desired shadow effect.
There would be absolutely no problem, if I wanted to cut of a semicircle. Because in this case the semicircle path fully sits inside of view bounds.
But I do need to achieve a result shown in the first image.
I was thinking about building the path line by line, but the problem occurs when it comes to this arc, and the result will probably not be so accurate.
Does anyone have ideas how it could be done?
Thank you!
EDIT:
If the issue is that the path is wrong, do no composite the path in code using a circle and guessing the coordinates. Instead you should export the coordinates actual artwork and use the artwork coordinates for the curve. It looks like you have a sketch file so the easiest way to do this is to copy and paste into paint code which will give you the code for the curve, but in the event you don't have paint code (you should, its amazing for this kind of stuff) you can export the curve as SVG from sketch. If you open the resulting SVG in a text editor you will see its in human readable XML and you can extract the control points from your Bezier curve from there.
I was wondering how do someone achieve the "slide to unlock effect" on a UILabel's text without using a static image as previously asked here.
// I'd like to use the uilable's current text to this sample code but not seem to be able to do it.
// ---> UIImage *textImage = [UIImage imageNamed:#"SlideToUnlock.png"];
CGFloat textWidth = textImage.size.width;
CGFloat textHeight = textImage.size.height;
CALayer *textLayer = [CALayer layer];
textLayer.contents = (id)[textImage CGImage];
textLayer.frame = CGRectMake(10.0f, 215.0f, textWidth, textHeight);
CALayer *maskLayer = [CALayer layer];
// Mask image ends with 0.15 opacity on both sides. Set the background color of the layer
// to the same value so the layer can extend the mask image.
maskLayer.backgroundColor = [[UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.15f] CGColor];
maskLayer.contents = (id)[[UIImage imageNamed:#"Mask.png"] CGImage];
// Center the mask image on twice the width of the text layer, so it starts to the left
// of the text layer and moves to its right when we translate it by width.
maskLayer.contentsGravity = kCAGravityCenter;
maskLayer.frame = CGRectMake(-textWidth, 0.0f, textWidth * 2, textHeight);
// Animate the mask layer's horizontal position
CABasicAnimation *maskAnim = [CABasicAnimation animationWithKeyPath:#"position.x"];
maskAnim.byValue = [NSNumber numberWithFloat:textWidth];
maskAnim.repeatCount = 1e100f;
maskAnim.duration = 1.0f;
[maskLayer addAnimation:maskAnim forKey:#"slideAnim"];
textLayer.mask = maskLayer;
[self.view.layer addSublayer:textLayer];
Thanks
You can use the below Project. It is a UILabel with slide to unlock animation
http://code4app.net/ios/Animated-Label/505fd71a6803fa1077000001
Top: UILabel with opaque background and clear text
Clear text is rendered in drawRect: func through complicated masking process
Middle: Worker View that is performing a repeating animation moving an image behind the top label
Bottom: a UIView that you add the middle and top subview to in that order. Can be whatever color you want the text to be
An example can be seen here https://github.com/jhurray/AnimatedLabelExample
I am trying to round the bottom two corners of a UIView, and have the layer’s border show up rounded as well. I am currently doing:
UIRectCorners corners = UIRectCornerBottomLeft | UIRectCornerBottomRight;
CGSize radii = CGSizeMake(kThisViewCornerRadius, kThisViewCornerRadius);
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:myView.bounds
byRoundingCorners:corners
cornerRadii:radii];
CAShapeLayer *maskLayer = [CAShapeLayer layer];
[maskLayer setPath:path.CGPath];
myView.layer.mask = maskLayer;
This works fine for normal views. However, myView’s layer has its borderColor and borderWidth set, and as you can see from the screenshot, the layer’s border is not getting rounded:
I also tried subclassing UIView and returning [CAShapeLayer layer] from +[Class layerClass], and setting up the view’s layer as a shape layer, but the border ended up beneath the view’s subviews.
Is it possible to round some of a view’s corners, round the view’s layer’s border, and clip the subviews underneath the layer’s border?
Note that this is not about how to round some corners and not others, but rather how to get the stroke to behave correctly.
I figured out a new way of thinking about it, thanks to David Rönnqvist’s comment.
I was trying to do the corner rounding and the stroke all in one layer. Instead, I broke it up into two layers: one to mask the view’s layer to round the corners, and the other in a view to add the stroke.
UIView *containerView = [[UIView alloc] initWithFrame:someFrame];
UIRectCorners corners = UIRectCornerBottomLeft | UIRectCornerBottomRight;
CGSize radii = CGSizeMake(kThisViewCornerRadius, kThisViewCornerRadius);
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:myView.bounds
byRoundingCorners:corners
cornerRadii:radii];
// Mask the container view’s layer to round the corners.
CAShapeLayer *cornerMaskLayer = [CAShapeLayer layer];
[cornerMaskLayer setPath:path.CGPath];
containerView.layer.mask = cornerMaskLayer;
// Make a transparent, stroked layer which will dispay the stroke.
CAShapeLayer *strokeLayer = [CAShapeLayer layer];
strokeLayer.path = path.CGPath;
strokeLayer.fillColor = [UIColor clearColor].CGColor;
strokeLayer.strokeColor = [UIColor redColor].CGColor;
strokeLayer.lineWidth = 2; // the stroke splits the width evenly inside and outside,
// but the outside part will be clipped by the containerView’s mask.
// Transparent view that will contain the stroke layer
UIView *strokeView = [[UIView alloc] initWithFrame:containerView.bounds];
strokeView.userInteractionEnabled = NO; // in case your container view contains controls
[strokeView.layer addSublayer:strokeLayer];
// configure and add any subviews to the container view
// stroke view goes in last, above all the subviews
[containerView addSubview:strokeView];
Zev Eisenberg's answer is the right one.
Although I prefer:
[self.layer addSublayer:strokeLayer];
instead of creating and adding a subview:
CGSize radii = CGSizeMake(radius, radius);
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:self.bounds
byRoundingCorners:corners
cornerRadii:radii];
// Mask the container view’s layer to round the corners.
CAShapeLayer *cornerMaskLayer = [CAShapeLayer layer];
[cornerMaskLayer setPath:path.CGPath];
self.layer.mask = cornerMaskLayer;
// Make a transparent, stroked layer which will dispay the stroke.
CAShapeLayer *strokeLayer = [CAShapeLayer layer];
strokeLayer.path = path.CGPath;
strokeLayer.fillColor = [UIColor clearColor].CGColor;
strokeLayer.strokeColor = color.CGColor;
strokeLayer.lineWidth = 2; // the stroke splits the width evenly inside and outside,
// but the outside part will be clipped by the containerView’s mask.
[self.layer addSublayer:strokeLayer];
Here is the small code. Alloc init a view and send to this method to get corners rounded. You can optionally round any of the corners u want. Also give shadow stroke color.
-(void) setMaskTo:(UIView*)view byRoundingCorners:(UIRectCorner)corners withColor: (UIColor*) color
{
UIBezierPath* rounded = [UIBezierPath bezierPathWithRoundedRect:view.bounds byRoundingCorners:corners cornerRadii:CGSizeMake(9.0, 9.0)];
CAShapeLayer* shape = [[[CAShapeLayer alloc] init] autorelease];
[shape setPath:rounded.CGPath];
shape.strokeColor = [[UIColor grayColor] CGColor];
view.backgroundColor=color;
view.layer.mask = shape;
}
Call the method like this.
[self setMaskTo:ABCView byRoundingCorners:UIRectCornerAllCorners withColor:[UIColor greenColor]];
I'm trying to track down a bug in some iOS code for an iPad app. In one of our views, we've added sublayers to have a shadow and make sure the bottom of the view has rounded edges. Here's the code where we add the sublayers:
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.bounds
byRoundingCorners:(UIRectCornerBottomLeft|UIRectCornerBottomRight)
cornerRadii:CGSizeMake(12.0f, 12.0f)];
// Create the shadow layer
shadowLayer = [CAShapeLayer layer];
[shadowLayer setFrame:self.bounds];
[shadowLayer setMasksToBounds:NO];
[shadowLayer setShadowPath:maskPath.CGPath];
shadowLayer.shadowColor = [UIColor blackColor].CGColor;
shadowLayer.shadowOffset = CGSizeMake(0.0f, 0.0f);
shadowLayer.shadowOpacity = 0.5f;
shadowLayer.shadowRadius = 6.0f;
roundedLayer = [CALayer layer];
[roundedLayer setFrame:self.bounds];
[roundedLayer setBackgroundColor:[UIColor colorFromHex:#"#e4ecef"].CGColor];
[self.layer insertSublayer:shadowLayer atIndex:0];
// Add inner view (since we're rounding corners, parent view can't mask to bounds b/c of shadow - need extra view)
maskLayer = [CAShapeLayer layer];
maskLayer.frame = self.bounds;
maskLayer.path = maskPath.CGPath;
innerView = [[UIView alloc] initWithFrame:self.bounds];
innerView.backgroundColor = [UIColor whiteColor];
innerView.layer.mask = maskLayer;
[self addSubview:innerView];
It shows up fine on the screen of the iPad, but I want to take a screenshot programmatically. I've added a category to UIView with this method:
- (UIImage*)screenshot {
UIGraphicsBeginImageContext(self.frame.size);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageWriteToSavedPhotosAlbum(viewImage, nil, nil, nil);
return viewImage;
}
When I look at the screenshot that is taken, it no longer has the rounded corners or the shadow behind my view. Why aren't they showing up?
Found an explanation here: CALayer renderInContext
Additionally, layers that use 3D transforms are not rendered, nor are
layers that specify backgroundFilters, filters, compositingFilter, or
a mask values.
It looks like some sublayers can't be handled by renderInContext, which is why they aren't showing up in my screenshots.
Gradient sublayers
The snapshot is not taking any gradient layers with alpha channel in it.
Remove the alpha from colors, works for me
I have a collection of CALayers. Each layer is a sublayer of the same parent CALayer, and each has a shadow applied to it. The layers are positioned dynamically, and there are many of them, so I can't predict how they'll be arranged ahead of time.
If the layers are adjacent to each other (close enough that they are almost touching) the shadow of one of the CALayers is rendered on top of the other CALayer. That's probably the desired effect in most cases, but I want my layers to exist in the same z-plane. (An example of this is the way CSS3 shadows are applied to block elements in web design.)
Is this possible? How can I achieve this?
(I had this idea: Adding a 'shadow' sublayer to each CALayer with my own shadow image, and setting the z-position to a lower value. But doesn't the layer-tree make this impossible? Z-positions in one layer's coordinate system are independent from z-positions in another layer's coordinate system, right?)
If all of the shadowed layers have the same shadow settings, put them into a container layer and set the shadow on the container layer. Example:
- (void)viewDidLoad
{
[super viewDidLoad];
CALayer *containerLayer = [CALayer layer];
containerLayer.frame = self.view.bounds;
containerLayer.shadowRadius = 10;
containerLayer.shadowOpacity = 1;
[self.view.layer addSublayer:containerLayer];
CAShapeLayer *layer1 = [CAShapeLayer layer];
layer1.bounds = CGRectMake(0, 0, 200, 200);
layer1.position = CGPointMake(130, 130);
layer1.path = [UIBezierPath bezierPathWithOvalInRect:layer1.bounds].CGPath;
layer1.fillColor = [UIColor redColor].CGColor;
[containerLayer addSublayer:layer1];
CAShapeLayer *layer2 = [CAShapeLayer layer];
layer2.bounds = CGRectMake(0, 0, 200, 200);
layer2.position = CGPointMake(170, 200);
layer2.path = [UIBezierPath bezierPathWithOvalInRect:layer2.bounds].CGPath;
layer2.fillColor = [UIColor blueColor].CGColor;
[containerLayer addSublayer:layer2];
}
Output: