Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get CAShapeLayer superview position?

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];}

Please check my orignal graph

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];} 

Please check this result

like image 828
Gopik Avatar asked Nov 15 '16 19:11

Gopik


2 Answers

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(α)

math

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:

iOS

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.

like image 159
alexburtnik Avatar answered Oct 12 '22 11:10

alexburtnik


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.

Demonstrating the centered view

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
like image 45
esthepiking Avatar answered Oct 12 '22 09:10

esthepiking