在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?

虽然它们看起来像是同一个问题,但我很确定它们相同。第一题知道圆心,第二题画的是贝塞尔曲线,不是圆弧

描述

现在我们有AB两个点和给定的曲线半径,如何画圆弧如图所示?

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

我目前的解决方案

我现在有了解决方案,我会在下面的答案中展示。

既然这么复杂,请问有没有更简单的方法解决?

1.找到圆心

这个可以用二元二次方程求解,如图:

虽然还有其他的解决办法,但是现在圆心的位置已经知道了。

2。计算起始角和扫描角

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

通过几何方法,我们可以计算出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。科特林代码

帮助函数的全部代码:

    /**
     * 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)。就这些了。

还有一些要改变的方面:

  1. startAngle最好用atan2()来计算。因为acos()在某些点上有奇点。
  2. 也可以用 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
    )
}