Applying a Gradient to CAShapeLayer
Solution 1
You could use the path of your shape to create a masking layer and apply that on the gradient layer, like this:
UIView *v = [[UIView alloc] initWithFrame:self.window.frame];
CAShapeLayer *gradientMask = [CAShapeLayer layer];
gradientMask.fillColor = [[UIColor clearColor] CGColor];
gradientMask.strokeColor = [[UIColor blackColor] CGColor];
gradientMask.lineWidth = 4;
gradientMask.frame = CGRectMake(0, 0, v.bounds.size.width, v.bounds.size.height);
CGMutablePathRef t = CGPathCreateMutable();
CGPathMoveToPoint(t, NULL, 0, 0);
CGPathAddLineToPoint(t, NULL, v.bounds.size.width, v.bounds.size.height);
gradientMask.path = t;
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.startPoint = CGPointMake(0.5,1.0);
gradientLayer.endPoint = CGPointMake(0.5,0.0);
gradientLayer.frame = CGRectMake(0, 0, v.bounds.size.width, v.bounds.size.height);
NSMutableArray *colors = [NSMutableArray array];
for (int i = 0; i < 10; i++) {
[colors addObject:(id)[[UIColor colorWithHue:(0.1 * i) saturation:1 brightness:.8 alpha:1] CGColor]];
}
gradientLayer.colors = colors;
[gradientLayer setMask:gradientMask];
[v.layer addSublayer:gradientLayer];
If you want to also use the shadows, you would have to place a "duplicate" of the shape layer under the gradient layer, recycling the same path reference.
Solution 2
Many thanks to @Palimondo for a great answer!
In case someone is looking for Swift 4 + filling animation code of this solution:
let myView = UIView(frame: .init(x: 0, y: 0, width: 200, height: 150))
view.addSubview(myView)
myView.center = view.center
// Start and finish point
let startPoint = CGPoint(x: myView.bounds.minX, y: myView.bounds.midY)
let finishPoint = CGPoint(x: myView.bounds.maxX, y: myView.bounds.midY)
// Path
let path = UIBezierPath()
path.move(to: startPoint)
path.addLine(to: finishPoint)
// Gradient Mask
let gradientMask = CAShapeLayer()
let lineHeight = myView.frame.height
gradientMask.fillColor = UIColor.clear.cgColor
gradientMask.strokeColor = UIColor.black.cgColor
gradientMask.lineWidth = lineHeight
gradientMask.frame = myView.bounds
gradientMask.path = path.cgPath
// Gradient Layer
let gradientLayer = CAGradientLayer()
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
// make sure to use .cgColor
gradientLayer.colors = [UIColor.red.cgColor, UIColor.green.cgColor]
gradientLayer.frame = myView.bounds
gradientLayer.mask = gradientMask
myView.layer.addSublayer(gradientLayer)
// Corner radius
myView.layer.cornerRadius = 10
myView.clipsToBounds = true
Extra. In case you also need a "filling animation", add this lines:
// Filling animation
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.duration = 10
gradientMask.add(animation, forKey: "LineAnimation")
Solution 3
This is a great solution, but you might encounter unexpected problems if you're creating a category on CAShapeLayer where you don't immediately have the view.
See Setting correct frame of a newly created CAShapeLayer
Bottom line, get the bounds of the path then set the gradient mask's frame using the path bounds and translate as necessary. Good thing here is that by using the path's bounds rather than any other frame, the gradient will only fit within the path bounds (assuming that's what you want).
// Category on CAShapeLayer
CGRect pathBounds = CGPathGetBoundingBox(self.path);
CAShapeLayer *gradientMask = [CAShapeLayer layer];
gradientMask.fillColor = [[UIColor blackColor] CGColor];
gradientMask.frame = CGRectMake(0, 0, pathBounds.size.width, pathBounds.size.height);
gradientMask.path = self.path;
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.startPoint = CGPointMake(0.5,1.0);
gradientLayer.endPoint = CGPointMake(0.5,0.0);
gradientLayer.frame = CGRectMake(0, 0, pathBounds.size.width, pathBounds.size.height);
NSMutableArray *colors = [NSMutableArray array];
for (int i = 0; i < 10; i++) {
[colors addObject:(id)[[UIColor colorWithHue:(0.1 * i) saturation:1 brightness:.8 alpha:1] CGColor]];
}
gradientLayer.colors = colors;
[gradientLayer setMask:gradientMask];
[self addSublayer:gradientLayer];
Admin
Updated on July 09, 2020Comments
-
Admin almost 4 years
Does anyone have any experience in applying a Gradient to a CAShapeLayer? CAShapeLayer is a fantastic layer class, but it appears to only support solid fill coloring, whereas I'd like it to have a gradient fill (actually an animatable gradient at that).
Everything else to do with CAShapeLayer (shadows, shapes, stroke color, animatable shape path) is fantastic.
I've tried placing a CAGradientLayer inside a CAShapeLayer, or indeed setting the CAShapeLayer as the mask of the GradientLayer and adding both to a container layer, but these don't have the right outcome.
Should I subclass CAShapeLayer, or is there a better way forward?
Thanks.
-
Jonathan Dumaine over 10 yearsThis is an amazing answer. For those not aware of this (like I was), I'd like to add that you can add the mask, gradient, shadow and any other layers you may need to a Container layer (
CALayer container = [CALayer layer]
) and then only have to manage that container layer if you need to animate its position. -
思齐省身躬行 over 6 yearsGreat answer! Thank you!