Swift:让自定义进度视图跟随用户点击?用户界面贝塞尔曲线
Swift: make custom progress view follow user tap? UIBezier
好的,我在这里实现了一个圆形的自定义 UIProgressView:
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)
}
这会根据percentage
沿着大圆的路径移动小圆。问题是我需要允许用户通过 tapping/scrubbing 在 UIBezierPath -
上控制这个百分比
到目前为止,我已经知道点击是否在较大的圆圈内:
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
}
}
}
如何从这个 CGPoint 中获取沿着路径点击的较大圆的百分比(百分比)?
我尝试过的:
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
如果让触摸 = touches.first {
让 currentPoint = touch.location(in: self)
// 用你的 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)
}
}
}
}
抓紧这涉及到一些数学!
假设你的圆心是 (0, 0)。假设你有一个点 (x, y) 位于圆周上,你可以使用 atan2 or arctan2 function 来获取角度(以弧度为单位; 弧度是你想要的,因为 UIBezierPath
使用弧度) 该点相对于圆心。因此,通过这个角度,您可以知道用户触摸了圆周上的哪个位置(称之为 touch angle
)。您可以将其用作 UIBezierPath
上的 endAngle
。
注意:如果你的圆心不是(0, 0)而是(x1, y1),通过减去触摸点(x, y)的差值来补偿即,您的新点是 (x - x1, y - y1)
如果您想要该点从 0 弧度开始覆盖的圆周的百分比。您可以使用公式 percentage = (touch angle*(in rads)* * 100)/2π)
.
More on atan2/arctan2
您可以使用公式 touch angle = tan^(-1)(y/x)
又名 touch angle = arctan2(y,x)
计算您的接触角。其中 (x, y) 是用户在圆周上触摸的点。
Example and explanation (Edit)
好的呸,经过一番研究,我明白了。
取一个 look at this code,注意 touchesBegan(_ ...
函数。
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
}
}
}
我用 CAShapeLayer
而不是在 draw(_ rect...
中绘图;我建议你也这样做。你的方法也很好,但有一些变化,我会在最后提到。
我用过两条弧线:
1. 占位弧 - backCircleShapeLayer
2. 进度指示弧 - frontCircleShapeLayer
当用户点击占位符弧时,我们可以计算角度(以弧度为单位)并绘制从 start Angle
到 touch Angle
的进度指示弧,如前所述(在 draw(_ ...
方法 中使用此方法直接用 UIBezierPath
绘制笔画,但我正在做的是使用这些值来计算分数 perc
(介于 0和 1) 并将其用作进度弧的 stroke end
。
这带来了一些有趣的新知识。 atan(y/x)
的值是特定于域的 (Some math behind this, if you're interested)。所以 如果你的触摸在第四象限,你添加 360˚ 或 2π,如果它在第二或第三象限,你添加 180˚ 或 π。添加后,您的 touch angle
应该反映出完美的价值。
好的,我在这里实现了一个圆形的自定义 UIProgressView:
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)
}
这会根据percentage
沿着大圆的路径移动小圆。问题是我需要允许用户通过 tapping/scrubbing 在 UIBezierPath -
到目前为止,我已经知道点击是否在较大的圆圈内:
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
}
}
}
如何从这个 CGPoint 中获取沿着路径点击的较大圆的百分比(百分比)?
我尝试过的:
override func touchesBegan(_ touches: Set, with event: UIEvent?) { 如果让触摸 = touches.first { 让 currentPoint = touch.location(in: self) // 用你的 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)
}
}
}
}
抓紧这涉及到一些数学!
假设你的圆心是 (0, 0)。假设你有一个点 (x, y) 位于圆周上,你可以使用 atan2 or arctan2 function 来获取角度(以弧度为单位; 弧度是你想要的,因为 UIBezierPath
使用弧度) 该点相对于圆心。因此,通过这个角度,您可以知道用户触摸了圆周上的哪个位置(称之为 touch angle
)。您可以将其用作 UIBezierPath
上的 endAngle
。
注意:如果你的圆心不是(0, 0)而是(x1, y1),通过减去触摸点(x, y)的差值来补偿即,您的新点是 (x - x1, y - y1)
如果您想要该点从 0 弧度开始覆盖的圆周的百分比。您可以使用公式 percentage = (touch angle*(in rads)* * 100)/2π)
.
More on atan2/arctan2
您可以使用公式 touch angle = tan^(-1)(y/x)
又名 touch angle = arctan2(y,x)
计算您的接触角。其中 (x, y) 是用户在圆周上触摸的点。
Example and explanation (Edit)
好的呸,经过一番研究,我明白了。
取一个 look at this code,注意 touchesBegan(_ ...
函数。
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
}
}
}
我用 CAShapeLayer
而不是在 draw(_ rect...
中绘图;我建议你也这样做。你的方法也很好,但有一些变化,我会在最后提到。
我用过两条弧线:
1. 占位弧 - backCircleShapeLayer
2. 进度指示弧 - frontCircleShapeLayer
当用户点击占位符弧时,我们可以计算角度(以弧度为单位)并绘制从 start Angle
到 touch Angle
的进度指示弧,如前所述(在 draw(_ ...
方法 中使用此方法直接用 UIBezierPath
绘制笔画,但我正在做的是使用这些值来计算分数 perc
(介于 0和 1) 并将其用作进度弧的 stroke end
。
这带来了一些有趣的新知识。 atan(y/x)
的值是特定于域的 (Some math behind this, if you're interested)。所以 如果你的触摸在第四象限,你添加 360˚ 或 2π,如果它在第二或第三象限,你添加 180˚ 或 π。添加后,您的 touch angle
应该反映出完美的价值。