Ok I have a custom UIProgressView in shape of a circle implemented here:
class PlaybackLineView: UIView {
var percentage: Float = 0.0 {
didSet {
if percentage > 1 {
//print("happned!")
percentage = 1.0
}
DispatchQueue.main.async {
self.setNeedsDisplay()
}
}
}
override func draw(_ rect: CGRect) {
let playbackRed = UIColor(red: 181/255.0, green: 23/255.0, blue: 65/255.0, alpha: 1.0)
playbackRed.setFill()
playbackRed.setStroke()
let newWidth = rect.size.width * CGFloat(percentage)
let fillRect = CGRect(x: 0, y: 0, width: newWidth, height: rect.size.height)
let fillRectPath = UIBezierPath(rect: fillRect)
fillRectPath.fill()
}
@IBInspectable public var drawEndPoint: Bool = true
@IBInspectable public var drawWhenZero: Bool = false
@IBInspectable public var strokeWidth: CGFloat = 3.0
let smallCircleRadius: CGFloat = 6.0
var fullCirclePath: UIBezierPath!
override func draw(_ rect: CGRect) {
if !drawWhenZero && (percentage == 0 || percentage == 1) { return }
UIColor.clear.setFill()
let arcCenter = CGPoint(x: rect.midX, y: rect.midY)
let radius = (rect.width - smallCircleRadius * 2) / 2.0
let circleGray = UIColor(red: 218/255.0, green: 218/255.0, blue: 218/255.0, alpha: 1.0)
circleGray.setStroke()
fullCirclePath = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: 0.0, endAngle:CGFloat.pi * 2, clockwise: true)
fullCirclePath.lineWidth = strokeWidth
fullCirclePath.fill()
fullCirclePath.stroke()
let circleRed = UIColor(red: 181/255.0, green: 23/255.0, blue: 65/255.0, alpha: 1.0)
circleRed.setStroke()
let arcPath = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: 0.0, endAngle:CGFloat.pi * 2 * CGFloat(percentage), clockwise: true)
arcPath.fill()
arcPath.stroke()
arcPath.lineWidth = strokeWidth
if !drawEndPoint { return }
let smallCirclePath = UIBezierPath(arcCenter: arcPath.currentPoint, radius: smallCircleRadius, startAngle: 0.0, endAngle:CGFloat.pi * 2, clockwise: true)
circleRed.setFill()
smallCirclePath.fill()
smallCirclePath.stroke()
self.transform = CGAffineTransform(rotationAngle: (-90).degreesToRadians)
}
This moves the small circle along the path of the larger circle according to percentage
. Issue is I need to allow the user to control this percentage by tapping/scrubbing ON the UIBezierPath -
So far Ive gotten this that gets whether the tap was within the larger circle:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let currentPoint = touch.location(in: self)
// do something with your currentPoint
if(fullCirclePath.contains(currentPoint))
{
print("CONTAINS!: ", currentPoint)
//percentage = 0.5
}
}
}
How do I get how much (in percentage) of the larger circle the tap is along the path, from this CGPoint?
What Ive tried:
override func touchesBegan(_ touches: Set, with event: UIEvent?) { if let touch = touches.first { let currentPoint = touch.location(in: self) // do something with your currentPoint
//print(fullCirclePath.cgPath.cen)
if(fullCirclePath.contains(currentPoint))
{
touchAngle = atan2((currentPoint.x-arcCenter.x), (currentPoint.y-arcCenter.y))
percentage = Float(touchAngle * 100)/Float(M_PI * 2)
print("CONTAINS!: ", currentPoint, "Angle: ", touchAngle.radiansToDegrees, " Percent: ", percentage)
if(AudioPlayerManager.shared.isPlaying())
{
AudioPlayerManager.shared.seek(toProgress: percentage)
}
}
}
}
Hold tight this involves some math!
Assuming the centre of your circle is (0, 0). Say you have a point (x, y) lying on the circumference, you can use the atan2 or arctan2 function to get the angle (in radians; radians being what you want as UIBezierPath
uses radians) the point lies with respect to the centre of the circle. So, with this angle, you know where on the circumference of your circle the user has touched (call it touch angle
). You can use this as your endAngle
on UIBezierPath
.
Note: If your circle's centre is not (0, 0) but is (x1, y1), compensate by subtracting the difference from the touch points (x, y) i.e, your new points are (x - x1, y - y1)
If you want a percentage of how much of the circumference the point covers from 0 rads. You can use the formula percentage = (touch angle*(in rads)* * 100)/2π)
.
More on atan2/arctan2
You can calculate your touch angle using the formula touch angle = tan^(-1)(y/x)
aka touch angle = arctan2(y,x)
. Where (x, y) are the points the user has touched on the circumference.
Example and explanation (Edit)
Okay phew, after some research, I figured it out.
Take a look at this code, notice the touchesBegan(_ ...
function.
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
for touch in touches {
let point = touch.location(in: self)
if backCircleBezierPath!.contains(point) {
print("contains")
let centeredPoint = CGPoint(x: (point.x - frame.width/2), y: (point.y - frame.height/2))
print(centeredPoint)
var rads = atan(centeredPoint.y/centeredPoint.x)
if centeredPoint.x < 0 {
rads += CGFloat.pi
} else if centeredPoint.x > 0 && centeredPoint.y < 0 {
rads += CGFloat.pi * 2
}
let perc = (rads - backCircleStartAngle) / abs(backCircleStartAngle - backCircleEndAngle)
frontCircleShapeLayer?.strokeEnd = perc
}
}
}
I've used CAShapeLayer
instead of drawing in draw(_ rect...
; I recommend you do that same. Your way works fine too but with a few changes which I will mention in the end.
I've used two arcs:
1. Placeholder arc - backCircleShapeLayer
2. Progress indicator arc - frontCircleShapeLayer
When the user taps on the placeholder arc, we can calculate the angle (in radians) and draw the progress indicator arc from start Angle
to touch Angle
as mentioned before (use this method for drawing strokes directly with UIBezierPath
in the draw(_ ...
method), but what I'm doing is using those values to calculate the fraction perc
(between 0 and 1) and using that as the stroke end
of the progress arc.
This brings to some interesting new learning. The value of atan(y/x)
is domain specific (Some math behind this, if you're interested). So if your touch is in the 4th quadrant you add 360˚ or 2π and if it is in the 2nd or 3rd quadrant you add 180˚ or π. After this addition your touch angle
should reflect the perfect value.