How to draw radial gradients in a CALayer?
Asked Answered
S

4

29

I know CAGradientLayer doesn't support radial gradient at the moment and only has option of kCAGradientLayerAxial.

I want something like below:

enter image description here

I have looked around for this issue and found that there is a way around this. But the explanations were not clear to me. So I want to know if I can draw radial gradients using CAGradientLayer and if so, then how to do it?

Seagrave answered 13/11, 2014 at 11:1 Comment(3)
Why can't you use CGContextDrawRadialGradient? You can always render to an image, then assign it to the contents of a CALayer. Using CALayer's renderInContext: is also an option, but it all leads to CGContextDrawRadialGradient.Reserpine
@Reserpine I have multiple layers added as a sublayer to my UIView and I am drawing bezierPath on these layers. So all the the properties like solid fill, stroke width etc are associated with the layer. So just want to maintain the consistency in code.Seagrave
@Reserpine Also I didn't get your concept of rendering to an image and then assigning it to the contents of a CALayer. Can you please elaborate a bit more.Seagrave
R
49

From what I understood, you just need a layer that draws a gradient, and CGContextDrawRadialGradient works perfectly for that need. And to reiterate on what you said, CAGradientLayer doesn't support radial gradients, and nothing we can do about that, except unnecessary swizzling that can be done cleanly with a CALayer subclass.

(note: the gradient drawing code was taken from here. It isn't what this answer is about.)


viewDidLoad:

GradientLayer *gradientLayer = [[GradientLayer alloc] init];
gradientLayer.frame = self.view.bounds;

[self.view.layer addSublayer:gradientLayer];

CALayer subclass:

- (instancetype)init
{
    self = [super init];
    if (self) {
        [self setNeedsDisplay];
    }
    return self;
}

- (void)drawInContext:(CGContextRef)ctx
{

    size_t gradLocationsNum = 2;
    CGFloat gradLocations[2] = {0.0f, 1.0f};
    CGFloat gradColors[8] = {0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.5f};
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, gradColors, gradLocations, gradLocationsNum);
    CGColorSpaceRelease(colorSpace);

    CGPoint gradCenter= CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
    CGFloat gradRadius = MIN(self.bounds.size.width , self.bounds.size.height) ;

    CGContextDrawRadialGradient (ctx, gradient, gradCenter, 0, gradCenter, gradRadius, kCGGradientDrawsAfterEndLocation);


    CGGradientRelease(gradient);
}

enter image description here

Reserpine answered 14/11, 2014 at 7:16 Comment(10)
how can this be achieved in SWIFT ?Propjet
@Propjet the above code is directly applicable to swift. It would just be like override func drawInContext(ctx: CGContext!) { …Ligialignaloes
thank you @AndyPoes I'm having trouble with this line >> "CGContextDrawRadialGradient(ctx, gradient, gradCenter, 0.0, gradCenter, gradRadius, kCGGradientDrawsAfterEndLocation)" and the the instance methodPropjet
@Propjet Not sure the specific trouble you're running into, but maybe this will help? gist.github.com/MrBendel/f10d46a3160ffdf28989Ligialignaloes
@AndyPoes fantastic, many thanks :). Is the init method also needed in swift ? and if so is there significant change to it, thanks.Propjet
I tried this but the gradient is showed above all the subview (UIButton, UILabels, etc), so their colors are "contaminated" by the gradient. Any clue to fix this?Peery
@WedgeSparda That's not possible, unless those controls are non-opaque. Just make sure the CALayer is at the bottom of the subLayers.Reserpine
How do we change the colors that it uses?Prunella
Radius should probably be max of half the width or height, i.e. MAX(width / 2, height / 2)Cordwainer
Is there any advantage of this method over the method mentioned by @rpstw? https://mcmap.net/q/197366/-how-to-draw-radial-gradients-in-a-calayer Setting CAGradientLayer.type to .radial seems to work fine.Warms
H
21

