缩放和翻译后如何获得相对于画布的触摸坐标?

Posted

技术标签:

【中文标题】缩放和翻译后如何获得相对于画布的触摸坐标?【英文标题】:How do I get touch coordinates with respect to canvas after scaling and translating? 【发布时间】:2016-11-19 20:28:03 【问题描述】:

在我移动和缩放画布后,我需要获取与画布相关的触摸 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))缩放画布,它就不起作用。

我知道它不应该起作用,但我就是不知道如何解决它。我已经看过其他问题,但没有一个答案谈到如果我根据特定点进行缩放,如何获得坐标。

【问题讨论】:

您可以在此答案中找到您要查找的内容。 ***.com/a/34598847/3877726 它在 javascript 中,但数学是相同的。要在变换后的视图中找到一个点,反转变换矩阵并将触摸坐标与 inv 矩阵相乘。它将处理所有情况,包括倾斜和剪切。 代码不起作用:(,它似乎无法在特定点处理缩放 这里已经有答案***.com/questions/38061734/… 【参考方案1】:

更新,超级简单的例子:

android 中正确制作场景的最基本方法是使用矩阵来修改视图,并使用该矩阵的逆矩阵来修改您的触摸。这是一个简化的答案。保持很短。

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) 
        event.transform(invertMatrix);
        switch (event.getAction()) 
            case MotionEvent.ACTION_DOWN:
                moving = null;
                for (RectF f : rectangles) 
                    if (f.contains(event.getX(), event.getY())) 
                        moving = f;
                        return true;
                    
                
                viewMatrix.postTranslate(50,50);
                viewMatrix.postScale(.99f,.99f);
                viewMatrix.postRotate(5);
                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) 
        canvas.concat(viewMatrix);
        for (RectF f : rectangles) 
            canvas.drawRect(f,paint);
        
    

这是相当简约的,但它显示了所有相关方面。移动视图、触摸修改、碰撞检测。每次触摸屏幕时,它都会沿对角线移动、缩小和旋转(基本上以螺旋方式移动),并创建一个黑色矩形。如果您触摸矩形,您可以随意移动它们。当您单击背景时,视图会呈螺旋状,会掉落黑色矩形。

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


other answer 中的行是“鉴于我们正在相对于原点进行缩放。”也就是说,我们的比例已经相对于原点。比例是相对于原点的,因为矩阵代码只是将 x 和 y 坐标相乘。

当事物相对于其他任何事物进行缩放时,它们实际上会被平移、缩放、平移。数学就是这样。如果我们对画布应用比例,则该比例已经相对于原点。有时您可以相对于一个点进行缩放,但这只是矩阵数学。

在大多数这样的视图实现中,通常通过放大来对一个点执行缩放。然后平移给定的视口。这是因为视口是类似的缩放矩形。

所以我们要做的是。弄清楚我们需要平移多少才能将点保持在同一位置,包括相对于视口的预缩放视图和缩放后视图。该代码是:

scalechange = newscale - oldscale;
offsetX = -(zoomPointX * scalechange);
offsetY = -(zoomPointY * scalechange);

然后我们做 canvas.translate(offsetX,OffsetY);


这里的问题是,如何为给定的 Android 触摸事件将其翻译回来。为此,我们将应用于视图的所有相同操作以相反的顺序应用于触摸位置。

基本上,矩阵数学的工作方式是您必须以相反的顺序应用反向运算才能获得反转。虽然这就是为什么我们倾向于为我们的矩阵转换获得倒置矩阵。在 Android 中,我们为我们做了很多事情。如果您了解正在发生的事情,我们可以解决所有这些问题,而且真的不必担心这些。


你可以在这个项目中检查一个很好的实现(MIT 许可证,我对相关部分进行了编码): https://github.com/Embroidermodder/MobileViewer

MotionEvent 类可以通过 Matrix 进行非常重要的修改。并且矩阵可以倒置。如果我们理解这一点,我们就会明白所有的工作都是为我们完成的。我们只需要我们制作的任何矩阵,并将其应用于视图。我们得到该矩阵的逆矩阵,并将该逆矩阵应用于触摸事件,当它们发生时。 -- 现在我们的触摸事件发生在场景空间中。

如果我们想要某些东西的位置,我们也可以调用 matrix.mapPoints() 让我们根据需要简单地来回转换它们。

另一种方法是获取我们想要的场景并通过 View 类而不是在画布中进行转换。这将使触摸事件发生在与屏幕相同的空间中。但是 Android 会取消在视图之外发生的触摸事件,因此在视图的原始剪辑部分之外开始的 MotionEvents 将被丢弃。所以这是一个非首发。你想翻译画布。并计数器翻译 MotionEvent。

