Crop Part of CALayer - ios

.
I want to crop out the Red triangle part from the CAshapelayer.
This is the code i used for drawing it:
Radial Gradient background in Swift and my code is:
let bgLayer = RadialGradientLayer(center: speed_dial_center_point, radius: speed_dial_size.width/2, colors: [UIColor.clear.cgColor,UIColor(red: 211/255, green: 211/255, blue: 211/255, alpha: 0.5).cgColor],speed_scale_margin: speed_scale_margin)//CALayer()
bgLayer.frame = CGRect(origin: rect.origin, size: speed_dial_size)
bgLayer.contentsScale = self.layer.contentsScale
bgLayer.setNeedsDisplay()
backgondArc_Layer.addSublayer(bgLayer)
self.layer.insertSublayer(backgondArc_Layer, below: speed_dial_layer)

So add a mask layer to your gradient layer that masks out the part you want to remove.

Related

Animating CAShapeLayer path smoothly

I'm trying to make a Gauge UIView to mimic the following image as close as possible
func gradientBezierPath(percent: CGFloat) -> UIBezierPath {
// vary this to move the start of the arc
let startAngle = CGFloat(180).toRadians()//-CGFloat.pi / 2 // This corresponds to 12 0'clock
// vary this to vary the size of the segment, in per cent
let proportion = CGFloat(50 * percent)
let centre = CGPoint (x: self.frame.size.width / 2, y: self.frame.size.height / 2)
let radius = self.frame.size.height/4//self.frame.size.width / (CGFloat(130).toRadians())
let arc = CGFloat.pi * 2 * proportion / 100 // i.e. the proportion of a full circle
// Start a mutable path
let cPath = UIBezierPath()
// Move to the centre
cPath.move(to: centre)
// Draw a line to the circumference
cPath.addLine(to: CGPoint(x: centre.x + radius * cos(startAngle), y: centre.y + radius * sin(startAngle)))
// NOW draw the arc
cPath.addArc(withCenter: centre, radius: radius, startAngle: startAngle, endAngle: arc + startAngle, clockwise: true)
// Line back to the centre, where we started (or the stroke doesn't work, though the fill does)
cPath.addLine(to: CGPoint(x: centre.x, y: centre.y))
return cPath
}
override func draw(_ rect: CGRect) {
// let endAngle = percent == 1.0 ? 0 : (percent * 180) + 180
path = UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2),
radius: self.frame.size.height/4,
startAngle: CGFloat(180).toRadians(),
endAngle: CGFloat(0).toRadians(),
clockwise: true)
percentPath = UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2),
radius: self.frame.size.height/4,
startAngle: CGFloat(180).toRadians(),
endAngle: CGFloat(0).toRadians(),
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = self.path.cgPath
shapeLayer.strokeColor = UIColor(red: 110 / 255, green: 78 / 255, blue: 165 / 255, alpha: 1.0).cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = 5.0
shapeLayer.lineCap = .round
self.layer.addSublayer(shapeLayer)
percentLayer.path = self.percentPath.cgPath
percentLayer.strokeColor = UIColor(red: 255 / 255, green: 93 / 255, blue: 41 / 255, alpha: 1.0).cgColor
percentLayer.fillColor = UIColor.clear.cgColor
percentLayer.lineWidth = 8.0
// percentLayer.strokeEnd = CGFloat(percent)
percentLayer.lineCap = .round
self.layer.addSublayer(percentLayer)
// n.b. as #MartinR points out `cPath.close()` does the same!
// circle shape
circleShape.path = gradientBezierPath(percent: 1.0).cgPath//cPath.cgPath
circleShape.strokeColor = UIColor.clear.cgColor
circleShape.fillColor = UIColor.green.cgColor
self.layer.addSublayer(circleShape)
gradient.frame = frame
gradient.mask = circleShape
gradient.type = .radial
gradient.colors = [UIColor(red: 255 / 255, green: 93 / 255, blue: 41 / 255, alpha: 0.0).cgColor,
UIColor(red: 255 / 255, green: 93 / 255, blue: 41 / 255, alpha: 0.0).cgColor,
UIColor(red: 255 / 255, green: 93 / 255, blue: 41 / 255, alpha: 0.4).cgColor]
gradient.locations = [0, 0.35, 1]
gradient.startPoint = CGPoint(x: 0.49, y: 0.55) // increase Y adds more orange from top to bottom
gradient.endPoint = CGPoint(x: 0.98, y: 1) // increase x pushes orange out more to edges
self.layer.addSublayer(gradient)
//myTextLayer.string = "\(Int(percent * 100))"
myTextLayer.backgroundColor = UIColor.clear.cgColor
myTextLayer.foregroundColor = UIColor.white.cgColor
myTextLayer.fontSize = 85.0
myTextLayer.frame = CGRect(x: (self.frame.size.width / 2) - (self.frame.size.width/8), y: (self.frame.size.height / 2) - self.frame.size.height/8, width: 120, height: 120)
self.layer.addSublayer(myTextLayer)
}
This produces the following in a playground which is pretty close to what i'm aiming for:
The problem comes when trying to animate the change in the gauge value. I can animate the percentLayer pretty easy with modifying strokeEnd, but animating the circleShape.path for the gradient results in some non-smooth animations if there's a large change in the percent value of the gauge. Here's the function i use to animate both layers (it's called on a timer every 2 seconds right now to simulate gauge value changes).
func randomPercent() {
let random = CGFloat.random(in: 0.0...1.0)
// Animate the percent layer
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = percentLayer.strokeEnd
animation.toValue = random
animation.duration = 1.5
percentLayer.strokeEnd = random
percentLayer.add(animation, forKey: nil)
// Animate the gradient layer
let newShapePath = gradientBezierPath(percent: random)
let gradientAnimation = CABasicAnimation(keyPath: "path")
gradientAnimation.duration = 1.5
gradientAnimation.toValue = newShapePath
gradientAnimation.fromValue = circleShape.path
circleShape.path = newShapePath.cgPath
self.circleShape.add(gradientAnimation, forKey: nil)
myTextLayer.string = "\(Int(random * 100))"
}
Notice how when the animation is done with small changes in the value, the animation looks good. However when there's a large change the gradient animation doesn't look natural at all. Any ideas on how to improve this? Or maybe is it possible to animate a different keyPath for better performance? Any help would be greatly appreciated!
You can't use the Bezier path addArc() function to animate an arc and change the arc distance.
The problem is control points. In order for an animation to work smoothly, the starting and ending shape must have the same number and type of control points. Under the covers, the UIBezierPath (and CGPath) objects create arcs approximating a circle by combining Bezier curves (I don't remember if it uses Quadratic or Cubic Bezier curves.) The entire circle is made up of multiple connected Bezier curves ("Bezier" the mathematical spline function, not UIBeizerPath, which is a UIKit function that creates shapes that can include Bezier paths.) I seem to remember a Bezier approximation of a circle is made up of 4 linked cubic Bezier curves. (See this SO answer for a discussion of what that looks like, if you're interested.)
Here is my understanding of how it works. (I might have the details wrong, but it illustrates the problem in any case.) As you move from <= 1/4 of a full circle to > 1/4 of a full circle, the arc function will use first 1 cubic Bezier section, then 2. At the transition from <= 1/2 of a circle to > 1/2 of a circle, it will shift to 3 Bezier curves, and at the transition from <= 3/4 of a circle to > 3/4 of a circle, it will switch to 4 Bezier curves.
The solution:
You are on the right track with using strokeEnd. Always create your shape as the full circle, and set strokeEnd to something less than 1. That will give you a part of a circle, but in a way that you can animate smoothly. (You can animate strokeStart as well.)
I've animated circles just like you describe using CAShapeLayer and strokeEnd (It was a number of years ago, so it was in Objective-C.) I wrote an article here on OS on using the approach to animate a mask on a UIImageView and create a "clock wipe" animation. If you have an image of your full shaded circle you could use that exact approach here. (You should be able to add a mask layer to any UIView's content layer or other layer, and animate that layer as in my clock wipe demo. Let me know if you need help deciphering the Objective-C.
Here is the sample clock wipe animation I created:
Note that you can use this effect to mask any layer, not just an image view.
EDIT: I posted an update to my clock wipe animation question and answer with a Swift version of the project.
You can get to the new repo directly at https://github.com/DuncanMC/ClockWipeSwift.
For your application I would set up the parts of your gauge that you need to animate as a composite of layers. You'd then attach a CAShapeLayer based mask layer to that composite layer and add a circle arc path to that shape layer and animate the strokeEnd as shown in my sample project. My clock wipe animation reveals the image like the sweep of a clock hand from the center of the layer. In your case you'd center the arc on the bottom center of your layer, and only use a half-circle arc in your shape layer. Using a mask that way would give you a sharp-edged crop to your composited layer. you'd lose the round end caps on your red arc. To fix that you'd have to animate the red arc as it's own shape layer (using strokeEnd) and animate the gradient fill's arc strokeEnd separately.

How do i make uiview using linear gradient in swift

i have in my figma this style: background: linear-gradient(155.98deg, #F5FEFF 0%, #C5F8FF 0%, rgba(106, 164, 189, 0.5) 47.57%, #0E8B9C 88.75%), #FFFFFF;
i need to make my uiview as linear-gradient
The basic idea is to configure a CAGradientLayer with an array of colors and locations. If you want a diagonal gradient, specify a startPoint and endPoint, too.
But you need to decipher that Figma CSS. The first parameter to linear-gradient is the angle. Linear gradients in iOS are not measured in angles, but rather with startPoint and endPoint, both of which are CGPoint references where 0, 0 represents the upper left corner of the view and 1, 1 represents the lower right corner. Your angle of 155.98deg suggests that you're looking for a “from upper left to lower right” gradient, which would be a startPoint of 0, 0 and an endPoint of 1, 1. (The exact mapping between the angle and these CGPoint values will vary based upon the aspect ratio of the view in question.)
The next parameter to linear-gradient is an array of tuples of colors and location. Solid colors are represented in hex strings. Translucent colors are represented with integer rgb values. So #F5FEFF 0% is at location 0.0 and a color of 0xf5 for red, 0xfe for green, and 0xff for blue. You need to divide all those numbers by 0xff (or 255). (The Xcode color literal will do that conversion for you.) And the 0% at the end of the string is the relative location of this color in the gradient (where 0% is the start of the gradient and 100% is the end). This correlates nicely to the CAGradientLayer property locations (though the % numbers are represented as values between 0 and 1).
The rgba(106, 164, 189, 0.5) 47.57% is red, green, and blue values of 106, 164, and 189, respectively, (each divided by 255). The 0.5 is an alpha of 50%. And the 47.57 is the position (47.57% of the way along the gradient).
Anyway, to render this, define an array of colors, array of positions, and the start and end of the gradient:
#IBDesignable class GradientView: UIView {
override class var layerClass: AnyClass { CAGradientLayer.self }
var gradientLayer: CAGradientLayer { layer as! CAGradientLayer }
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
func configure() {
gradientLayer.colors = [#colorLiteral(red: 0.9607843137, green: 0.9960784314, blue: 1, alpha: 1), #colorLiteral(red: 0.7725490196, green: 0.9725490196, blue: 1, alpha: 1), #colorLiteral(red: 0.4156862745, green: 0.6431372549, blue: 0.7411764706, alpha: 0.5), #colorLiteral(red: 0.05490196078, green: 0.5450980392, blue: 0.6117647059, alpha: 1)].map { $0.cgColor }
gradientLayer.locations = [0, 0, 0.4757, 0.8875]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
}
}
If you want to see either the hex representation or integer representation of those colors, just double click on the color and tap on "Other" to see the RGB representations. E.g. I double clicked on the second color and I can see the #C5F8FF value:
A couple of final observations:
Note, I didn't just add a CAGradientLayer and set its frame. Instead, I created a view whose backing layer is a gradient. That way, if the view size changes because of constraints (or if you animate the size change), the size of the CAGradientLayer changes dynamically, too.
I made it #IBDesignable so I could see it in the storyboard. That’s not necessary, but is useful if using storyboards.
I wonder if your designer really meant to have both #F5FEFF and #C5F8FF at the 0% position. I suspect that was a mistake. (Figma makes it too easy to have multiple color data points in the gradient overlap with each other at either 0% or 100%.)
The CSS you've provided does not look syntactically correct. Notably, I don't know what to make of that #FFFFFF floating at the end (and your gradient points don’t go all the way to 100%). But, I don't think I need to know your full intent here ... you can just update the CAGradientLayer colors and locations arrays as you see fit. Just make sure they both have the same number of entries.
As you have noted, if you tap on the rightmost “Code” tab in the panel on the right in Figma, you can see an iOS code snippet. I would be wary of using that code, verbatim, in your app, because it (a) won’t use the UIView subclass outlined above, breaking if the view changes size or is animated; and (b) it will tend to use hard-coded values whereas you generally want to use constraints to dictate the layouts within iOS. Use this code snippet for inspiration, suggestions of possible API, etc., but I would not be inclined to use that exact code. Like all auto-generated code snippets, Figma’s suggested iOS code is not ideal.
let layerGradient = CAGradientLayer()
layerGradient.colors = [hexStringToUIColor(hex: "yourhex").cgColor, hexStringToUIColor(hex: "yourhex").cgColor]
layerGradient.startPoint = CGPoint(x: 0, y: 0.5)
layerGradient.endPoint = CGPoint(x: 1, y: 0.5)
layerGradient.frame = CGRect(x: 0, y: 0, width: yourView.bounds.width, height: yourView.bounds.height)
self.yourView.layer.addSublayer(layerGradient)
Essentially you add a layer on top of your view which you then assign a start point and end point and provide colors to use.
This is the function used to convert hex strings to colors
func hexStringToUIColor (hex:String) -> UIColor {
var cString:String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
if (cString.hasPrefix("#")) {
cString.remove(at: cString.startIndex)
}
if ((cString.count) != 6) {
return UIColor.gray
}
var rgbValue:UInt64 = 0
Scanner(string: cString).scanHexInt64(&rgbValue)
return UIColor(
red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
alpha: CGFloat(1.0)
)
}

Stroke a CGPath with a gradient along its length [duplicate]

I have a relatively straight forward implementation of a progress view set up with CALayer objects. The progress view itself is a subview of UIView.
Here is the code that sets up the progress ring:
self.progressRingLayer = CAShapeLayer()
let innerRect = CGRectInset(bounds, CGFloat(self.lineWidth) / 2, CGFloat(self.lineWidth) / 2)
let innerPath = UIBezierPath(ovalInRect: innerRect)
self.progressRingLayer.path = innerPath.CGPath
self.progressRingLayer.fillColor = UIColor.clearColor().CGColor
self.progressRingLayer.strokeColor = kProgressColor.CGColor
self.progressRingLayer.anchorPoint = CGPointMake(0.5, 0.5)
self.progressRingLayer.transform = CATransform3DRotate(self.progressRingLayer.transform, (CGFloat(M_PI))*1, 0, 0, 1)
self.progressRingLayer.lineCap = kCALineCapRound
self.progressRingLayer.lineWidth = CGFloat(self.lineWidth)
self.layer.addSublayer(self.progressRingLayer)
What I am trying to do now is add a gradient to the progressRingLayer that follows (or bends with) the path. I have been successful in adding a linear gradient to the fill, but not to just the path.
Here is an example of what effect I want:
So far everything I have found requires a bunch of additional steps with CoreGraphics and CGContext that don't quite fit with my implementation. Any help would be great, thanks!
What I would do is draw a gradient layer, then draw on top of that a layer that is black with the arc erased.
Here's my attempt at roughly the image you provided (I omitted the white label in the center, but that's trivial):
And here's the code that generated it:
let r = CGRectMake(100,100,130,100)
let g = CAGradientLayer()
g.frame = r
let c1 = UIColor(
red: 151.0/255.0, green: 81.0/255.0, blue: 227.0/255.0, alpha: 1)
let c2 = UIColor(
red: 36.0/255.0, green: 176.0/255.0, blue: 233.0/255.0, alpha: 1)
g.colors = [c1.CGColor as AnyObject, c2.CGColor as AnyObject];
self.view.layer.addSublayer(g)
let percent = CGFloat(0.64) // percentage of circle
UIGraphicsBeginImageContextWithOptions(r.size, false, 0)
let con = UIGraphicsGetCurrentContext()
CGContextFillRect(con, CGRect(origin: CGPoint(), size: r.size))
CGContextSetLineWidth(con, 5)
CGContextSetLineCap(con, kCGLineCapRound)
CGContextSetBlendMode(con, kCGBlendModeClear)
let pi = CGFloat(M_PI)
CGContextAddArc(con, r.size.width/2.0, r.size.height/2.0, 30,
-pi/2.0, -pi/2.0 + percent*pi*2.0, 0)
CGContextStrokePath(con)
let im = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let b = CALayer()
b.frame = r
b.contents = im.CGImage
self.view.layer.addSublayer(b)
The gradient layer (the first part of the code) is just a "serving suggestion". If that is not the gradient you want, you can design your own. You could draw it in Photoshop and use an image as the content of the gradient layer. Or you could make an "angular" layer in code, using third-party code such as https://github.com/paiv/AngleGradientLayer. The point of the example is merely to show how it is possible to "erase" an arc in a black layer so as to reveal the gradient concealed behind it, and thus appear to paint with a gradient.

How can I obtain a CGColor from RGB values?

I am trying to create a custom RGB CGColor using Swift to use as a border color for a UIButton. I've tried following code but the color is not visible:
var red = UIColor(red: 100.0, green: 130.0, blue: 230.0, alpha: 1.0)
self.layer.borderColor = red.CGColor
Is there any way to create a CGColor directly from RGB values?
You have to give the values between 0 and 1.0. So divide the RGB values by 255.
Change Your code to
var red = UIColor(red: 100.0/255.0, green: 130.0/255.0, blue: 230.0/255.0, alpha: 1.0)
self.layer.borderColor = red.CGColor
You can get that down to one line in Swift 3:
avLayer.backgroundColor = UIColor.red.CGColor
On Mac OS X there is CGColorCreateGenericRGB(), but it doesn't exist on iOS. You can use CGColorCreate with appropriate parameters, though.
self.layer.borderColor = CGColorCreate(CGColorSpaceCreateDeviceRGB(), [1.0, 0.5, 0.5, 1.0])
The values in the array are, in order of appearance: red, green, blue, and alpha. Just like UIColor :)
https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CGColor/#//apple_ref/c/func/CGColorCreate
UImage *newImage = [self imageWithColor:[UIColor colorWithRed:200/255. green:228/255. blue:194/255. alpha:0.99]];
Maybe all you need is set the borderWidth, by default it is 0, so it is not visible:
self.layer.borderWidth = 1.0
Just in case you can set the cornerRadius if you need it rounded ... might be useful ;-)
self.layer.cornerRadius = 4.0