Here is the Swift3 code I use :

import UIKit

class RadialGradientLayer: CALayer {

    required override init() {
        super.init()
        needsDisplayOnBoundsChange = true
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    required override init(layer: Any) {
        super.init(layer: layer)
    }

    public var colors = [UIColor.red.cgColor, UIColor.blue.cgColor]

    override func draw(in ctx: CGContext) {
        ctx.saveGState()

        let colorSpace = CGColorSpaceCreateDeviceRGB()
        var locations = [CGFloat]()
        for i in 0...colors.count-1 {
            locations.append(CGFloat(i) / CGFloat(colors.count))
        }
        let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: locations)
        let center = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
        let radius = min(bounds.width / 2.0, bounds.height / 2.0)
        ctx.drawRadialGradient(gradient!, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: radius, options: CGGradientDrawingOptions(rawValue: 0))
    }
}
Howland answered 31/10, 2016 at 15:23 Comment(3)
Yep this worked for me. A couple notes: - If you're coding in a mix and match project (Obj-C + Swift), defining an array of CGColors will throw an error since Objective-C doesn't allow you to create an array of primitive data types. To overcome this you can create an array of UIColors, and convert them to their cgColor values in the draw function. - The circle this drew had jagged edges when added to a smaller subview. I fixed this by setting the layer's masksToBounds = true, then added added a corner radius to make the view containing the layer a circle, making the edge smooth.Hyperkeratosis
this worked very well for me too - only things I changed were to use max instead of min for radius and use .drawsAfterEndLocation instead of CGGradientDrawingOptions(rawValue: 0) as that ended up in a more pleasing result (imho obvs)Jellify
You can add your created Radial CALayer like this: view.layer.insertSublayer(gradientLayer, below: view.layer).Overburdensome
R
20

I don't know when but now you can change CAGradientLayer 's type to kCAGradientLayerRadialand it works.

Theoretically the performance of the Core Animation way is better than Core Graphics.

Some example code:

class View : UIView {

    let gradientLayer = CAGradientLayer()
    override init(frame: CGRect) {
        super.init(frame: frame)

        gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
        gradientLayer.endPoint = CGPoint(x: 1, y: 1)
        gradientLayer.locations = [
            NSNumber(value: 0.6),
            NSNumber(value: 0.8)
        ]
        gradientLayer.type = .radial
        gradientLayer.colors = [
            UIColor.green.cgColor,
            UIColor.purple.cgColor,
            UIColor.orange.cgColor,
            UIColor.red.cgColor
        ]
        self.layer.addSublayer(gradientLayer)
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func layoutSublayers(of layer: CALayer) {
        assert(layer === self.layer)
        gradientLayer.frame = layer.bounds
    }
}
Reparable answered 24/10, 2018 at 3:42 Comment(2)
Works perfectly. But what do locations (0.6 & 0.8) and start/end points mean?Key
Much better. Thanks for adding this. Wouldn't have even checked for it.Marder
P
4

Swift 5

import UIKit

final class RadialGradientLayer: CALayer {

    let centerColor: CGColor
    let edgeColor: CGColor

    init(centerColor: UIColor, edgeColor: UIColor) {
        self.centerColor = centerColor.cgColor
        self.edgeColor = edgeColor.cgColor
        super.init()
        needsDisplayOnBoundsChange = true
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func draw(in ctx: CGContext) {
        ctx.saveGState()
        let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(),
                                  colors: [centerColor, edgeColor] as CFArray,
                                  locations: [0.0, 1.0])

        let gradCenter = CGPoint(x: bounds.midX, y: bounds.midY)
        let gradRadius = min(bounds.width, bounds.height)

        ctx.drawRadialGradient(gradient!,
                               startCenter: gradCenter,
                               startRadius: 0.0,
                               endCenter: gradCenter,
                               endRadius: gradRadius,
                               options: .drawsAfterEndLocation)
    }
}
Porterfield answered 6/4, 2020 at 17:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.