How to get a CAShapeLayer supervision position?

I am working on a pie chart, and I need to add a small view at the end of the selected circle (not the shadow) to indicate the target value. Can I find the environment (highlight) the super-representation of x, y positions based on the endpoint of the move?

UIView->Circle (using CAShapeLayer and UIBezierPath ). I need to get the UIView position based on the final value of the Circle stroke.

Please view this link (e.g. 23% with a dashed line) http://support.softwarefx.com/media/74456678-5a6a-e211-84a5-0019b9e6b500/large

Thanks in advance! Find the position of the green end of the circle

Update: I tried the alexburtnik code. Actually, I work on the hourly chart in lens c, but this is not a problem. I tried, as Alexburtnik said, I believe that it works great for an anti-hour wise schedule. We also need to make some changes to the code to work clockwise. Please give a decision 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 schedule

And I tried, as estepicking mentioned, here I added a code and a 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.

+5
source share
2 answers

1. Math

Forget iOS for a moment and solve the math problem.

Task: find (x; y) a point for a given α.

Solution: x = cos (α); y = sin (α)

math

2. Replacement

Your starting point is not 0 in terms of trigonometry, but rather in π / 2 . Also, your arc angle is determined by the progress parameter, so it leads you to the following:

x = cos (progress * 2 * π + π / 2) = -sin (progress * 2 * π)
y = sin (progress * 2 * π + π / 2) = cos (progress * 2 * π)

3. Now back to iOS:

Since the y axis returns at the beginning of iOS in the upper left corner, you must convert the previous solution this way:

x '= 0.5 * (1 + x)
y '= 0.5 * (1-y)

Green for mathematical coordinates, blue for the iOS coordinate system, which we converted to:

iOS

Now all you have to do is multiply the result by width and height:

x '= 0.5 * width * (1 - sin (progress * 2 * π))
y '= 0.5 * height * (1 - cos (stroke * 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 clockwise, just multiply the 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 (stroke * 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.

+5
source

To understand the essence, you need to work a little on math.

When you define an arc in UIBezierPath, you pass startAngle and endAngle . In this case, I think you want to align the other view with the end of the line.

 UIBezierPath(arcCenter center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool) 

Now you need to take the sine and cosine of this endAngle parameter. The sine of the final angle will give you the y position in unit coordinates, as well as in the range [-1, 1], and the cosine of the final angle will give you the x position also in unit coordinates. Then these coordinates can be scaled according to the size of the arc and adjusted so that it corresponds to the view, and then shifted to the position of the view to get the position of the end of the move.

Here is an example of a playground for this effect. I aligned the UILabel so that the text is centered over the endpoint. Paste this into your iOS playground to try yourself.

Centralized Demonstration

 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 
+2
source

Source: https://habr.com/ru/post/1259787/


All Articles