Search code examples
androidcanvas

How do I get touch coordinates with respect to canvas after scaling and translating?


I need to get the touch x and y with respect to the canvas to check for collisions and things like that after I have moved and scaled the canvas.

I already managed to get the touch coordinate whenever I translate the canvas or scale it around the origin (0,0) by using the following code:

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

But if I scale the canvas around another point like for example canvas.scale(scale,scale,50,50), it doesn't work .

I know it shouldn't work but I just couldn't figure out how to solve it. I already looked at other questions but none of the answers talks about how to get the coordinate if I scale according to a specific point.


Solution

  • The most basic way to properly do a scene in android is to use a matrix to modify the view and the inverse of that matrix to modify your touches. Here's a simplified answer. Kept very short.

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

    This is rather minimalist, but it shows all the relevant aspects.

    1. Moving the view
    2. Touch modification
    3. Collision detection.

    Each time you touch the screen it will move diagonally, zoomout, and rotate (basically moves in a spiral), and create a black rectangle. If you touch the rectangles you can move them around to your heart's content. When you click the background, more spiraling the view, dropping black rectangles.

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


    The other way does not work. You could, in theory, take the scene we want and convert that via the View class rather than in the canvas. This would make the touch events occur in the same space as the screen. But Android will void out touch events that occur outside of the view, So MotionEvents that begin outside of the original clipped part of the view will be discarded. So this is a non-starter. You want to transform the canvas, and counter transform the MotionEvents.