How to create a gradient perimeter using CALayer - ios

I've been trying for some time now, using CAGradientLayer to produce this .
Initially I tried having a gradient background using the .colors property, however this is only a background fill. Trying this approach, I tried to add another CALayer inside that had a black background, however i could never get the radius correct, and it would create a line of various thickness at the rounded corners.
Is there a better way to create this rounded rect border with a gradient to it? Will CALayer not achieve this and should I move over to UIBezierPath or CGContextRef?
Code for failed attempt:
UIView *view = [[UIView alloc]initWithFrame:CGRectMake(12*twelthWidth - squareCentre.x - squareSize.width, squareCentre.y, squareSize.width, squareSize.height)];
// Create the rounded layer, and mask it using the rounded mask layer
CAGradientLayer *roundedLayer = [CAGradientLayer layer];
[roundedLayer setFrame:view.bounds];
roundedLayer.cornerRadius = view.bounds.size.width/5;
roundedLayer.masksToBounds = YES;
roundedLayer.colors = [NSArray arrayWithObjects:(id)[[UIColor redColor] CGColor], (id)[[UIColor blueColor] CGColor], nil];
roundedLayer.borderWidth = 4;
roundedLayer.borderColor = [UIColor clearColor].CGColor;
// Add these two layers as sublayers to the view
int BorderWidth = 5;
CALayer *solidColour = [CALayer layer];
solidColour.cornerRadius = view.bounds.size.width/5;
solidColour.masksToBounds = YES;
solidColour.backgroundColor = [UIColor blackColor].CGColor;
[solidColour setFrame:CGRectMake(BorderWidth, BorderWidth, roundedLayer.bounds.size.width - 2*BorderWidth, roundedLayer.bounds.size.height - 2*BorderWidth)];
[view.layer insertSublayer:roundedLayer atIndex:0];
[view.layer insertSublayer:solidColour above:roundedLayer];
[self.view addSubview:view];
Which produces:
Whereby the corners aren't right. Could it be that I need to calculate a different radius for the second layer?
Edit
After setting the radius of the solid colour to solidColour.bounds.size.width/5, it still isn't right - It goes too thin at the corners.