Swift: Gradient along a bezier path (using CALayers)

I have a relatively straight forward implementation of a progress view set up with CALayer objects. The progress view itself is a subview of UIView.
Here is the code that sets up the progress ring:
self.progressRingLayer = CAShapeLayer()
let innerRect = CGRectInset(bounds, CGFloat(self.lineWidth) / 2, CGFloat(self.lineWidth) / 2)
let innerPath = UIBezierPath(ovalInRect: innerRect)
self.progressRingLayer.path = innerPath.CGPath
self.progressRingLayer.fillColor = UIColor.clearColor().CGColor
self.progressRingLayer.strokeColor = kProgressColor.CGColor
self.progressRingLayer.anchorPoint = CGPointMake(0.5, 0.5)
self.progressRingLayer.transform = CATransform3DRotate(self.progressRingLayer.transform, (CGFloat(M_PI))*1, 0, 0, 1)
self.progressRingLayer.lineCap = kCALineCapRound
self.progressRingLayer.lineWidth = CGFloat(self.lineWidth)
self.layer.addSublayer(self.progressRingLayer)
What I am trying to do now is add a gradient to the progressRingLayer that follows (or bends with) the path. I have been successful in adding a linear gradient to the fill, but not to just the path.
Here is an example of what effect I want:
So far everything I have found requires a bunch of additional steps with CoreGraphics and CGContext that don't quite fit with my implementation. Any help would be great, thanks!
What I would do is draw a gradient layer, then draw on top of that a layer that is black with the arc erased.
Here's my attempt at roughly the image you provided (I omitted the white label in the center, but that's trivial):
And here's the code that generated it:
let r = CGRectMake(100,100,130,100)
let g = CAGradientLayer()
g.frame = r
let c1 = UIColor(
red: 151.0/255.0, green: 81.0/255.0, blue: 227.0/255.0, alpha: 1)
let c2 = UIColor(
red: 36.0/255.0, green: 176.0/255.0, blue: 233.0/255.0, alpha: 1)
g.colors = [c1.CGColor as AnyObject, c2.CGColor as AnyObject];
self.view.layer.addSublayer(g)
let percent = CGFloat(0.64) // percentage of circle
UIGraphicsBeginImageContextWithOptions(r.size, false, 0)
let con = UIGraphicsGetCurrentContext()
CGContextFillRect(con, CGRect(origin: CGPoint(), size: r.size))
CGContextSetLineWidth(con, 5)
CGContextSetLineCap(con, kCGLineCapRound)
CGContextSetBlendMode(con, kCGBlendModeClear)
let pi = CGFloat(M_PI)
CGContextAddArc(con, r.size.width/2.0, r.size.height/2.0, 30,
-pi/2.0, -pi/2.0 + percent*pi*2.0, 0)
CGContextStrokePath(con)
let im = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let b = CALayer()
b.frame = r
b.contents = im.CGImage
self.view.layer.addSublayer(b)
The gradient layer (the first part of the code) is just a "serving suggestion". If that is not the gradient you want, you can design your own. You could draw it in Photoshop and use an image as the content of the gradient layer. Or you could make an "angular" layer in code, using third-party code such as https://github.com/paiv/AngleGradientLayer. The point of the example is merely to show how it is possible to "erase" an arc in a black layer so as to reveal the gradient concealed behind it, and thus appear to paint with a gradient.

Resources