Implementing pinch-zoom and pan/drag in an Android view on the canvas

by vivin

After we get the translation information, we actually want to translate the canvas. We can do this in the onDraw(Canvas canvas) method. Recall that earlier when we had only implemented zooming, we called invalidate() inside the onScale(ScaleGestureDetector detector) listener. The problem is that the onScale(…) method is called only when zooming happens and not when panning or dragging happens. So any calculations we make inside the onTouchEvent(…) method during panning won’t get reflected inside the onDraw(…) method. So what we need to do first, is to remove the call to invalidate() out of the onScale(…) method. When we do that, the method ends up looking like this:

@Override
public boolean onScale(ScaleGestureDetector detector) {
    scaleFactor *= detector.getScaleFactor();
    scaleFactor = Math.max(MIN_ZOOM, Math.min(scaleFactor, MAX_ZOOM));
    return true;
}

This is not a huge deal because all we really care about is the value of scaleFactor; we can call invalidate() whenever we deem it convenient. So now that we have both the scaling and translating information at hand, we can add code to the onDraw(…) method to take this into account. We will also add code to the onTouchEvent(…) method that will call invalidate():

@Override
public boolean onTouchEvent(MotionEvent event) {

    switch (event.getAction() & MotionEvent.ACTION_MASK) {
        ...
        ...
    }

    //This will set the value of scaleFactor
    detector.onTouchEvent(event);

    //The only time we want to re-draw the canvas is if we are panning (which happens when the mode is
    //DRAG and the zoom factor is not equal to 1) or if we're zooming
    if ((mode == DRAG && scaleFactor != 1f) || mode == ZOOM) {
        invalidate();
    }

    return true;
}

@Override
public void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.save();

    //We're going to scale the X and Y coordinates by the same amount
    canvas.scale(scaleFactor, scaleFactor);

    //We need to divide by the scale factor here, otherwise we end up with excessive panning based on our zoom level
    //because the translation amount also gets scaled according to how much we've zoomed into the canvas.
    canvas.translate(translateX / scaleFactor, translateY / scaleFactor);
    canvas.restore();
}