Search code examples
androidzoomingandroid-canvas

Correcting coordinates of touch events after zooming and paning canvas


When using scaling, MotionEvent coordinates can be corrected by dividing by the ScaleFactor.

Further, when scaling and paning, divide by scalefactor and subtract offset.

When dealing with zoom, however, it isn't as easy. Dividing does get the correct relative coordinates, but because pan is involved, 0 isn't 0. 0 can be -2000 in offset.

So how can I correct the TouchEvents to give the correct coordinates after zoom and pan?

Code:

Zoom:

class Scaler extends ScaleGestureDetector {
    public Scaler(Context context, OnScaleGestureListener listener) {
        super(context, listener);
    }

    @Override
    public float getScaleFactor() {
        return super.getScaleFactor();
    }
}
class ScaleListener implements ScaleGestureDetector.OnScaleGestureListener{

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        scaleFactor *= detector.getScaleFactor();

        if(scaleFactor > 2) scaleFactor = 2;
        else if(scaleFactor < 0.3f) scaleFactor = 0.3f;
        scaleFactor = ((float)((int)(scaleFactor * 100))) / 100;//jitter-protection
        scaleMatrix.setScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());
        return true;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {return true;}

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {

        System.out.println("ScaleFactor: " + scaleFactor);
    }
}

TouchEvent:

@Override
public boolean onTouchEvent(MotionEvent ev) {

    int pointers = ev.getPointerCount();

    if(pointers == 2 ) {
        zoom = true;
        s.onTouchEvent(ev);
    }else if(pointers == 1 && zoom){
        if(ev.getAction() == MotionEvent.ACTION_UP)
            zoom = false;
        return true;
    }else {

        if (ev.getAction() == MotionEvent.ACTION_DOWN) {

            //scaled physical coordinates
            x = ev.getX() /*/ mScaleFactorX*/;//unscaled
            y = ev.getY() /*/ mScaleFactorY*/;
            sx = ev.getX() / scaleFactor;//scaled
            sy = ev.getY() / scaleFactor;
            //////////////////////////////////////////
            tox = toy = true;


        } else if (ev.getAction() == MotionEvent.ACTION_UP) {

            if (tox && toy) {
                x = ev.getX() /*/ mScaleFactorX*/;
                y = ev.getY() /*/ mScaleFactorY*/;
                sx = ev.getX() / scaleFactor;
                sy = ev.getY() / scaleFactor;
                System.out.println("XY: " + sx + "/" + sy);
                Rect cursor = new Rect((int) x, (int) y, (int) x + 1, (int) y + 1);
                Rect scaledCursor = new Rect((int)sx, (int)sy, (int)sx+1, (int)sy+1);
                ...
            }

        } else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
            //This is where the pan happens. 

            float currX = ev.getX() / scaleFactor;
            float currY = ev.getY() / scaleFactor;

            float newOffsetX = (sx - currX),
                    newOffsetY = (sy - currY);

            if (newOffsetY < Maths.convertDpToPixel(1, c) && newOffsetY > -Maths.convertDpToPixel(1, c))
                newOffsetY = 0;
            else tox = false;

            if (newOffsetX < Maths.convertDpToPixel(1, c) && newOffsetX > -Maths.convertDpToPixel(1, c))
                newOffsetX = 0;
            else toy = false;
            this.newOffsetX = newOffsetX;
            this.newOffsetY = newOffsetY;
            offsetX += newOffsetX;
            offsetY += newOffsetY;
            sx = ev.getX() / scaleFactor;
            sy = ev.getY() / scaleFactor;
        }

    }
    return true;

}

Implementation of the zooming matrix:

Matrix scaleMatrix = new Matrix();
public void render(Canvas c) {
    super.draw(c);

    if (c != null) {
        backgroundRender(c);
        c.setMatrix(scaleMatrix);
        //Example rendering:
        c.drawRect(0 - offsetX,0 - offsetY,10 - offsetX,10 - offsetY,paint);
        c.setMatrix(null);//null the matrix to allow for unscaled rendering after this line. For UI objects.

    }
}

What the issue is, is that when zooming 0 shifts but the coordinates of the objects does not. Meaning objects rendered at e.g. -2500, -2500 will appear to be rendered at over 0,0. Their coordinates are different from the TouchEvent. So how can I correct the touch events?

What I have tried:

This causes laggy zoom and the objects flying away. ev = MotionEvent in onTouchEvent. Doesn't correct the coordinates

Matrix invert = new Matrix(scaleMatrix);
invert.invert(invert);
ev.transform();

This doesn't work because the coordinates are wrong compared to objects. Objects with coordinates < 0 show over 0 meaning MotionEvents are wrong no matter what.

int sx = ev.getX() / scaleFactor;//same with y, but ev.getY()

Solution

  • Found a solution after doing a ton more research

    Whenever getting the scaled coordinates, get the clipBounds of the canvas and add the top and left coordinates to X/Y coordinates:

    sx = ev.getX() / scaleFactor + clip.left;
    sy = ev.getY() / scaleFactor + clip.top ;
    

    clip is a Rect defined as the clipBounds of the Canvas.

    public void render(Canvas c) {
            super.draw(c);
    
        if (c != null) {
            c.setMatrix(scaleMatrix);
            clip = c.getClipBounds();
            (...)
        }
    }