I try to draw a gradient in a rectangle.
Following picture shows, that gradient in general works (blue), but it fills the entire screen.
I like to do the gradient only in a specific area like the green rectangle.
How can I do this ?
My code for the gradient experiment :-)
let context = UIGraphicsGetCurrentContext()
//let locations: [CGFloat] = [ 0.0, 0.25, 0.5, 0.75 ]
let locations: [CGFloat] = [ 0.0, 0.99 ]
let colors = [UIColor.blueColor().CGColor,
UIColor.whiteColor().CGColor]
let colorspace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradientCreateWithColors(colorspace,
colors, locations)
var startPoint = CGPoint()
var endPoint = CGPoint()
startPoint.x = 0.0
startPoint.y = 10.0
endPoint.x = self.frame.width-100;
endPoint.y = 10
//let rectangle_main = CGRectMake(CGFloat(15), CGFloat(0), CGFloat(1000), CGFloat(30));
//CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0)
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0)
You need to specify a clipping path right before drawing the gradient.
In your case, create a rectangle path and call CGContextClip(context)
CGContextAddRect(context, CGRect(...))
CGContextClip(context)
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0)
The gradient will be clipped to the rectangle.
Related
When trying to draw a radial gradient from white to clear, I obtain different results depending on whether I use UIColor(white:alpha:), UIColor.white.withAlpha.Components(alpha:) or UIColor.color. How Can I get the same gradient with a clear color as with a plain color?
I order to draw a radial gradient, I have overridden the draw(context:) method (see below). My code seems to work fine when using plain colours for a gradient but works "mysteriously" when using a clear color.
override func draw(in ctx: CGContext) {
super.draw(in: ctx)
ctx.saveGState()
let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colorFill as CFArray, locations: self.locations as! [CGFloat] )
let startPoint = CGPoint(x: startX, y: startY )
let endPoint = CGPoint(x: endX , y: endY)
let drawingOption : CGGradientDrawingOptions = radius > 0 ? [] : .drawsAfterEndLocation
//self.masksToBounds = true
if gradientType == .radial {
ctx.drawRadialGradient(gradient!, startCenter: startPoint, startRadius: 0.0, endCenter: endPoint, endRadius: self.frame.width / 2, options: drawingOption)
} else {
self.startPoint = startPoint
self.endPoint = endPoint
}
}
Here's the results I obtain depending on the input colours for the gradient:
[UIColor.white.cgColor,UIColor.black.cgColor]
(desired result with clear color instead of black)
[UIColor.white.cgColor, UIColor.clear.cgColor]
[UIColor.white.cgColor, UIColor.white.withAlphaComponent(0.0).cgColor]
[UIColor(white: 0.0, alpha: 1).cgColor, UIColor(white: 1.0, alpha: 0).cgColor]
Could someone can explain why I don't get the same output as with only plain colours (output I wish to obtain)?
Thanks a lot for taking the time to read and reply!
The layer is not drawn only in the draw(in context:) method. This line of code confirms some properties had been set prior to the call of the method, which has priorly triggered the drawing of the layer (self.locations):
CGColorSpaceCreateDeviceRGB(), colors: colorFill as CFArray, locations: self.locations as! [CGFloat] )
By removing the set of self.colors and self.locations properties in the custom initializer and storing the values in Type Properties instead before calling self.setNeedDisplay(), the layer is only being drawn in the draw(in context:) method.
class CustomLayer {
override init() {
super.init()
needsDisplayOnBoundsChange = true
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var colors = []()
var colorMap = [NSNumber]()
custom init() {
colors = [color,color]
colorMap = [0.0, 1.0]
self.setNeedDisplay()
}
override func draw(in ctx: CGContext) {
super.draw(in: ctx)
ctx.saveGState()
guard let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors as CFArray, locations: colorMap as! [CGFloat] ) else {return}
let startPoint = CGPoint(x: startX, y: startY )
let endPoint = CGPoint(x: endX , y: endY)
self.masksToBounds = true
if gradientType == .radial {
ctx.drawRadialGradient(gradient, startCenter: startPoint, startRadius: 0.0, endCenter: endPoint, endRadius: self.frame.width / 2, options: drawingOption)
} else {
self.locations = colorMap
self.colors = colors
self.startPoint = startPoint
self.endPoint = endPoint
}
}
}
Cheers!
I am working on a task in which I must create a circle with no fill, but a gradient stroke. For reference, here is the end result I am after;
Given other occurrences with the app, I am drawing my circle like so;
let c = UIGraphicsGetCurrentContext()!
c.saveGState()
let clipPath: CGPath = UIBezierPath(roundedRect: converted_rect, cornerRadius: converted_rect.width / 2).cgPath
c.addPath(clipPath)
c.setLineWidth(9.0)
c.setStrokeColor(UIColor.blue.cgColor)
c.closePath()
c.strokePath()
c.restoreGState()
let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
This results in a circle with a blue stroke. Despite many searches around SO, I'm struggling to figure out how I'd replace that setStrokeColor with a gradient, rather than a blue color. My most success came from creating a CAGradientLayer, then masking it with a CAShapeLayer created from the path, but I was only able to create a filled circle, not a hollow circle.
Thank you!
The basic idea is to use your path as a clipping path, then draw the gradient.
let c = UIGraphicsGetCurrentContext()!
let clipPath: CGPath = UIBezierPath(ovalIn: converted_rect).cgPath
c.saveGState()
c.setLineWidth(9.0)
c.addPath(clipPath)
c.replacePathWithStrokedPath()
c.clip()
// Draw gradient
let colors = [UIColor.blue.cgColor, UIColor.red.cgColor]
let offsets = [ CGFloat(0.0), CGFloat(1.0) ]
let grad = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors as CFArray, locations: offsets)
let start = converted_rect.origin
let end = CGPoint(x: converted_rect.maxX, y: converted_rect.maxY)
c.drawLinearGradient(grad!, start: start, end: end, options: [])
c.restoreGState()
let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
Setup a CGGradient first with the desired colors. Then for a linear gradient you use drawLinearGradient. For a radial gradient, use drawRadialGradient.
I have done this circle gradient layer:
What I would like to have is only one gradient (the one on the left) while the gradient on the bottom would be removed to show a clear separation between red and yellow.
As I will need to make an animation out of it (like a loading view), I thought about having an image in the background and having a circle shape layer with a color (like white) on top and change the stroke of this layer as I need.
Another solution I thought about was having tow circle shape layers, one with the gradient, the other one without.
But both those solutions feels more like a hack and I was wondering if there was a proper one using just
Here is the code I used:
fileprivate func createProgressLayer() {
let startAngle = CGFloat(M_PI_2)
let endAngle = CGFloat(M_PI * 2 + M_PI_2)
let centerPoint = CGPoint(x: frame.width / 2 , y: frame.height / 2)
progressLayer.path = UIBezierPath(arcCenter:centerPoint, radius: frame.width / 2 - 30.0, startAngle: startAngle, endAngle: endAngle, clockwise: true).cgPath
progressLayer.backgroundColor = UIColor.clear.cgColor
progressLayer.fillColor = nil
progressLayer.strokeColor = UIColor.black.cgColor
progressLayer.lineWidth = 4.0
progressLayer.strokeStart = 0.0
progressLayer.strokeEnd = 1.0
let gradientMaskLayer = gradientMask()
gradientMaskLayer.mask = progressLayer
layer.addSublayer(gradientMaskLayer)
}
fileprivate func gradientMask() -> CAGradientLayer {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.locations = [0.0, 0.1]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 0)
let arrayOfColors: [AnyObject] = [UIColor.red.cgColor, UIColor.yellow.cgColor]
gradientLayer.colors = arrayOfColors
return gradientLayer
}
Shape layers only support a single color.
In order to get the effect that you're after I think you're going to need to use a shape layer as a mask on your gradient like you're doing.
Having a second white shape layer on top of the gradient+mask layer and changing strokeStart and/or strokeEnd also sounds like a reasonable way to do what you're trying to do.
Doing tricks ("hacks" as you call them) with layers is quite common and a reasonable way to do what you're trying to do.
I have UIView for drawing.
Can i change stroke line from default
CGContextSetStrokeColorWithColor(context, UIColor.blackColor().CGColor)
to https://www.img.in.th/image/79xF
or http://catheria.com/wp-content/uploads/2015/10/StrokeLinear.png
below is code draw radius gradient
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
let ctx0 = UIGraphicsGetCurrentContext()
let colorSpace: CGColorSpaceRef = CGColorSpaceCreateDeviceRGB()!
let locations: [CGFloat] = [0.0, 0.4, 1.0]
let arrColor = [UIColor.blackColor().CGColor,UIColor.blackColor().CGColor, UIColor.clearColor().CGColor]
let _gradient: CGGradientRef = CGGradientCreateWithColors(colorSpace, arrColor, locations)!
CGContextDrawRadialGradient(ctx0, _gradient, CGPoint(x: size.width/2, y: size.height/2), CGFloat(0), CGPoint(x: size.width/2, y: size.height/2), CGFloat(60), CGGradientDrawingOptions())
let _img0 = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
I can't manage to display 2 rectangles with gradients on the same view. My following code only shows the first rectangle. If I omit the rectangle1 in the code the rectangle2 is displayed. Only in combination it will only show rectangle1.
I like to display the blue rectangle1 ...
... and the red rectangle2 with different gradient ...
... at the same time.
I have the following code for this:
func draw_2_gradient_rectangles(){
let locations: [CGFloat] = [ 0.0, 1.0 ]
let colorspace = CGColorSpaceCreateDeviceRGB()
// first rectangle
let context = UIGraphicsGetCurrentContext()
let colors = [UIColor.blueColor().CGColor,
UIColor.whiteColor().CGColor]
let gradient = CGGradientCreateWithColors(colorspace,
colors, locations)
var startPoint1 = CGPoint()
var endPoint1 = CGPoint()
startPoint1.x = 0.0
startPoint1.y = 10.0
endPoint1.x = 100;
endPoint1.y = 10
let rectangle_main1 = CGRectMake(CGFloat(15), CGFloat(0), CGFloat(100), CGFloat(30));
CGContextAddRect(context, rectangle_main1);
CGContextClip(context)
CGContextDrawLinearGradient(context, gradient, startPoint1, endPoint1, 0)
// second rectangle
let context2 = UIGraphicsGetCurrentContext()
let colors2 = [UIColor.redColor().CGColor,
UIColor.whiteColor().CGColor]
let gradient2 = CGGradientCreateWithColors(colorspace,
colors2, locations)
var startPoint2 = CGPoint()
var endPoint2 = CGPoint()
startPoint2.x = 100;
startPoint2.y = 10.0;
endPoint2.x = 10.0;
endPoint2.y = 10.9;
let rectangle_main2 = CGRectMake(CGFloat(15), CGFloat(50), CGFloat(100), CGFloat(30));
CGContextAddRect(context2, rectangle_main2);
CGContextClip(context2)
CGContextDrawLinearGradient(context2, gradient2, startPoint2, endPoint2, 0);
}
What am I doing wrong ? Any help ?
UIGraphicsGetCurrentContext() does not create a context but just gives you a reference to the current one.
This means context is the same as context2. And in context you clipped the drawing area so the next CGContextAddRect will draw outside the clipping area.
What you need to do is save the drawing state before each rectangle creation code with :
CGContextSaveGState(context);
and restore it before doing the second rectangle code with
CGContextRestoreGState(context);
This will make sure the clipping area is reset before drawing the second rectangle. e.g:
CGContextSaveGState(context);
// Create rectangle1
CGContextRestoreGState(context);
CGContextSaveGState(context);
// Create rectangle2
CGContextRestoreGState(context);