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

时间:2016-07-16 22:33:24

标签: android canvas

我需要触摸画布上的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))缩放画布,则它不起作用。

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

3 个答案:

答案 0 :(得分:8)

更新,超级简单的例子:

在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的行是&#34;鉴于我们相对于原点进行缩放。&#34;也就是说我们的比例已经相对于原点。比例相对于原点,因为矩阵代码只是乘以x和y坐标。

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

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

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

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

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

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

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

您可以在此项目中检查已完成的实施(MIT许可证,我编写了相关部分): https://github.com/Embroidermodder/MobileViewer

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

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

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

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

private RectF viewPort;

Matrix viewMatrix;
Matrix invertMatrix;

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();
}

如果我们修改viewMatrix,我们可以使用它来派生端口,只需设置原始屏幕,然后使用Matrix将该矩形放在屏幕大小的屏幕上。

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).
}

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

        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();
}

答案 1 :(得分: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可以在别处使用来处理缩放(你负责处理代码),但是这段代码的作用是处理使指针坐标与缩放画布匹配的问题

答案 2 :(得分:1)

0

我刚发现有一个函数canvas.getClipBound(),它是一个表示可见视口的矩形,包括offsetx偏移y(分别是矩形的左边和顶部)和视口宽度和高度

只需调用这些函数,就画布而言,它会让你触摸和触摸