我们需要几节课。我们可以定义一个视口,并使用它来构建我们的矩阵:

private RectF viewPort;

Matrix viewMatrix;
Matrix invertMatrix;

当然不需要 viewPort,但从概念上讲它可以提供很大帮助。

这里我们从视口构建矩阵。也就是说,无论我们将其设置为哪个矩形,它都是我们可以查看的场景部分。

public void calculateViewMatrixFromPort() 
    float scale = Math.min(_height / viewPort.height(), _width / viewPort.width());
    viewMatrix = new Matrix();
    if (scale != 0) 
        viewMatrix.postTranslate(-viewPort.left, -viewPort.top);
        viewMatrix.postScale(scale, scale);

    
    calculateInvertMatrix();

如果我们修改 viewMatrix,我们可以使用它来导出端口,只需设置原始屏幕,然后使用 Matrix 将 Rectangle 设置为屏幕大小的屏幕。

public void calculateViewPortFromMatrix() 
    float[] positions = new float[] 
            0,0,
            _width,_height
    ;
    calculateInvertMatrix();
    invertMatrix.mapPoints(positions);
    viewPort.set(positions[0],positions[1],positions[2],positions[3]);

这假设我们有我们正在使用的视图的 _width 和 _height,我们可以简单地平移和缩放视图框。如果你想要一些更高级的东西,比如对屏幕应用旋转,你需要使用 4 个点,每个角 1 个,然后将矩阵应用于这些点。但是,你基本上可以很容易地添加这些东西,因为我们不直接处理繁重的工作,而是严重依赖矩阵。

我们还需要能够计算倒置矩阵,以便我们可以反转 MotionEvents:

public void calculateInvertMatrix() 
    invertMatrix = new Matrix(viewMatrix);
    invertMatrix.invert(invertMatrix);

然后我们将这些矩阵应用到画布上,并将反转矩阵应用到 MotionEvent

@Override
public boolean onTouchEvent(MotionEvent event) 
    //anything happening with event here is the X Y of the raw screen event.

    event.offsetLocation(event.getRawX()-event.getX(),event.getRawY()-event.getY()); //converts the event.getX() to event.getRaw() so the title bar doesn't fubar.

    //anything happening with event here is the X Y of the raw screen event, relative to the view.
    if (rawTouch(this,event)) return true;

    if (invertMatrix != null) event.transform(invertMatrix);
    //anything happening with event now deals with the scene space.

    return touch(this,event);

