在缩放和平移画布之后,我如何获取与画布相关的触摸坐标?

8

在我移动和缩放画布后,我需要获取相对于画布的触摸X和Y坐标以检查碰撞等问题。

当我平移画布或以原点(0,0)为中心缩放画布时,我已经成功获取了触摸坐标,使用以下代码:

private float convertToCanvasCoordinate(float touchx, float touchy) {
    float newX=touchx/scale-translatex;
    float newY=touchy/scale-translatey
}

但是如果我按照另外一个点进行缩放,例如 canvas.scale(scale,scale,50,50),它就不起作用了。

我知道这样做不应该管用,但我就是找不到解决方法。我已经看过其他的问题,但没有一个答案讲述如何获取按照特定点进行缩放时的坐标。


你可以在这个答案中找到你要找的内容。https://dev59.com/iZLea4cB1Zd3GeqP5KqN#34598847 它是用javascript编写的,但数学部分是相同的。要在转换视图中找到一个点,请反转变换矩阵并将触摸坐标与inv矩阵相乘。它将处理所有情况,包括倾斜和剪切。 - Blindman67
代码不工作 :( ,似乎无法处理特定点的缩放。 - has19
这个问题已经在这里得到了解答:http://stackoverflow.com/questions/38061734/how-do-i-make-the-coordinates-of-motionevent-match-the-ones-of-a-scaled-canvas。 - Zoe stands with Ukraine
3个回答

17

在安卓中正确制作场景的最基本方法是使用矩阵来修改视图,使用该矩阵的逆来修改触摸操作。下面是一个简化的回答,非常简短。

public class SceneView extends View {
    Matrix viewMatrix = new Matrix(), invertMatrix = new Matrix();
    Paint paint = new Paint();
    ArrayList<RectF> rectangles = new ArrayList<>();
    RectF moving = null;

    public SceneView(Context context) { super(context); }
    public SceneView(Context context, AttributeSet attrs) { super(context, attrs); }
    public SceneView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //transform touch. (inverted matrix)
        event.transform(invertMatrix);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                moving = null;
                //collision detection
                for (RectF f : rectangles) {
                    if (f.contains(event.getX(), event.getY())) {
                        moving = f;
                        return true;
                    }
                }
                // adding arbitrary transforms.
                viewMatrix.postTranslate(50,50);
                viewMatrix.postScale(.99f,.99f);
                viewMatrix.postRotate(5);
                // inverse matrix is needed for touches.
                invertMatrix = new Matrix(viewMatrix);
                invertMatrix.invert(invertMatrix);
                break;
            case MotionEvent.ACTION_MOVE:
                if (moving != null) {
                    moving.set(event.getX() - 50, event.getY() - 50, event.getX() + 50, event.getY() + 50);
                }
                break;
            case MotionEvent.ACTION_UP:
                if (moving == null) {
                    rectangles.add(new RectF(event.getX() - 50, event.getY() - 50, event.getX() + 50, event.getY() + 50));
                }
                break;
        }
        invalidate();
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // transform the view by the matrix.
        canvas.concat(viewMatrix);
        // draw objects
        for (RectF f : rectangles) {
            canvas.drawRect(f,paint);
        }
    }

这个展示非常简约,但它显示了所有相关的方面。

  1. 移动视图
  2. 触摸修改
  3. 碰撞检测

每次您触摸屏幕,它会对角线移动,缩小并旋转(基本上是螺旋移动),并创建一个黑色矩形。如果您触摸矩形,则可以随心所欲地移动它们。当您单击背景时,将继续螺旋状移动视图并且掉落黑色矩形。

参见:https://youtu.be/-XSjanaAdWA


另一种方式行不通。理论上,您可以通过View类而不是在画布中转换我们想要的场景。这将使触摸事件发生在与屏幕相同的空间内。但是,Android将无效触摸事件发生在视图之外的位置。因此,在原始裁剪部分之外开始的MotionEvents将被丢弃。所以这是行不通的。您需要转换画布,并反向转换MotionEvents。


