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

by vivin

I was trying to get pinch-zoom and panning working on an Android view today. Basically I was trying to implement the same behavior you see when you use Google Maps (for example). You can zoom in and pan around until the edge of the image, but no further. Also, if the image is fully zoomed out, you can’t pan the image. Implementing the pinch-zoom functionality was pretty easy. I found an example on StackOverflow. I then wanted to implement panning (or dragging) as well. However, I wasn’t able to easily find examples and tutorials for this functionality. I started with this example that comes from the third edition of the Hello, Android! book but I didn’t get too far. So I started playing around a little bit with the events and started writing some code from scratch (using the example from Hello, Android!) so that I could have a better idea of what was happening.

As I mentioned before, getting zoom to work was pretty easy. Implementing panning/dragging was the hard part. The major issues I encountered and subsequently fixed were the following:

  1. Panning continues indefinitely in all directions.
  2. When you zoom and then pan, stop, and then start again, the view jerks to a new position instead of panning from the existing position.
  3. Excessive panning towards the left and top can be constrained, but panning towards the right and bottom is not so easily constrained.

Once I fixed all the problems, I figured that it would be nice to document it for future reference, and I also think it would be a useful resource for others who have the same problem. Now a little disclaimer before I go any further: I’m not an Android expert and I’m really not that great with graphics; I just started it learning to program for Android this semester for one of my Masters electives. So there might be a better way of doing all this, and if there is, please let me know! Also, if you want to skip all the explanations and just see the code, you can skip to the last page.

Let’s start with the simple stuff first, that is implementing pinch-zoom. To implement pinch-zoom, we make use of the ScaleGestureDetector class. This class helps you detect the pinch-zoom event. Using it is pretty simple:

public class ZoomView extends View {

    private static float MIN_ZOOM = 1f;
    private static float MAX_ZOOM = 5f;

    private float scaleFactor = 1.f;
    private ScaleGestureDetector detector;

    public ZoomView(Context context) {
        super(context);
        detector = new ScaleGestureDetector(getContext(), new ScaleListener());
    }

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

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

        canvas.save();
        canvas.scale(scaleFactor, scaleFactor);

        // ...
        // your canvas-drawing code
        // ...

        canvas.restore();
    }

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

Your view class has four private members: MIN_ZOOM, MAX_ZOOM, detector, and scaleFactor. The first two are static constants that define the maximum and minimum zoom allowed. The third is of type ScaleGestureDetector and does all the heavy lifting as far as zooming is concerned. The fourth member holds the scaling factor i.e., a number that represents the amount of “zoom”.

Now what does this do?

In the constructor, you initialize the detector. The constructor to ScaleGestureDetector takes two parameters: the current context, and a listener. Our listener is defined inside the class ScaleListener which extends the abstract class ScaleGestureDetector.SimpleOnScaleGestureListener. Inside the onScale method, we get the current scale factor from the detector. We then check to see if it is greater or smaller than our upper and lower bounds. If so, we make sure that it we limit the value to be between those bounds. We then call invalidate() which forces the canvas to redraw itself.

The actual scaling happens inside the onDraw method. There, we save the canvas, set its scaling factor (which is the one we got from the detector), draw anything we need to draw, and then restore the canvas. What happens now is that anything you draw on the canvas is now scaled by the scaling factor. This is what gives you the “zoom” effect.