28

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?

Mazyod
  • 22,319
  • 10
  • 92
  • 157
blancos
  • 1,576
  • 2
  • 16
  • 38
  • 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`. – Mazyod Nov 13 '14 at 14:15
  • @Mazyod 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. – blancos Nov 14 '14 at 06:31
  • @Mazyod 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. – blancos Nov 14 '14 at 06:33

4 Answers4

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

julian.a
  • 1,163
  • 2
  • 9
  • 21
Mazyod
  • 22,319
  • 10
  • 92
  • 157
  • how can this be achieved in SWIFT ? – DrPatience Jul 01 '15 at 14:28
  • 1
    @DrPatience the above code is directly applicable to swift. It would just be like ```override func drawInContext(ctx: CGContext!) { …``` – Andy Poes Jul 07 '15 at 20:59
  • thank you @AndyPoes I'm having trouble with this line >> "CGContextDrawRadialGradient(ctx, gradient, gradCenter, 0.0, gradCenter, gradRadius, kCGGradientDrawsAfterEndLocation)" and the the instance method – DrPatience Jul 08 '15 at 08:22
  • 1
    @DrPatience Not sure the specific trouble you're running into, but maybe this will help? https://gist.github.com/MrBendel/f10d46a3160ffdf28989 – Andy Poes Jul 08 '15 at 15:35
  • @AndyPoes fantastic, many thanks :). Is the init method also needed in swift ? and if so is there significant change to it, thanks. – DrPatience Jul 09 '15 at 10:31
  • 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? – WedgeSparda Oct 08 '15 at 14:20
  • @WedgeSparda That's not possible, unless those controls are non-opaque. Just make sure the `CALayer` is at the bottom of the `subLayers`. – Mazyod Oct 08 '15 at 16:47
  • How do we change the colors that it uses? – TheJeff Dec 08 '16 at 18:04
  • Radius should probably be max of half the width or height, i.e. MAX(width / 2, height / 2) – mojuba Sep 28 '18 at 12:35
  • Is there any advantage of this method over the method mentioned by @rpstw? https://stackoverflow.com/a/52960769/89590 Setting `CAGradientLayer.type` to `.radial` seems to work fine. – Nate Cook Feb 28 '19 at 19:44
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))
    }
}
Alexis C.
  • 4,898
  • 1
  • 31
  • 43
  • 7
    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. – Iron John Bonney Dec 06 '16 at 02:24
  • 1
    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) – atomoil Jun 01 '18 at 16:49
  • You can add your created Radial `CALayer` like this: `view.layer.insertSublayer(gradientLayer, below: view.layer)`. – Chintan Shah Mar 11 '19 at 13:42
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
    }
}
rpstw
  • 1,582
  • 14
  • 16
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)
    }
}
Hayk Brsoyan
  • 186
  • 5