Search code examples
androidandroid-canvasscale

Android Canvas ScaleFocus causing jumping position


I am working on a game that will be looking down onto a grid moving pieces around (similar to Chess). I want the user to be able to pan and zoom the map. I have been making transformations to a matrix, and then concatenating those changes to the canvas matrix. While it's mostly working, I have a weird bug:

After the initial scale, if I pinch to scale again, the screen jumps (it's as if a swiped somewhere else and it moved there instantly) and then scales smoothly. I have narrowed this behavior down to the scaleFocus variables of the scale function

canvas.scale(scaleFactor, scaleFactor, scaleFocusX, scaleFocusY);

If I set scaleFocusX and scaleFocusY to 0, the always use the origin as the scale focus and the jumping does not occur. However, using the origin is not practical as you scroll farther from it. Here is a summary of my code.

public void onDraw(Canvas canvas) {
  ...
  canvas.translate(-mPosX, -mPosY);
  canvas.scale(scaleFactor, scaleFactor, scaleFocusX, scaleFocusY);

  //Create an inverse of the tranformation matrix, to be used when determining click location.
  canvas.getMatrix().invert(canvasMatrix);
  ... }


public class MyOnScaleGestureListener extends
        ScaleGestureDetector.SimpleOnScaleGestureListener {

    @Override
    public boolean onScale(ScaleGestureDetector detector) {

        scaleFactor *= detector.getScaleFactor();
        // Don't let the object get too small or too large.
        scaleFactor = Math.max(MIN_SCALE, Math.min(scaleFactor, MAX_SCALE));
        return true;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        Point scaleFocus = calculateClickAbsoluteLocation
                (detector.getFocusX(), detector.getFocusY());
        scaleFocusX = scaleFocus.x;
        scaleFocusY = scaleFocus.y;
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {

    }


//This method will take point on the phone screen, and convert it to be
//a point on the canvas (since the canvas area is larger than the screen).
public Point calculateClickAbsoluteLocation(float x, float y) {
    Point result = new Point();
    float[] coordinates = {x, y};

    //MapPoints will essentially convert the click coordinates from "screen pixels" into
    //"canvas pixels", so you can determine what tile was clicked.
    canvasMatrix.mapPoints(coordinates);

    result.set((int)coordinates[0], (int)coordinates[1]);
    return result;
}

Solution

  • First of all, instead of using float[] coordinates = {x, y}; you better use PointF coordinates = new PointF(x, y);, then you can get x and y with coordinates.x and coordinates.y.

    Your problem is that when scaling you still handle the touch events (so touch is handled twice).

    We need a global variable that tells whether we are scaling or not: boolean scaling = false

    So, in OnTouchEvent you should check it:

    @Override
        public boolean onTouchEvent(@NonNull MotionEvent event) { 
            detector.onTouchEvent(event);
            if (detector.isInProgress()) 
                scaling = true;
    

    Release with: if (event.getAction() == MotionEvent.ACTION_UP) { scaling = false;

    And all of your code in onTouchEvent that works with clicks should be surrounded with

    if (!scaling) { ... }