在Android中,有没有一种只用两点和曲线半径画圆弧的方便方法?
Is there a convenient way to draw arc only using two points and curve radius in Android?
首先
我找了很久,已经看过很多问题,其中有两个:
How to draw Arc between two points on the Canvas?
虽然它们看起来像是同一个问题,但我很确定它们不相同。第一题知道圆心,第二题画的是贝塞尔曲线,不是圆弧
描述
现在我们有A
和B
两个点和给定的曲线半径,如何画圆弧如图所示?
由于 Path.arcTo
所需的参数是 RectF
、startAngle
和 sweepAngle
,因此似乎没有简单的方法。
我目前的解决方案
我现在有了解决方案,我会在下面的答案中展示。
既然这么复杂,请问有没有更简单的方法解决?
1.找到圆心
这个可以用二元二次方程求解,如图:
虽然还有其他的解决办法,但是现在圆心的位置已经知道了。
2。计算起始角和扫描角
根据圆心,RectF
容易知道。
现在计算 startAngle
和 sweepAngle
.
通过几何方法,我们可以计算出startAngle
和sweepAngle
:
val startAngle = acos((x1 - x0) / r) / Math.PI.toFloat() * 180
val endAngle = acos((x2 - x0) / r) / Math.PI.toFloat() * 180
val sweepAngle = endAngle - startAngle
此时x1
为A点的x坐标,x2
为B点的x坐标,r
为圆弧的曲线半径. (有可能的结果,另一种是[-startAngle, startAngle - endAngle]
。根据实际情况选择一个。)
因此,我们获得了 Path.arcTo
方法所需的所有参数,现在我们可以绘制圆弧了。
3。科特林代码
帮助函数的全部代码:
/**
* Append the arc which is starting at ([x1], [y1]), ending at ([x2], [y2])
* and with the curve radius [r] to the path.
* The Boolean value [clockwise] shows whether the process drawing the arc
* is clockwise.
*/
@Throws(Exception::class)
private fun Path.arcFromTo(
x1: Float, y1: Float, x2: Float, y2: Float, r: Float,
clockwise: Boolean = true
) {
val c = centerPos(x1, y1, x2, y2, r, clockwise) // circle centers
// RectF borders
val left = c.x - r
val top = c.y - r
val right = c.x + r
val bottom = c.y + r
val startAngle = acos((x1 - c.x) / r) / Math.PI.toFloat() * 180
val endAngle = acos((x2 - c.x) / r) / Math.PI.toFloat() * 180
arcTo(
left, top, right, bottom,
if (clockwise) startAngle else -startAngle,
if (clockwise) endAngle - startAngle else startAngle - endAngle,
false
)
}
// use similar triangles to calculate circle center
@Throws(Exception::class)
private fun centerPos(
x1: Float, y1: Float, x2: Float, y2: Float, r: Float,
clockwise: Boolean
): Point {
val ab = ((x1 - x2).p2 + (y1 - y2).p2).sqrt
if (ab > r * 2) throw Exception("No circle fits the condition.")
val a = ab / 2
val oc = (r.p2 - a.p2).sqrt
val dx = (oc * (y2 - y1) / ab).absoluteValue.toInt()
val dy = (oc * (x2 - x1) / ab).absoluteValue.toInt()
val cx = ((x1 + x2) / 2).toInt()
val cy = ((y1 + y2) / 2).toInt()
return if (x1 >= x2 && y1 >= y2 || x1 <= x2 && y1 <= y2)
if (clockwise) Point(cx + dx, cy - dy) else Point(cx - dx, cy + dy)
else
if (clockwise) Point(cx - dx, cy - dy) else Point(cx + dx, cy + dy)
}
可能没有更简单的方法了。所有能做的就是通过几何方法改进您的解决方案。由于圆心总是在弦的垂直平分线上,所以不需要求解如此广义的方程
顺便说一句,你是如何定义的 Clockwise/Counter-clockwise 还不清楚。弧的缠绕方向应独立于节点放置(=A,B 的坐标)确定。
如下图所示,在A到B的直线路径上,中心O要放在右手边(CW)或左手边(CCW)。就这些了。
还有一些要改变的方面:
- startAngle最好用atan2()来计算。因为acos()在某些点上有奇点。
- 也可以用 asin() 计算 sweepAngle。
毕竟代码可以稍微简化如下。
@Throws(Exception::class)
private fun Path.arcFromTo2(
x1: Float, y1: Float, x2: Float, y2: Float, r: Float,
clockwise: Boolean = true
) {
val d = PointF((x2 - x1) * 0.5F, (y2 - y1) * 0.5F)
val a = d.length()
if (a > r) throw Exception()
val side = if (clockwise) 1 else -1
val oc = sqrt(r * r - a * a)
val ox = (x1 + x2) * 0.5F - side * oc * d.y / a
val oy = (y1 + y2) * 0.5F + side * oc * d.x / a
val startAngle = atan2(y1 - oy, x1 - ox) * 180F / Math.PI.toFloat()
val sweepAngle = side * 2.0F * asin(a / r) * 180F / Math.PI.toFloat()
arcTo(
ox - r, oy - r, ox + r, oy + r,
startAngle, sweepAngle,
false
)
}
首先
我找了很久,已经看过很多问题,其中有两个:
How to draw Arc between two points on the Canvas?
虽然它们看起来像是同一个问题,但我很确定它们不相同。第一题知道圆心,第二题画的是贝塞尔曲线,不是圆弧
描述
现在我们有A
和B
两个点和给定的曲线半径,如何画圆弧如图所示?
由于 Path.arcTo
所需的参数是 RectF
、startAngle
和 sweepAngle
,因此似乎没有简单的方法。
我目前的解决方案
我现在有了解决方案,我会在下面的答案中展示。
既然这么复杂,请问有没有更简单的方法解决?
1.找到圆心
这个可以用二元二次方程求解,如图:
虽然还有其他的解决办法,但是现在圆心的位置已经知道了。
2。计算起始角和扫描角
根据圆心,RectF
容易知道。
现在计算 startAngle
和 sweepAngle
.
通过几何方法,我们可以计算出startAngle
和sweepAngle
:
val startAngle = acos((x1 - x0) / r) / Math.PI.toFloat() * 180
val endAngle = acos((x2 - x0) / r) / Math.PI.toFloat() * 180
val sweepAngle = endAngle - startAngle
此时x1
为A点的x坐标,x2
为B点的x坐标,r
为圆弧的曲线半径. (有可能的结果,另一种是[-startAngle, startAngle - endAngle]
。根据实际情况选择一个。)
因此,我们获得了 Path.arcTo
方法所需的所有参数,现在我们可以绘制圆弧了。
3。科特林代码
帮助函数的全部代码:
/**
* Append the arc which is starting at ([x1], [y1]), ending at ([x2], [y2])
* and with the curve radius [r] to the path.
* The Boolean value [clockwise] shows whether the process drawing the arc
* is clockwise.
*/
@Throws(Exception::class)
private fun Path.arcFromTo(
x1: Float, y1: Float, x2: Float, y2: Float, r: Float,
clockwise: Boolean = true
) {
val c = centerPos(x1, y1, x2, y2, r, clockwise) // circle centers
// RectF borders
val left = c.x - r
val top = c.y - r
val right = c.x + r
val bottom = c.y + r
val startAngle = acos((x1 - c.x) / r) / Math.PI.toFloat() * 180
val endAngle = acos((x2 - c.x) / r) / Math.PI.toFloat() * 180
arcTo(
left, top, right, bottom,
if (clockwise) startAngle else -startAngle,
if (clockwise) endAngle - startAngle else startAngle - endAngle,
false
)
}
// use similar triangles to calculate circle center
@Throws(Exception::class)
private fun centerPos(
x1: Float, y1: Float, x2: Float, y2: Float, r: Float,
clockwise: Boolean
): Point {
val ab = ((x1 - x2).p2 + (y1 - y2).p2).sqrt
if (ab > r * 2) throw Exception("No circle fits the condition.")
val a = ab / 2
val oc = (r.p2 - a.p2).sqrt
val dx = (oc * (y2 - y1) / ab).absoluteValue.toInt()
val dy = (oc * (x2 - x1) / ab).absoluteValue.toInt()
val cx = ((x1 + x2) / 2).toInt()
val cy = ((y1 + y2) / 2).toInt()
return if (x1 >= x2 && y1 >= y2 || x1 <= x2 && y1 <= y2)
if (clockwise) Point(cx + dx, cy - dy) else Point(cx - dx, cy + dy)
else
if (clockwise) Point(cx - dx, cy - dy) else Point(cx + dx, cy + dy)
}
可能没有更简单的方法了。所有能做的就是通过几何方法改进您的解决方案。由于圆心总是在弦的垂直平分线上,所以不需要求解如此广义的方程
顺便说一句,你是如何定义的 Clockwise/Counter-clockwise 还不清楚。弧的缠绕方向应独立于节点放置(=A,B 的坐标)确定。
如下图所示,在A到B的直线路径上,中心O要放在右手边(CW)或左手边(CCW)。就这些了。
还有一些要改变的方面:
- startAngle最好用atan2()来计算。因为acos()在某些点上有奇点。
- 也可以用 asin() 计算 sweepAngle。
毕竟代码可以稍微简化如下。
@Throws(Exception::class)
private fun Path.arcFromTo2(
x1: Float, y1: Float, x2: Float, y2: Float, r: Float,
clockwise: Boolean = true
) {
val d = PointF((x2 - x1) * 0.5F, (y2 - y1) * 0.5F)
val a = d.length()
if (a > r) throw Exception()
val side = if (clockwise) 1 else -1
val oc = sqrt(r * r - a * a)
val ox = (x1 + x2) * 0.5F - side * oc * d.y / a
val oy = (y1 + y2) * 0.5F + side * oc * d.x / a
val startAngle = atan2(y1 - oy, x1 - ox) * 180F / Math.PI.toFloat()
val sweepAngle = side * 2.0F * asin(a / r) * 180F / Math.PI.toFloat()
arcTo(
ox - r, oy - r, ox + r, oy + r,
startAngle, sweepAngle,
false
)
}