The problem you are seeing is because the inner and outer corner radius are the same. That is what causes the line thickness to vary. This illustration from CSS-Tricks highlights the issue (even thought you aren't using CSS, the problem is still the same):
The solution is to calculate the inner radius as:
innerRadis = outerRadius - lineThickness
As shown in this illustration by Joshua Hibbert:

Instead of using the view bounds to set the cornerRadius of the solid layer, use the layer's frame value after you've set it to the correct inset value:
solidColour.masksToBounds = YES;
solidColour.backgroundColor = [UIColor blackColor].CGColor;
[solidColour setFrame:CGRectMake(BorderWidth, BorderWidth, roundedLayer.bounds.size.width - 2*BorderWidth, roundedLayer.bounds.size.height - 2*BorderWidth)];
solidColour.cornerRadius = solidColour.frame.size.width/5;
[view.layer insertSublayer:roundedLayer atIndex:0];
[view.layer insertSublayer:solidColour above:roundedLayer];
With this I get the following image

Just for your convenience if you use swift
let v = yourUIVIEW
let outerRadius:CGFloat = 60
let borderWidth:CGFloat = 8;
let roundedLayer = CAGradientLayer()
roundedLayer.frame = v.bounds
roundedLayer.cornerRadius = outerRadius
roundedLayer.masksToBounds = true
roundedLayer.endPoint = CGPoint(x: 1 , y: 0.5)
roundedLayer.colors = [ UIColor.redColor().CGColor, UIColor.blueColor()!.CGColor]
let innerRadius = outerRadius - borderWidth
//Solid layer
let solidLayer = CALayer()
solidLayer.masksToBounds = true
solidLayer.backgroundColor = UIColor.whiteColor().CGColor
solidLayer.cornerRadius = innerRadius
solidLayer.frame = CGRect(x: borderWidth, y: borderWidth, width: roundedLayer.bounds.width - 2*borderWidth, height:roundedLayer.bounds.height - 2*borderWidth )
v.layer.insertSublayer(roundedLayer, atIndex: 0)
v.layer.insertSublayer(solidLayer, above: roundedLayer)

Related

Objective c : strange behaviour when masking layer with another layer and changing frame

UIBezierPath * path = [UIBezierPath bezierPathWithArcCenter:CGPointZero radius:80 startAngle:0 endAngle:(2 * M_PI) clockwise:YES];
CAShapeLayer * layer = [CAShapeLayer new];
layer.strokeColor = [UIColor redColor].CGColor;
layer.fillColor = [UIColor clearColor].CGColor;
layer.lineWidth = 10;
layer.strokeEnd = 1;
layer.position = self.view.center;
layer.lineCap = kCALineCapRound;
[layer setPath : path.CGPath];
[self.view.layer addSublayer : layer];
CAGradientLayer * gradientLayer = [CAGradientLayer layer];
gradientLayer.startPoint = CGPointMake(0.0,0.5);
gradientLayer.endPoint = CGPointMake(1.0,0.5);
gradientLayer.frame = self.view.frame;
NSArray *colors = #[(id)[UIColor greenColor].CGColor,(id)[UIColor blueColor].CGColor,(id)[UIColor yellowColor].CGColor];
gradientLayer.colors = colors;
[gradientLayer setMask : layer];
[self.view.layer addSublayer : gradientLayer];
What Im doing:
Adding CAShapeLayer to view.layer with a path to draw a circle.
Adding CAGradientLayer ( 3 colors ) to view.layer and setingMask to the CAShapeLayer.
Result: with and without mask
The problem :
If I change the frame of the gradient to be on top of the circle ( because I want to see the all the colors on the circle ) its moves the circle as well.
particularly the change to the x and y of the gradient
So instead of
gradientLayer.frame = self.view.frame;
I change the x and y only ( width and hight doesn't causing this problem as far as I checked )
gradientLayer.frame = CGRectMake(100,100, self.view.frame.size.width, self.view.frame.size.height);
And this is the result
Can anyone explain why this is happening and what are the possible solutions ?
What Im trying to achieve is this
And then masking it with the CAShapeLayer but then this problem happens
Thanks in advance
change the shapelayer position, because gradient layer is fullscreen and shapelayer set center position and after you change the frame of gradient layer shapelayer also change with same position.
layer.position = CGPointMake(30, 250);

How to set gradient color with angle

I want to set gradient on my button, i have to set simple linear gradient(without angle) with two color, but i don't know how to set angle value on gradient
Angle:- 61
below image define psd gradient overlay effect
Thanks in advance
Try it, maybe it will help
- (CAGradientLayer *)gradientLayerWithColors:(NSArray *)colors angle:(CGFloat)angle {
CAGradientLayer *layer = [CAGradientLayer layer];
layer.colors = colors;
CGFloat x = angle / 360.f;
CGFloat a = pow(sin((2*M_PI*((x+0.75)/2))),2);
CGFloat b = pow(sin((2*M_PI*((x+0.0)/2))),2);
CGFloat c = pow(sin((2*M_PI*((x+0.25)/2))),2);
CGFloat d = pow(sin((2*M_PI*((x+0.5)/2))),2);
layer.startPoint = CGPointMake(a, b);
layer.endPoint = CGPointMake(c, d);
return layer;
}
CAGradientLayer *gradientMask = [CAGradientLayer layer];
gradientMask.frame = CGRectMake(0, 0, myView.bounds.size.width, myView.bounds.size.height);
gradientMask.colors = #[(id)[UIColor redColor].CGColor,
(id)[UIColor greenColor].CGColor];
gradientMask.locations = #[#0.0, #0.10, #0.60, #1.0];
[myView.layer insertSublayer:gradientMask atIndex:0];
adjust location value and frame size accordingly.
This is a slightly modified code generated by PaintCode that solves just this problem:
UIBezierPath* rectanglePath = [UIBezierPath bezierPathWithRect: self.view.bounds];
UIBezierPath* rectangleRotatedPath = [rectanglePath copy];
CGAffineTransform transform = CGAffineTransformMakeRotation(yourAngleInRadians);
[rectangleRotatedPath applyTransform: transform];
CGRect rectangleBounds = CGPathGetPathBoundingBox(rectangleRotatedPath.CGPath);
transform = CGAffineTransformInvert(transform);
CGPoint startPoint = CGPointApplyAffineTransform(CGPointMake(CGRectGetMinX(rectangleBounds), CGRectGetMidY(rectangleBounds)), transform);
CGPoint endPoint = CGPointApplyAffineTransform(CGPointMake(CGRectGetMaxX(rectangleBounds), CGRectGetMidY(rectangleBounds)), transform);
The code takes your initial rectangle (black rectangle) and angle (shown as black line), rotates the rectangle (you'll get blue rectangle), then finds bounding box of the rotated rectangle (red rectangle). Takes endpoints on the bounds (red dots) and transforms (rotates) them back using invert transform (blue dots).
Other solution might be finding intersection of your rectangle and a line defined by the angle.

Setting CALayer maskToBounds causing layer to disappear

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

making drop shadows on UIViews

I use this code in my customized UIView initialization:
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
...
UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRect:CGRectMake(frame.origin.x, frame.size.height, frame.size.width, 15)];
self.layer.masksToBounds = NO;
self.layer.shadowColor = [UIColor blackColor].CGColor;
self.layer.shadowOffset = CGSizeMake(0.0f, 4.0f);
self.layer.shadowOpacity = 0.5f;
self.layer.shadowPath = shadowPath.CGPath;
}
return self;
}
trying to make a drop shadow like this:
1)
But I got this effect:
2)
You can see this is an upside down version of what I want to achieve. How to make the shadow effect of the first image?
The issue is just with your shadowPath.
Using CGRectMake(frame.origin.x, frame.size.height, frame.size.width, 15) to create your UIBezierPath will set an incorrect origin.
First, origin.x should be 0.0f or the shadow will shift far away if your UIView's origin.x != 0.0f. Second, you need to line up the bottom of the shadowPath with the bottom of your UIView.
This is a screenshot of UIViews using your shadow code illustrating these issues. (In the lower UIView you cannot see the shadow because it is far off the right of the screen).
You will see what you intended if you change the rect to:
const CGFloat shadowRectHeight = 15.0f;
CGRect shadowRect = CGRectMake(0.0f, self.bounds.size.height - shadowRectHeight, self.bounds.size.width, shadowRectHeight)];
Do like this and set the UIView alpha
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = rect;
gradient.colors = [NSArray arrayWithObjects:(id)[UIColor blackColor].CGColor,
(id)[UIColor grayColor].CGColor,
(id)[UIColor blackColor].CGColor,nil];
[self.layer insertSublayer:gradient atIndex:0];
you can add or remove UIColor you want to display.

