在Android中是否有一种仅使用两点和曲线半径绘制圆弧的便捷方法?

问题描述

首先

我找了很久,我已经看到很多问题,包括两个:

How to draw Arc between two points on the Canvas?

How to draw a curved line between 2 points on canvas?

尽管它们看起来像是同一个问题,但我很确定它们相同。在第一个问题中,圆心是已知的,在第二个问题中,它绘制的是贝塞尔曲线而不是圆弧。

说明

现在我们有两个点AB,并且给定了曲线半径,如何绘制如图所示的圆弧?

image

由于 Path.arcTo 的必需参数是 RectFstartAnglesweepAngle,因此似乎没有什么简单的方法

我目前的解决方

我现在有了我的解决方案,我将在下面的答案中说明。

既然解决方案这么复杂,不知道有没有更简单的方法解决

解决方法

1.找到圆心

这可以通过二元二次方程求解,如图所示:

center

虽然还有其他的解决办法,反正现在圆心的位置是知道的。

2.计算起始角和扫描角

根据圆心,RectF很容易知道。 现在计算 startAnglesweepAngle

calculate angle

通过几何方法,我们可以计算出startAnglesweepAngle

    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. kotlin 代码

帮助功能的完整代码:

    /**
     * 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,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)
    }
,

可能没有更简单的方法。所能做的就是通过几何方法来完善您的解决方案。由于圆心总是在弦的垂直平分线上,所以不需要解这么广义的方程。

顺便说一下,您是如何定义顺时针/逆时针的还不清楚。弧的缠绕方向应独立于节点位置(=A,B 的坐标)确定。

如下图所示,在从 A 到 B 的直线路径上,中心 O 应放在右侧(CW)或左侧(CCW)。仅此而已。

Path.ArcTo with radius and end-points

还有一些需要改变的方面:

  1. 最好通过 atan2() 计算 startAngle。因为 acos() 在某些方面具有奇点。
  2. 也可以使用 asin() 计算sweepAngle。

毕竟代码可以稍微简化如下。

@Throws(Exception::class)
private fun Path.arcFromTo2(
    x1: 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
    )
}