I'm working on a circular chart, and I need to add small view at end of the highlighted circle (not shadow) for pointing at the target value. Can I find a circle (highlight) super view x,y positions based on the stroke end point?
UIView->Circle
(using CAShapeLayer
and UIBezierPath
). I need to get a position of UIView
based on ending Circle
stroke value.
Please refer this link(like 23% with dotted line) http://support.softwarefx.com/media/74456678-5a6a-e211-84a5-0019b9e6b500/large
Thanks in advance! Need to find green end circle position
Update: I have tried alexburtnik code, Actually am working on clock-wise graph in objective c but that's not a problem here. I tried as alexburtnik mentioned, I believe it perfectly works for anti-clock wise graph. We need to make some change in code to work for Clockwise too, Please give solution if you know.
CGFloat radiusCircle = (self.frame.size.height * 0.5) - ([_lineWidth floatValue]/2.0f);
-(void)addTargetViewWithOptions:(CGFloat)progress andRadius:(CGFloat)radius{
CGFloat x = radius * (1 + (cos(M_PI * (2 * progress + 0.5))));
CGFloat y = radius * (1 - (sin(M_PI * (2 * progress + 0.5))));
UIView *targetView = [[UIView alloc]initWithFrame:CGRectMake(x, y, 40, 30)];
targetView.backgroundColor = [UIColor greenColor];
[self addSubview:targetView];}
And i tried as esthepiking mentioned, here i added code and screenshot
-(void)addTargetView{
CGFloat endAngle = -90.01f;
radiusCircle = (self.frame.size.height * 0.5) - ([_lineWidth floatValue]/2.0f);
endAngleCircle = DEGREES_TO_RADIANS(endAngle);//-1.570971
// Size for the text
CGFloat width = 75;
CGFloat height = 30;
// Calculate the location of the end of the stroke
// Cos calculates the x position of the point (in unit coordinates)
// Sin calculates the y position of the point (in unit coordinates)
// Then scale this to be on the range [0, 1] to match the view
CGFloat endX = (cos(endAngleCircle) / 2 + 0.5);
CGFloat endY = (sin(endAngleCircle) / 2 + 0.5);
// Scale the coordinates to match the diameter of the circle
endX *= radiusCircle * 2;
endY *= radiusCircle * 2;
// Translate the coordinates based on the location of the frame
endX -= self.frame.origin.x;
endY -= self.frame.origin.y;
// Vertically align the label
endY += height;
// Setup the label
UIView *targetView = [[UIView alloc]initWithFrame:CGRectMake(endX, endY, width, height)];
targetView.backgroundColor = [UIColor redColor];
[self addSubview:targetView];}
1. Math
Let's forget about iOS for a moment and solve a math problem.
Problem: find (x;y) point for a given α.
Solution: x = cos(α); y = sin(α)
2. Substitution
Your starting point is not at 0 in terms of trigonometry, but rather at π/2. Also your arc angle is defined by progress parameter, so it brings you to this:
x = cos(progress * 2 * π + π/2) = -sin(progress * 2 * π)
y = sin(progress * 2 * π + π/2) = cos(progress * 2 * π)
3. Now let's return to iOS:
Since in iOS origin is in the top left corner an y axis is reverted you should convert the previous solution this way:
x' = 0.5 * (1 + x)
y' = 0.5 * (1 - y)
Green is for mathematical coordinates, blue is for iOS coordinate system, which we transformed to:
Now everything you need to do is to multiply the result by width and height:
x' = 0.5 * width * (1 - sin(progress * 2 * π))
y' = 0.5 * height * (1 - cos(progress * 2 * π))
4. Code:
func point(progress: Double, radius: CGFloat) -> CGPoint {
let x = radius * (1 - CGFloat(sin(progress * 2 * Double.pi))))
let y = radius * (1 - CGFloat(cos(progress * 2 * Double.pi))))
return CGPoint(x: x, y: y)
}
Edit
If you want to switch it to clockwise direction, just multiply angle by -1
:
x = cos(-progress * 2 * π + π/2) = -sin(-progress * 2 * π) = sin(progress * 2 * π)
y = sin(-progress * 2 * π + π/2) = cos(-progress * 2 * π) = cos(progress * 2 * π)x' = 0.5 * width * (1 + sin(progress * 2 * π))
y' = 0.5 * height * (1 - cos(progress * 2 * π))
func point(progress: Double, radius: CGFloat) -> CGPoint {
let x = radius * (1 + CGFloat(sin(progress * 2 * Double.pi))))
let y = radius * (1 - CGFloat(cos(progress * 2 * Double.pi))))
return CGPoint(x: x, y: y)
}
Hope this helps.
To get the point, you'll need to do a little bit of math.
When you define an arc in a UIBezierPath, you pass in the startAngle
and the endAngle
. In this case, I believe you want to line up another view with the end of the stroke.
UIBezierPath(arcCenter center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool)
Now you'll need to take the sine and cosine of that endAngle
parameter. Sine of the end angle will give you the y position in unit coordinates, aka on the range [-1, 1], and the cosine of the end angle will give you the x position also in unit coordinates. Those coordinates can then be scaled by the size of the arc and adjusted so it matches the view, then shifted by the view's position to get the position of the end of the stroke.
Here's an example playground for this effect. I aligned a UILabel so the text is centered over the end point. Paste this into an iOS playground to try it out yourself.
import UIKit
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
// Frame to wrap all of this inside of
let frame = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
frame.backgroundColor = .white
let radius = 100 as CGFloat
// Outermost gray circle
let grayCircle = CAShapeLayer()
grayCircle.frame = frame.frame
grayCircle.path = UIBezierPath(arcCenter: frame.center, radius: radius, startAngle: 0, endAngle: CGFloat(M_PI * 2), clockwise: true).cgPath
grayCircle.strokeColor = UIColor.lightGray.cgColor
grayCircle.lineWidth = 10
grayCircle.fillColor = UIColor.clear.cgColor
frame.layer.addSublayer(grayCircle)
// Ending angle for the arc
let endAngle = CGFloat(M_PI / 5)
// Inner green arc
let greenCircle = CAShapeLayer()
greenCircle.frame = frame.frame
greenCircle.path = UIBezierPath(arcCenter: frame.center, radius: radius, startAngle: CGFloat(-M_PI_2), endAngle: endAngle, clockwise: false).cgPath
greenCircle.strokeColor = UIColor.green.cgColor
greenCircle.lineWidth = 10
greenCircle.fillColor = UIColor.clear.cgColor
greenCircle.lineCap = kCALineCapRound
frame.layer.addSublayer(greenCircle)
// Size for the text
let width = 75 as CGFloat
let height = 30 as CGFloat
// Calculate the location of the end of the stroke
// Cos calculates the x position of the point (in unit coordinates)
// Sin calculates the y position of the point (in unit coordinates)
// Then scale this to be on the range [0, 1] to match the view
var endX = (cos(endAngle) / 2 + 0.5)
var endY = (sin(endAngle) / 2 + 0.5)
// Scale the coordinates to match the diameter of the circle
endX *= radius * 2
endY *= radius * 2
// Translate the coordinates based on the location of the frame
endX -= frame.frame.origin.x
endY -= frame.frame.origin.y
// Vertically align the label
endY += height
// Setup the label
let text = UILabel(frame: CGRect(x: endX, y: endY, width: width, height: height))
text.text = "Stroke end!"
text.font = UIFont.systemFont(ofSize: 14)
frame.addSubview(text)
PlaygroundPage.current.liveView = frame
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With