Applying a Gradient to CAShapeLayer
Asked Answered
G

3

38

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.

Glimmering answered 19/1, 2011 at 10:8 Comment(2)
I believe this question contains the answer by Matt Long.Kapor
For anyone googling to this excellent older question, here's the full, detailed, explanation of exactly how this works: https://mcmap.net/q/410626/-mask-uiview-with-uibezierpath-stroke-onlyChauvin
K
66

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.

Kapor answered 31/8, 2011 at 23:52 Comment(2)
This 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.Gavrielle
Great answer! Thank you!Steerage
R
13

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")
Regurgitation answered 8/7, 2018 at 14:8 Comment(0)
P
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];
Phoenix answered 3/2, 2014 at 11:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.