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:
- Panning continues indefinitely in all directions.
- 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.
- 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.
I appreciate how clear you tried to explain everything. Thank you!
I want to do a “zoomable paint”, I mean a paint that I can zoom/zoom out and pan/drag the canvas and then draw on it.
I have a problem that I can’t solve: when I draw while the canvas is zoomed, I retrieve the X and Y coordinate and effectively drawing it on the canvas. But these coordinates are not correct because of the zoomed canvas.
I tried to correct these (multiply by (zoomHeigh/screenHeight)) but I can’t find a way to retrieve where I must draw on the original/none-zoomed screen
Whenever I try to implement above method the screen jitters on account of zoom effect but does not zoom essentially. I am drawing circle points on canvas and on zooming I want them to scale further apart. I don’t know if I tend to custom draw those circle points based on dimensions of zoomed canvas why is the overall effect a mere jitter ?