How to draw radial gradients in a CALayer?
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);
}
Radial gradient background in Swift
Have a look at my implementation of RadialGradientLayer, and feel free to modify it
class RadialGradientLayer: CALayer {
override init(){
super.init()
needsDisplayOnBoundsChange = true
}
init(center:CGPoint,radius:CGFloat,colors:[CGColor]){
self.center = center
self.radius = radius
self.colors = colors
super.init()
}
required init(coder aDecoder: NSCoder) {
super.init()
}
var center:CGPoint = CGPointMake(50,50)
var radius:CGFloat = 20
var colors:[CGColor] = [UIColor(red: 251/255, green: 237/255, blue: 33/255, alpha: 1.0).CGColor , UIColor(red: 251/255, green: 179/255, blue: 108/255, alpha: 1.0).CGColor]
override func drawInContext(ctx: CGContext!) {
CGContextSaveGState(ctx)
var colorSpace = CGColorSpaceCreateDeviceRGB()
var locations:[CGFloat] = [0.0, 1.0]
var gradient = CGGradientCreateWithColors(colorSpace, colors, [0.0,1.0])
var startPoint = CGPointMake(0, self.bounds.height)
var endPoint = CGPointMake(self.bounds.width, self.bounds.height)
CGContextDrawRadialGradient(ctx, gradient, center, 0.0, center, radius, 0)
}
}
In my case I needed it with two colors only and if you need more colors you need to modify location
array declared in drawInContext
. Also after creating object from this class don't forget to call its setNeedsDisplay()
otherwise it wont work. Also sometimes I needed different size gradients so thats why you have to pass radius parameter in initializer and the center point of your gradient
Draw radial background on a UIBezierPath
Here's how you can make your original code work with minimal changes.
override func draw(_ rect: CGRect) {
// [...]
// your code untouched until this line:
bezierPath.close()
// change the rest of the function to:
bezierPath.fill()
UIGraphicsGetCurrentContext()!.setBlendMode(.sourceIn)
let colors = [UIColor.green.cgColor, UIColor.blue.cgColor] as CFArray
let gradient = CGGradient(colorsSpace: nil, colors: colors, locations: nil)
let endPosition = min(frame.width, frame.height) / 2
let center = CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2)
UIGraphicsGetCurrentContext()?.drawRadialGradient(gradient!, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: endPosition, options: .drawsAfterEndLocation)
}
Result:
The idea is to first draw the bezier with the rays. The color does not matter here, this is only to draw alpha.
Then we draw the gradient on top of the alpha using one of Quartz' peculiar blend modes: kCGBlendModeSourceIn
(see Porter-Duff alpha compositing).
This mode draws anything on top of existing alpha, replacing only the pixel's color, leaving alpha as it was. It's basically like using the current drawing's alpha as a mask.
How to cut out a hole with radial gradient in view
You can play around a little bit and get
To achieve such result I prepared sample code in playground - just copy-paste it and try.
import UIKit
import PlaygroundSupport
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)
}
//default colors
public var colors = [UIColor.red.cgColor, UIColor.clear.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, bounds.height)
ctx.drawRadialGradient(gradient!, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: radius, options: CGGradientDrawingOptions(rawValue: 0))
}
}
let view = UIView(frame: CGRect(x: 0, y: 0, width: 375, height: 300))
view.backgroundColor = UIColor.green
let label = UILabel(frame: view.bounds)
label.text = "test"
label.font = UIFont.systemFont(ofSize: 30)
label.textAlignment = .center
view.addSubview(label)
let gradientLayer = RadialGradientLayer()
gradientLayer.frame = view.bounds
gradientLayer.colors = [UIColor.clear.cgColor, UIColor.black.cgColor]
gradientLayer.setNeedsDisplay()
view.layer.addSublayer(gradientLayer)
view
How do I draw an angle gradient using CALayer or CoreGraphics functions?
Unfortunately, Core Graphics and Quartz only support axial (linear) and radial (circular) gradients. For an angle gradient you’ll either have to draw it manually (perhaps by drawing triangles in a circle at a resolution fine enough for the color shifts to be unnoticeable) or have a precomposed angle-gradient image that you resize and clip to your drawing needs.
Draw gradient layer in drawInRect
Using a gradient layer or drawing a gradient in draw rect is quite different. To draw the gradient in drawRect
you should try something like the following:
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
UIColor *startColor = [UIColor colorWithRed:1.0f green:.0f blue:.0f alpha:1.0f];
UIColor *endColor = [UIColor colorWithRed:.0f green:1.0f blue:.0f alpha:1.0f];
NSArray *colors = @[(__bridge id)startColor.CGColor, (__bridge id)endColor.CGColor];
CGGradientRef gradient = CGGradientCreateWithColors(CGColorSpaceCreateDeviceRGB(), (CFArrayRef)colors, nil);
CGContextDrawLinearGradient(UIGraphicsGetCurrentContext(), gradient, CGPointMake(0.0f, 0.0f), CGPointMake(self.bounds.size.width, self.bounds.size.height), kCGGradientDrawsBeforeStartLocation|kCGGradientDrawsAfterEndLocation);
}
Overriding drawRect
is on a bit lower level so you may not use most of layer calls inside it without creating a loop.
Related Topics
What #Defines Are Set Up by Xcode When Compiling for Iphone
Swift Stack and Heap Understanding
Enterprise App Deployment Doesn't Install on iOS 8.1.3
How to Remove the Xcode Warning Apple MACh-O Linker Warning 'Pointer Not Aligned at Address
Trouble with Autolayout on Uitableviewcell
How to Save Nsmutablearray in Nsuserdefaults
Linking Objective-C Categories in a Static Library
How to Hide Tab Bar with Animation in iOS
How to Set the Full Width of Separator in Uitableview
How to Dismiss Keyboard iOS Programmatically When Pressing Return
Simply Mask a Uiview with a Rectangle
How to Dismiss a Storyboard Popover
Uicollectionview Auto Scroll to Cell at Indexpath
Show iPhone Cut Copy Paste Menu on Uilabel