Masking a CALayer with another CALayer

I'm trying to make a donut shape with CALayers. One CALayer will be a large circle, the other one will be a smaller circle positioned in its center, masking it.
The large circle displays fine, but whenever I call circle.mask = circleMask; then the view appears empty.
Here's my code:
AriDonut.h
#import <UIKit/UIKit.h>
#interface AriDonut : UIView
-(id)initWithRadius:(float)radius;
#end
AriDonut.m
#import "AriDonut.h"
#import <QuartzCore/QuartzCore.h>
#implementation AriDonut
-(id)initWithRadius:(float)radius{
self = [super initWithFrame:CGRectMake(0, 0, radius, radius)];
if(self){
//LARGE CIRCLE
CALayer *circle = [CALayer layer];
circle.bounds = CGRectMake(0, 0, radius, radius);
circle.backgroundColor = [UIColor redColor].CGColor;
circle.cornerRadius = radius/2;
circle.position = CGPointMake(radius/2, radius/2);
//SMALL CIRLCE
CALayer *circleMask = [CALayer layer];
circleMask.bounds = CGRectMake(0, 0, 10, 10);
circleMask.cornerRadius = radius/2;
circleMask.position = circle.position;
//circle.mask = circleMask;
[self.layer addSublayer:circle];
}
return self;
}
I've tried setting the large circle's superlayer nil like this:
CALayer *theSuper = circle.superlayer;
theSuper = nil;
But it didin't make a difference.
I also tried setting Circle's masksToBounds property to YES and NO, but it didn't make a difference.
Any thoughts?
Indeed, as #David indicates the current (iOS 5.1) CALayer masks can't be reversed, which poses a problem if you want to use them to make a transparent hole a simple circular CALayer.
What you can do to get a donut is make a circular CALayer's backgroundColor transparent, but give it a borderColor and a wide borderWidth. Here's the dunkin' code:
CALayer *theDonut = [CALayer layer];
theDonut.bounds = CGRectMake(0,0, radius, radius);
theDonut.cornerRadius = radius/2;
theDonut.backgroundColor = [UIColor clearColor].CGColor;
theDonut.borderWidth = radius/5;
theDonut.borderColor = [UIColor orangeColor].CGColor;
[self.layer addSublayer:theDonut];
This is pretty easy using UIBezierPath and a CAShapeLayer as a masking layer. Code sample written as though it's in a UIView subclass.
Objective-C:
CGRect outerRect = self.bounds;
CGFloat inset = 0.2 * outerRect.size.width; // adjust as necessary for more or less meaty donuts
CGFloat innerDiameter = outerRect.size.width - 2.0 * inset;
CGRect innerRect = CGRectMake(inset, inset, innerDiameter, innerDiameter);
UIBezierPath *outerCircle = [UIBezierPath bezierPathWithRoundedRect:outerRect cornerRadius:outerRect.size.width * 0.5];
UIBezierPath *innerCircle = [UIBezierPath bezierPathWithRoundedRect:innerRect cornerRadius:innerRect.size.width * 0.5];
[outerCircle appendPath:innerCircle];
CAShapeLayer *maskLayer = [CAShapeLayer new];
maskLayer.fillRule = kCAFillRuleEvenOdd; // Going from the outside of the layer, each time a path is crossed, add one. Each time the count is odd, we are "inside" the path.
maskLayer.path = outerCircle.CGPath;
self.layer.mask = maskLayer;
Swift:
let outerRect = self.bounds
let inset: CGFloat = 0.2 * outerRect.width // adjust as necessary for more or less meaty donuts
let innerDiameter = outerRect.width - 2.0 * inset
let innerRect = CGRect(x: inset, y: inset, width: innerDiameter, height: innerDiameter)
let outerCircle = UIBezierPath(roundedRect: outerRect, cornerRadius: outerRect.width * 0.5)
let innerCircle = UIBezierPath(roundedRect: innerRect, cornerRadius: innerRect.width * 0.5)
outerCircle.appendPath(innerCircle)
let mask = CAShapeLayer()
mask.fillRule = kCAFillRuleEvenOdd
mask.path = outerCircle.CGPath
self.layer.mask = mask
It is the alpha value of the masking layers content that is used as a mask. (If you would add the mask as a sublayer instead of using it as a mask. Everything that is covered by the sublayer would be visible when used as a mask. Everything that is not covered by the sublayer would be hidden when used as a mask.)
Since your small circle is fully transparent , everything is masked away (is hidden). If you set the backgroundColor of it to any, fully opaque color (only the alpha value is used for the mask) then it will let those pixels through.
Note that this is the reverse of what you want. This will leave you with only "the hole of the donut" visible. There is no built in way to do a reverse mask Instead you would have to draw the content of the mask some other way like using a CAShapeLayer or using drawInContext:.
I succeeded with a CAShapeLayer masking a CALayer. To specify the shape of the masking CAShapeLayer I used UIBezierPath.
I posted the code in my answer to this question: How to Get the reverse path of a UIBezierPath. For the donut shape uncomment the commented line.

Resources