22

I am trying to implement pinch zoom and drag using Android's gesture listener and scale listener. The problem is that when I perform pinch zoom, the image (which I am trying to zoom) bounces to a particular location. Also the zoom position is not centered. The following code demonstrates what I am trying to achieve. Any idea why the image is jumping (and how to correct it) ?

public class CustomView extends View {
    Bitmap image;
    int screenHeight;
    int screenWidth;
    Paint paint;
    GestureDetector gestures;
    ScaleGestureDetector scaleGesture;
    float scale = 1.0f;
    float horizontalOffset, verticalOffset;

    int NORMAL = 0;
    int ZOOM = 1;
    int DRAG = 2;
    boolean isScaling = false;
    float touchX, touchY;
        int mode = NORMAL;

    public CustomView(Context context) {
    super(context);
            //initializing variables
    image = BitmapFactory.decodeResource(getResources(),
            R.drawable.image_name);
            //This is a full screen view
    screenWidth = getResources().getDisplayMetrics().widthPixels;
    screenHeight = getResources().getDisplayMetrics().heightPixels;
    paint = new Paint();
    paint.setAntiAlias(true);
    paint.setFilterBitmap(true);
    paint.setDither(true);
    paint.setColor(Color.WHITE);

    scaleGesture = new ScaleGestureDetector(getContext(),
            new ScaleListener());
    gestures = new GestureDetector(getContext(), new GestureListener());
    mode = NORMAL;
    initialize();
}

//Best fit image display on canvas 
    private void initialize() {
    float imgPartRatio = image.getWidth() / (float) image.getHeight();
    float screenRatio = (float) screenWidth / (float) screenHeight;

    if (screenRatio > imgPartRatio) {
        scale = ((float) screenHeight) / (float) (image.getHeight()); // fit height
        horizontalOffset = ((float) screenWidth - scale
                * (float) (image.getWidth())) / 2.0f;
        verticalOffset = 0;
    } else {
        scale = ((float) screenWidth) / (float) (image.getWidth()); // fit width
        horizontalOffset = 0;
        verticalOffset = ((float) screenHeight - scale
                * (float) (image.getHeight())) / 2.0f;
    }
        invalidate();
}

@Override
protected void onDraw(Canvas canvas) {
    canvas.save();
    canvas.drawColor(0, Mode.CLEAR);
    canvas.drawColor(Color.WHITE);
    if(mode == DRAG || mode == NORMAL) {
        //This works perfectly as expected
        canvas.translate(horizontalOffset, verticalOffset);
        canvas.scale(scale, scale);
        canvas.drawBitmap(image, getMatrix(), paint);
    }
    else if (mode == ZOOM) {
        //PROBLEM AREA - when applying pinch zoom,
        //the image jumps to a position abruptly
        canvas.scale(scale, scale, touchX, touchY);
        canvas.drawBitmap(image, getMatrix(), paint);
    }
    canvas.restore();
}

public class ScaleListener implements OnScaleGestureListener {
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        float scaleFactorNew = detector.getScaleFactor();
        if (detector.isInProgress()) {
            touchX = detector.getFocusX();
            touchY = detector.getFocusY();
            scale *= scaleFactorNew;
            invalidate(0, 0, screenWidth, screenHeight);
        }
        return true;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        isScaling = true;
        mode=ZOOM;
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
        mode = NORMAL;
        isScaling = false;
    }

}

public class GestureListener implements GestureDetector.OnGestureListener,
        GestureDetector.OnDoubleTapListener {

    @Override
    public boolean onDown(MotionEvent e) {
        isScaling = false;
        return true;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2,
            float distanceX, float distanceY) {
        if (!isScaling) {
            mode = DRAG;
            isScaling = false;
            horizontalOffset -= distanceX;
            verticalOffset -= distanceY;
            invalidate(0, 0, screenWidth, screenHeight);
        } else {
            mode = ZOOM;
            isScaling = true;
        }
        return true;
    }
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    scaleGesture.onTouchEvent(event);
    gestures.onTouchEvent(event);
    return true;
}
}

Thanks in advance.

