如何从Android画布(Canvas)中获取触摸的形状(弧形)?

5
我正在构建一个自定义视图。
它有4个弧。
我在onDraw()期间使用RectF形状绘制弧。
// arcPaint is a Paint object initialized
// in the View's constructor
arcPaint.setStrokeWidth(strokeWidth);
arcPaint.setColor(arcColor);

// Draw arcs, arc1, arc2, arc3 & arc4 are
// measured and initialized during onMeasure()
canvas.drawArc(arc1, startAngle, arc1Angle, false, arcPaint);
canvas.drawArc(arc2, startAngle, arc2Angle, false, arcPaint);
canvas.drawArc(arc3, startAngle, arc3Angle, false, arcPaint);
canvas.drawArc(arc4, startAngle, arc4Angle, false, arcPaint);

因此,结果是:

enter image description here

如果我使用以下代码使用canvas.drawRect()绘制RectF对象:
canvas.drawRect(arc1, arcPaint);
canvas.drawRect(arc2, arcPaint);
canvas.drawRect(arc3, arcPaint);
canvas.drawRect(arc4, arcPaint);

结果是这个:

enter image description here

将所有图形,包括弧和矩形,绘制在一起,得到这个:

enter image description here

我知道我可以重写 onTouchEvent() 方法并获取X、Y坐标,但我不知道每个坐标与画布中绘制的每个形状之间的关系。
我想做的是在 canvas 中检测当一个弧被触摸时,而不是一个矩形,我该怎么做?
编辑:
通过“不是一个矩形”,我的意思是,我不想检测用户触摸这些区域(矩形的角落)的时候:

enter image description here


请查看MikeM的编辑。 - Jorge E. Hernández
是的,矩形始终是正方形。 有点像,弧将是开放的(270度)。 是的,我总是会画出270度的弧。 - Jorge E. Hernández
啊,好的,正方形。这样就简单多了。首先,你有中心点,或者至少可以从RectF方法centerX()/centerY()获取它。然后,你可以用基本的距离公式计算出到触摸点的距离:distance = sqrt(dx² + dy²)。然后,如果该距离与弧的半径之差的绝对值小于等于Paint笔画宽度的一半,则触摸点在该弧上。明白我的意思吗?应该很容易检查触摸是否在开放的90°内,并在那里忽略它。 - Mike M.
dx²dy² 中的 d 是什么? - Jorge E. Hernández
我正在尝试。我会及时告诉你的;-) - Jorge E. Hernández
显示剩余11条评论
1个回答

5

这基本上归结为比较该图形中心点到触摸点之间的距离。由于包围矩形始终是正方形,因此其中一个距离实际上是固定的,这使得计算更加容易。

这里的一般方法是计算中心点到触摸点的距离,并检查该距离与弧的半径之差是否在半个描边宽度以内。

private static boolean isOnRing(MotionEvent event, RectF bounds, float strokeWidth) {
    // Figure the distance from center point to touch point.
    final float distance = distance(event.getX(), event.getY(),
                                    bounds.centerX(), bounds.centerY());

    // Assuming square bounds to figure the radius.
    final float radius = bounds.width() / 2f;

    // The Paint stroke is centered on the circumference,
    // so the tolerance is half its width.
    final float halfStrokeWidth = strokeWidth / 2f;

    // Compare the difference to the tolerance.
    return Math.abs(distance - radius) <= halfStrokeWidth;
}

private static float distance(float x1, float y1, float x2, float y2) {
    return (float) Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
}

onTouchEvent()方法中,只需使用MotionEvent、弧形边界的RectFPaint的描边宽度调用isOnRing()。例如:
@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            if (isOnRing(event, arc1, arcPaint.getStrokeWidth())) {
                // Hit.
            }
            break;
            ...
    }
    ...
}

这本身只能确定触点是否在半径和线宽描述的完整圆环上。也就是说,它没有考虑弧线中的“间隙”。如果需要处理这个问题,有多种方法。

如果你打算让起始角度和扫描角度与x/y轴平齐,最简单的方法可能是检查触点和中心点的坐标差异的符号。大体上来说,如果触摸点x减去中心点x是正数,那么你在右边;如果是负数,那么你在左边。同样地,计算顶部或底部,可以确定触摸发生在哪个象限,以及是否忽略事件。

final float dx = event.getX() - arc1.centerX();
final float dy = event.getY() - arc1.centerY();

if (dx > 0) {
    // Right side
}
else {
    // Left side
}
...

然而,如果任意一个角度不与轴线重合,那么数学计算就会稍微复杂一些。例如:

private static boolean isInSweep(MotionEvent event, RectF bounds,
                                 float startAngle, float sweepAngle) {
    // Figure atan2 angle.
    final float at =
        (float) Math.toDegrees(Math.atan2(event.getY() - bounds.centerY(),
                                          event.getX() - bounds.centerX()));

    // Convert from atan2 to standard angle.
    final float angle = (at + 360) % 360;

    // Check if in sweep.
    return angle >= startAngle && angle <= startAngle + sweepAngle;
}

onTouchEvent()中的if语句将被修改以包含两个检查。

if (isOnRing(event, arc1, arcPaint.getStrokeWidth()) &&
    isInSweep(event, arc1, startAngle, arc1Angle)) {
    // Hit.
}

这些方法可以很容易地合并为一个,但出于清晰起见,它们保持分开。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接