非常感谢您的快速回复,我非常感激。但是您的代码真的很大 :) ,我刚刚发现有一个函数canvas.getclipbound,它可以获取屏幕的可见视口和偏移量x和y,这解决了我的问题,请查看我的答案以获取更多详细信息。 - has19
重要的是顶部部分,你基本上可以将其缩小为两个矩阵,并在onDraw和onTouch事件中使用它们。 - Tatarize
应该说,这是我在Google Play上编写并发布的“Touch Embroidery Free”所使用的相同概念的修改版本。但是,重要的不仅仅是工作代码部分,更重要的是这些概念的重要性。基本上,您可以将矩阵应用于您绘制的画布上,然后在触摸事件上应用该矩阵的逆。只需这两行相关代码(以及设置矩阵的某些内容),所有工作都为您完成。您甚至可以扭曲视图或使整个视图围绕某个点以某个速度旋转,并使触摸事件与场景完美对齐。 - Tatarize
好的,将代码缩小到了必要的部分(然后添加了一些非常基本的非必要部分)。 - Tatarize
一般来说,这是正确的看待它们的方式。一旦你理解了将场景绘制转换为视图和触摸到视图的数学只是一个矩阵对象,基本上你可以用非常少的努力做任何事情。它大大简化了一切。我自己甚至编写了一个完整的小部件场景系统,就像Netbeans和其他系统的Visual Library API一样。因此,您可以获得一个基本上无限的工作领域,并且可以适当地操作任意数量的小部件,同时只需要几行代码即可获得功能丰富的世界。 - Tatarize
1
这个应该得到更多的赞,你刚刚帮我省了很多困惑。我一直在使用与画布相同的矩阵进行转换,甚至没有想到需要用到逆矩阵!真是救命稻草!我也没意识到 MotionEvent 有一个 Transform 方法,非常方便,我再也不需要 MapPoints 了! - Simon Jackson

4
 private float convertToCanvasXCoordinate(float touchx,float offsetx,float viewportVisibleWidth){
        float newx=(touchx*viewportVisibleWidth)/getWidth()+offsetx;
        return newx;
    }

    private float convertToCanvasYCoordinate(float touchy,float offsety,float viewportVisibleHeight){
        float newy=(touchy*viewportVisibleHeight)/getHeight()+offsety;
        return newy;
    }

我刚刚发现了一个函数canvas.getClipBound(),它返回一个矩形,代表可见的视口,包括offsetx和offsety(分别是矩形的左侧和顶部)以及视口的宽度和高度。

只需调用这些函数,就可以获得相对于画布的touchx和touchy。


我真的想不出来这个问题的答案,直到看到了你的帖子。谢谢伙计。 - SeanDp32
@has19 这里的getWidth()和getHeight()是什么,它们从哪里来? - zeeali
@zeeali 我认为它们是视图的宽度和高度,所以在这种情况下它们将是画布的尺寸。 - Robin

1
这应该会有帮助:

float px = e.getX() / mScaleFactorX;
float py = e.getY() / mScaleFactorY;
int ipy = (int) py;
int ipx = (int) px;
Rect r = new Rect(ipx, ipy, ipx+2, ipy+2);

而画布在哪里:

    final float scaleFactorX = getWidth()/(WIDTH*1.f);
    final float scaleFactorY = getHeight()/(HEIGHT*1.f);

    if(mScaleFactorX == INVALID){
        mScaleFactorX = scaleFactorX;
        mScaleFactorY = scaleFactorY;
    }

这是一种非常简单的方法,它有效地将onTouch坐标缩小到与画布相同的最小/最大值,从而使它们被缩放。不要使用getRawX和getRawY,因为如果您正在使用自定义布局并添加了视图和其他元素,那么它将返回错误的坐标。getX和getY返回针对您的视图缩放的准确坐标。
这非常简单,代码量不多。scaleFactor可以在其他地方用于处理缩放(您负责该代码),但是此代码的作用是解决获取指针坐标以匹配缩放画布的问题。

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