chochim
  • 1,710
  • 5
  • 17
  • 30
  • if you don't use the DRAG mode before you use the ZOOM mode - meaning, ZOOM is the 1st thing you do before any translations are applied, does the image still jump? – Gil Moshayof Oct 21 '13 at 00:13
  • Yes the image still jumps. – chochim Oct 21 '13 at 02:08
  • shouldn't the horizontal / vertical offset be applied even when you're scaling? have you tried applying these while scaling? – Gil Moshayof Oct 21 '13 at 02:25
  • @GilMoshayof - The horizontal and vertical scaling should be applied during scaling as well. But that does not make any difference - the image still jumps. It seems that I need to fiddle with the scaling pivot points. A similar question (albeit with a much cleaner implementation) is also asked here: http://stackoverflow.com/questions/19471356/canvas-scalescale-scale-px-py-jerks-to-a-new-position As per the answer provided in this question, the pivots around which we are scaling need to be adjusted. I'm still scratching my head in vain to calculate the pivot coordinates correctly. – chochim Oct 21 '13 at 02:31
  • how about trying to set the touchX / touchY in your onScaleBegin? Does that change anything? – Gil Moshayof Oct 21 '13 at 03:09
  • @GilMoshayof - Didnt help :( – chochim Oct 21 '13 at 20:19

1 Answers1

41

I have implemented this behaviour, and I used a matrix to handle all the zooming and scrolling (and rotation, in my case). It makes for neat code and works like clockwork.

Store a Matrix as a class member:

Matrix drawMatrix;

Edit: Store old focus point, used to get the focus shift during scaling.

float lastFocusX;
float lastFocusY;

Edit: Set lastFocus variables in onScaleBegin

@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
    lastFocusX = detector.getFocusX();
    lastFocusY = detector.getFocusY();
    return true;
}

Replace your onScale:

@Override
public boolean onScale(ScaleGestureDetector detector) {
    Matrix transformationMatrix = new Matrix();
    float focusX = detector.getFocusX();
    float focusY = detector.getFocusY();

    //Zoom focus is where the fingers are centered, 
    transformationMatrix.postTranslate(-focusX, -focusY);

    transformationMatrix.postScale(detector.getScaleFactor(), detector.getScaleFactor());

/* Adding focus shift to allow for scrolling with two pointers down. Remove it to skip this functionality. This could be done in fewer lines, but for clarity I do it this way here */
    //Edited after comment by chochim
    float focusShiftX = focusX - lastFocusX;
    float focusShiftY = focusY - lastFocusY;
    transformationMatrix.postTranslate(focusX + focusShiftX, focusY + focusShiftY);
    drawMatrix.postConcat(transformationMatrix);
    lastFocusX = focusX;
    lastFocusY = focusY;
    invalidate();
    return true;
}

Similarly in onScroll:

@Override
public boolean onScroll(MotionEvent downEvent, MotionEvent currentEvent,
            float distanceX, float distanceY) {
    drawMatrix.postTranslate(-distanceX, -distanceY);
    invalidate();
    return true;
}

in onDraw; Draw with your drawMatrix:

canvas.drawBitmap(image, drawMatrix, paint);

Happy coding!

Erhannis
  • 4,256
  • 4
  • 34
  • 48
Tore Rudberg
  • 1,594
  • 15
  • 16
  • 1
    Lucid, elegant solution. What is detector.getFocusShiftX() ? – chochim Oct 23 '13 at 16:30
  • 1
    Ah, sorry. When I implemented this I made my own gesture detector to get rotation info as well. getFocusShift is a utility method that is apparently not available in ScaleGestureDetector. I will edit my answer for completeness – Tore Rudberg Oct 24 '13 at 10:39
  • 1
    You don't need to use focusShift if you are already translating matrix in onScroll. For proper working change `transformationMatrix.postTranslate(focusX + focusShiftX), focusY + focusShiftY);` to `transformationMatrix.postTranslate(focusX, focusY);` – David Novák Feb 22 '15 at 11:08
  • @DavidNovák: If a gesture is recognized as scale you will not get scroll updates for that same gesture, my code allows the user to scroll during a scale event. If your don't want that behaviour, then by all means, do it your way. – Tore Rudberg Feb 23 '15 at 08:53