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

has*_*s19 3 android canvas

我需要相对于画布获得触摸x和y,以便在我移动并缩放画布后检查碰撞和类似的事情.

每当我翻译画布时,我已经设法获得了触摸坐标,或者使用以下代码在原点(0,0)周围缩放它:

private float convertToCanvasCoordinate(float touchx, float touchy) {
    float newX=touchx/scale-translatex;
    float newY=touchy/scale-translatey
}
Run Code Online (Sandbox Code Playgroud)

但是,如果我将画布围绕另一个点进行缩放,例如canvas.scale(scale,scale,50,50),它就不起作用了.

我知道它应该不起作用,但我无法弄清楚如何解决它.我已经查看了其他问题,但如果按照特定点进行缩放,没有一个答案谈到如何获得坐标.

Tat*_*ize 10

更新,超级简单的例子:

在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);
        }
    }
Run Code Online (Sandbox Code Playgroud)

这是相当简约的,但它显示了所有相关方面.移动视图,触摸修改,碰撞检测.每次触摸屏幕时,它都会对角移动,缩小和旋转(基本上以螺旋方式移动),并创建一个黑色矩形.如果触摸矩形,可以将它们移动到心脏的内容.单击背景时,会使视图更加螺旋形,从而丢弃黑色矩形.

请参阅:https://youtu.be/-XSjanaAdWA


这里另一个答案中的一行是"假设我们相对于原点进行缩放." 也就是说我们的规模已经相对于原点.比例是相对于原点的,因为矩阵代码只是乘以x和y坐标.

当事物相对于其他任何东西缩放时,它们实际上是翻译,缩放,翻译回来的.这就是数学必须如何.如果我们将比例应用于画布,则该比例已经相对于原点.有时您可以相对于某个点进行缩放,但这只是矩阵数学.

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

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

scalechange = newscale - oldscale;
offsetX = -(zoomPointX * scalechange);
offsetY = -(zoomPointY * scalechange);
Run Code Online (Sandbox Code Playgroud)

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


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

基本上,矩阵数学的工作方式必须以相反的顺序应用反向运算才能得到反演.虽然这就是我们倾向于为矩阵变换得到反转矩阵的原因.在Android中,我们为我们完成了很多工作.如果你了解正在发生的事情,我们可以解决所有这些问题,而且真的不必担心这些问题.


你可以在这个项目中检查一个做得好的实现(麻省理工学院许可证,我编写了相关部分):https: //github.com/Embroidermodder/MobileViewer

Matrix可以非常重要地修改MotionEvent类.矩阵可以反转.如果我们理解这一点,我们就会明白所有工作都是为我们完成的.我们简单地采用我们制作的任何矩阵,并将其应用于View.我们得到该矩阵的逆矩阵,并将这个倒置矩阵应用于触摸事件,因为它们发生了. - 现在我们的触摸事件发生在场景空间中.

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

另一种方法是采用我们想要的场景并通过View类而不是画布转换它.这将使触摸事件发生在与屏幕相同的空间中.但Android将使视图外部发生的触摸事件无效,因此将丢弃在视图的原始剪切部分之外开始的MotionEvent.所以这是一个非首发.您想翻译画布.并且计数器翻译MotionEvent.

我们需要一些课程.我们可以定义一个视图端口,并使用它来构建我们的矩阵:

private RectF viewPort;

Matrix viewMatrix;
Matrix invertMatrix;
Run Code Online (Sandbox Code Playgroud)

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

这里我们从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();
}
Run Code Online (Sandbox Code Playgroud)

如果我们修改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]);
}
Run Code Online (Sandbox Code Playgroud)

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

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

public void calculateInvertMatrix() {
    invertMatrix = new Matrix(viewMatrix);
    invertMatrix.invert(invertMatrix);
}
Run Code Online (Sandbox Code Playgroud)

然后我们将这些矩阵应用于画布,将反转矩阵应用于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);
}
Run Code Online (Sandbox Code Playgroud)

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).
}
Run Code Online (Sandbox Code Playgroud)

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

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

它可以节省在画布中堆叠的状态数量.然后在委托谁知道什么之后,如果某个类调用.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();
}
Run Code Online (Sandbox Code Playgroud)