MotionEvent 类中的一个显着缺陷是 getRawX() 和 getRawY()(它们是屏幕上的实际原始触摸而不是视图中的触摸,只允许您进行单指定位。真的这是相当严重的,但我们可以简单地为 MotionEvent 设置一个偏移量,以便 getX(3) 和各个点在 getRawX(3) 所在的位置正确重叠。这将让我们正确处理标题栏等,因为 MotionEvents 是技术上相对于视图而言,我们需要它们相对于屏幕(有时它们是相同的,例如全屏模式)。

现在,我们完成了。

所以我们可以应用这些矩阵并删除它们并非常轻松地切换我们的上下文,而无需知道它们是什么,或者我们当前的视图正在查看什么,并正确获取所有不同的触摸事件和触摸事件的各种指针计数。

我们还可以在不同的翻译中绘制我们的东西。例如,如果我们想要一个不随场景移动,而是相对于屏幕移动的工具。

@Override
public void onDraw(Canvas canvas) 
        //Draw all of our non-translated stuff. (under matrix bit).
        canvas.save();
        if (viewMatrix != null) canvas.setMatrix(viewMatrix);
        //Draw all of our translated stuff.
        canvas.restore();
        //Draw all of our non-translated stuff. (over matrix bit).

最好保存并恢复画布,这样我们应用的矩阵就会被移除。特别是如果将绘制事件传递给不同的类会使事情变得复杂。有时这些类可能会在画布中添加一个矩阵,这就是视图类源代码本身看起来有点像的原因:

        int level = canvas.getSaveCount();
        canvas.save();
        //does the drawing in here, delegates to other draw routines.
        canvas.restoreToCount(level);

它节省了画布中堆叠了多少状态的计数。然后在委派给谁知道什么之后,它会恢复到那个级别,以防某些类调用了.save() 但没有调用 restore()。您可能也想这样做。

如果我们想要完整的平移和缩放代码,我们也可以这样做。在将缩放点设置为各种触摸之间的中点等方面有一些技巧。


float dx1;
float dy1;
float dx2;
float dy2;
float dcx;
float dcy;

@Override
public boolean rawTouch(View drawView, MotionEvent event) 
//I want to implement the touch events in the screen space rather than scene space.
//This does pinch to zoom and pan.
    float cx1 = event.getX();
    float cy1 = event.getY();
    float cx2 = Float.NaN, cy2 = Float.NaN;
    float px = cx1;
    float py = cy1;
    if (event.getPointerCount() >= 2) 
        cx2 = event.getX(1);
        cy2 = event.getY(1);
        px = (cx1 + cx2) / 2;
        py = (cy1 + cy2) / 2;
    


    switch (event.getActionMasked()) 
        case MotionEvent.ACTION_MOVE:
            float deltascale = (float) (distance(cx1,cy1,cx2,cy2) / distance(dx1,dy1,dx2,dy2));
            float dpx = px-dcx;
            float dpy = py-dcy;
            if (!Float.isNaN(dpx)) pan(dpx, dpy);
            if (!Float.isNaN(deltascale)) scale(deltascale, px, py);
            view.invalidate();
            break;
        default:
            cx1 = Float.NaN;
            cy1 = Float.NaN;
            cx2 = Float.NaN;
            cy2 = Float.NaN;
            px = Float.NaN;
            py = Float.NaN;
            break;
    
    dx1 = cx1;
    dy1 = cy1;
    dx2 = cx2;
    dy2 = cy2;
    dcx = px;
    dcy = py;
    return true;


@Override
public boolean touch(View drawView, MotionEvent event) 
    //if I wanted to deal with the touch event in scene space.
    return false;


public static double distance(float x0, float y0, float x1, float y1) 
    return Math.sqrt(distanceSq(x0, y0, x1, y1));

public static float distanceSq(float x0, float y0, float x1, float y1) 
    float dx = x1 - x0;
    float dy = y1 - y0;
    dx *= dx;
    dy *= dy;
    return dx + dy;




public void scale(double deltascale, float x, float y) 
    viewMatrix.postScale((float)deltascale,(float)deltascale,x,y);
    calculateViewPortFromMatrix();


public void pan(float dx, float dy) 
    viewMatrix.postTranslate(dx,dy);
    calculateViewPortFromMatrix();

【讨论】:

非常感谢您的快速响应,我真的很感激,但您的代码真的很大:),我刚刚发现有一个函数 canvas.getclipbound 可以让我看到屏幕以及解决我的问题的偏移量 x 和 y,请查看我的答案以获取更多详细信息 这是最重要的部分,您基本上可以将其缩小到两个矩阵。并在 onDraw 和 onTouch 事件上使用它们。 应该是,它是我用于“Touch Embroidery Free”的相同概念的修改版本,我编写并放在谷歌播放。但比工作代码位更重要的是概念的重要性。基本上,您可以将矩阵应用于您绘制的画布,然后将该矩阵的逆矩阵应用于触摸事件。只需这两行相关的代码,(以及设置矩阵的东西)所有工作就为您完成了。您可以从字面上倾斜视图,或者使整个视图围绕某个点以某种速度旋转,并使触摸事件与场景完美对齐。 好的,将代码缩减到基本位(然后添加一些非常基本的非基本位)。 这应该得到更多的支持,你给我省了很多麻烦。我正在使用与画布相同的矩阵进行转换,甚至不认为我需要 INVERSE!救生员!也没有意识到 MotionEvent 有一个 Transform 方法,非常方便,我没有更多的 MapPoints!【参考方案2】:

这应该会有所帮助:

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 可用于其他地方来处理缩放(您负责该代码),但此代码的作用是处理获取指针坐标以匹配缩放画布的问题

【讨论】:

【参考方案3】:
 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 偏移量 y(分别为矩形的左侧和顶部)以及视口的宽度和高度

只需调用这些函数,它就会让你在画布方面变得触手可及

【讨论】:

直到我看到你的帖子,我才明白这一点。谢谢队友 @has19 这里的getWidth()和getHeight()是什么,从哪里来的? @zeeali 我认为它们是视图的宽度和高度,所以在这种情况下它们将是画布大小。

以上是关于缩放和翻译后如何获得相对于画布的触摸坐标?的主要内容,如果未能解决你的问题,请参考以下文章

捏缩放(绘图线)Android Canvas后获取正确的坐标

缩放和平移画布后鼠标坐标不匹配

JS中canvas画布绘制中如何实现缩放,位移,旋转

javascript 代码技巧 —— javascript获取坐标/滚动/宽高/距离

javascript 代码技巧 —— javascript获取坐标/滚动/宽高/距离

如何获取鼠标单击画布元素的坐标? [复制]