I’ve been doing some Android programming recently, and I wrote a pan / zoom listener that can be used on a VideoView, or an ImageView. This is an extension on some code I found here http://stackoverflow.com/questions/3813119/scale-limits-on-pinch-zoom-of-android which it turns out was from a touch / pinch example in a book called Hello Android http://pragprog.com/book/eband3/hello-android
The advantage my code has over other implementations is that the image will zoom in on the exact spot between your fingers, and it is limited to stop the image from going offscreen.
Here’s some example code of using it with an ImageView:
FrameLayout view = new FrameLayout(context); FrameLayout.LayoutParams fp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT, Gravity.TOP | Gravity.LEFT); ImageView imageView = new ImageView(this); //Use line below for large images if you have hardware rendering turned on //imageView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); // Line below is optional, depending on what scaling method you want imageView.setScaleType(ImageView.ScaleType.MATRIX); view.addView(imageView, fp); view.setOnTouchListener(new PanAndZoomListener(view, imageView, Anchor.TOPLEFT));
Here’s some example code of using it with a VideoView:
FrameLayout view = new FrameLayout(context); FrameLayout.LayoutParams fp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT, Gravity.CENTER); VideoView videoView = new VideoView(this); view.addView(videoView, fp); view.setOnTouchListener(new PanAndZoomListener(view, videoView, Anchor.CENTER));
Here’s the source
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.FloatMath;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.widget.FrameLayout;
import android.widget.ImageView;
public class PanAndZoomListener implements OnTouchListener {
public static class Anchor {
public static final int CENTER = 0;
public static final int TOPLEFT = 1;
}
private static final String TAG = "PanAndZoomListener";
// We can be in one of these 3 states
static final int NONE = 0;
static final int DRAG = 1;
static final int ZOOM = 2;
int mode = NONE;
// Remember some things for zooming
PointF start = new PointF();
PointF mid = new PointF();
float oldDist = 1f;
PanZoomCalculator panZoomCalculator;
public PanAndZoomListener(FrameLayout containter, View view, int anchor) {
panZoomCalculator = new PanZoomCalculator(containter, view, anchor);
}
public boolean onTouch(View view, MotionEvent event) {
// Handle touch events here...
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
start.set(event.getX(), event.getY());
Log.d(TAG, "mode=DRAG");
mode = DRAG;
break;
case MotionEvent.ACTION_POINTER_DOWN:
oldDist = spacing(event);
Log.d(TAG, "oldDist=" + oldDist);
if (oldDist > 10f) {
midPoint(mid, event);
mode = ZOOM;
Log.d(TAG, "mode=ZOOM");
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
mode = NONE;
Log.d(TAG, "mode=NONE");
break;
case MotionEvent.ACTION_MOVE:
if (mode == DRAG) {
panZoomCalculator.doPan(event.getX() - start.x, event.getY() - start.y);
start.set(event.getX(), event.getY());
} else if (mode == ZOOM) {
float newDist = spacing(event);
Log.d(TAG, "newDist=" + newDist);
if (newDist > 10f) {
float scale = newDist / oldDist;
oldDist = newDist;
panZoomCalculator.doZoom(scale, mid);
}
}
break;
}
return true; // indicate event was handled
}
// Determine the space between the first two fingers
private float spacing(MotionEvent event) {
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return FloatMath.sqrt(x * x + y * y);
}
// Calculate the mid point of the first two fingers
private void midPoint(PointF point, MotionEvent event) {
// ...
float x = event.getX(0) + event.getX(1);
float y = event.getY(0) + event.getY(1);
point.set(x / 2, y / 2);
}
class PanZoomCalculator {
/// The current pan position
PointF currentPan;
/// The current zoom position
float currentZoom;
/// The windows dimensions that we are zooming/panning in
View window;
View child;
Matrix matrix;
// Pan jitter is a workaround to get the video view to update it's layout properly when zoom is changed
int panJitter = 0;
int anchor;
PanZoomCalculator(View container, View child, int anchor) {
// Initialize class fields
currentPan = new PointF(0, 0);
currentZoom = 1f;;
this.window = container;
this.child = child;
matrix = new Matrix();
this.anchor = anchor;
onPanZoomChanged();
this.child.addOnLayoutChangeListener(new OnLayoutChangeListener() {
// This catches when the image bitmap changes, for some reason it doesn't recurse
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
onPanZoomChanged();
}
});
}
public void doZoom(float scale, PointF zoomCenter) {
float oldZoom = currentZoom;
// multiply in the zoom change
currentZoom *= scale;
// this limits the zoom
currentZoom = Math.max(getMinimumZoom(), currentZoom);
currentZoom = Math.min(8f, currentZoom);
// Adjust the pan accordingly
// Need to make it such that the point under the zoomCenter remains under the zoom center after the zoom
// calculate in fractions of the image so:
float width = window.getWidth();
float height = window.getHeight();
float oldScaledWidth = width * oldZoom;
float oldScaledHeight = height * oldZoom;
float newScaledWidth = width * currentZoom;
float newScaledHeight = height * currentZoom;
if (anchor == Anchor.CENTER) {
float reqXPos = ((oldScaledWidth - width) * 0.5f + zoomCenter.x - currentPan.x) / oldScaledWidth;
float reqYPos = ((oldScaledHeight - height) * 0.5f + zoomCenter.y - currentPan.y) / oldScaledHeight;
float actualXPos = ((newScaledWidth - width) * 0.5f + zoomCenter.x - currentPan.x) / newScaledWidth;
float actualYPos = ((newScaledHeight - height) * 0.5f + zoomCenter.y - currentPan.y) / newScaledHeight;
currentPan.x += (actualXPos - reqXPos) * newScaledWidth;
currentPan.y += (actualYPos - reqYPos) * newScaledHeight;
} else {
// assuming top left
float reqXPos = (zoomCenter.x - currentPan.x) / oldScaledWidth;
float reqYPos = (zoomCenter.y - currentPan.y) / oldScaledHeight;
float actualXPos = (zoomCenter.x - currentPan.x) / newScaledWidth;
float actualYPos = (zoomCenter.y - currentPan.y) / newScaledHeight;
currentPan.x += (actualXPos - reqXPos) * newScaledWidth;
currentPan.y += (actualYPos - reqYPos) * newScaledHeight;
}
onPanZoomChanged();
}
public void doPan(float panX, float panY) {
currentPan.x += panX;
currentPan.y += panY;
onPanZoomChanged();
}
private float getMinimumZoom() {
return 1f;
}
/// Call this to reset the Pan/Zoom state machine
public void reset() {
// Reset zoom and pan
currentZoom = getMinimumZoom();
currentPan = new PointF(0f, 0f);
onPanZoomChanged();
}
public void onPanZoomChanged() {
// Things to try: use a scroll view and set the pan from the scrollview
// when panning, and set the pan of the scroll view when zooming
float winWidth = window.getWidth();
float winHeight = window.getHeight();
if (currentZoom <= 1f) {
currentPan.x = 0;
currentPan.y = 0;
} else if (anchor == Anchor.CENTER) {
float maxPanX = (currentZoom - 1f) * window.getWidth() * 0.5f;
float maxPanY = (currentZoom - 1f) * window.getHeight() * 0.5f;
currentPan.x = Math.max(-maxPanX, Math.min(maxPanX, currentPan.x));
currentPan.y = Math.max(-maxPanY, Math.min(maxPanY, currentPan.y));
} else {
// assume top left
float maxPanX = (currentZoom - 1f) * window.getWidth();
float maxPanY = (currentZoom - 1f) * window.getHeight();
currentPan.x = Math.max(-maxPanX, Math.min(0, currentPan.x));
currentPan.y = Math.max(-maxPanY, Math.min(0, currentPan.y));
}
if (child instanceof ImageView && ((ImageView) child).getScaleType()== ImageView.ScaleType.MATRIX) {
ImageView view = (ImageView) child;
Drawable drawable = view.getDrawable();
if (drawable != null) {
Bitmap bm = ((BitmapDrawable) drawable).getBitmap();
if (bm != null) {
// Limit Pan
float bmWidth = bm.getWidth();
float bmHeight = bm.getHeight();
float fitToWindow = Math.min(winWidth / bmWidth, winHeight / bmHeight);
float xOffset = (winWidth - bmWidth * fitToWindow) * 0.5f * currentZoom;
float yOffset = (winHeight - bmHeight * fitToWindow) * 0.5f * currentZoom;
matrix.reset();
matrix.postScale(currentZoom * fitToWindow, currentZoom * fitToWindow);
matrix.postTranslate(currentPan.x + xOffset, currentPan.y + yOffset);
((ImageView) child).setImageMatrix(matrix);
}
}
} else {
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
lp.leftMargin = (int) currentPan.x + panJitter;
lp.topMargin = (int) currentPan.y;
lp.width = (int) (window.getWidth() * currentZoom);
lp.height = (int) (window.getHeight() * currentZoom);
panJitter ^= 1;
child.setLayoutParams(lp);
}
}
}
}
12 Comments
Hi!
Thanks a ton for the code. It works perfect on android 4.0.4 and so, but I am having problems with 2.3.3. It zooms keeping the top left corner always in screen, not between the fingers. Also panning doesn’t work. Any ideas what might go wrong?
I’ve only tested this on Android 4 upwards. I think in the older versions you had to do an extra step to get the x,y coordinates of the touch event.
I have used your code for canvas view zoom/pinch and it’s working. Thank’s
Your welcome.
I believe android 2.* don’t like negative values in MarginLayoutParams margins. I am not using matrix scaletype as it seems to misbehave for some reason.
Just wanted to tell you I got the matrix scaletype working. And it works on all versions now. Thanks for sharing this code!
Glad you got it working. My requirements were simpler, I developed an app for a customer who wanted it to run on an ASUS Transformer Prime and nothing else and this code was just something generic that I was able to souvenir for my blog.
Getting error class classDefNot Found……please suggest me
Hi,
Thanks for all the hard work on the code. I am having a little trouble getting it going though. I have it zooming vertically but not horizontally. Any ideas?
Thanks,
George
This is not working on 2.2 classdefnot found exception.
Unfortunately this was written specifically for Android 4.0 (as per customer requirements) and no other version. The codebase this was part of was delivered, and support is being handled by someone else. Since I pushlished this I haven’t done any other Android development, so I’m not in a position to help out with back or forward porting.
I haven’t picked up any Android jobs since I wrote this code 7 months ago as part of a job for an Android 4.0 app, so I’m probably not going to be much help at this present time. The only thing I can think of is that there is something else interacting with the image/video that is keeping it centered or sized horizontally.
If you are panning/zooming an image I have 2 different modes, 1 acts directly on the image itself, the other acts on the layout of the image container. You could try switching between the 2 by including or not including this line as seen at in the example at the top of the blog post:
imageView.setScaleType(ImageView.ScaleType.MATRIX);
2 Trackbacks
[...] an interesting example. It comes from Java Pan / Zoom listener for Android (reference 2 below). Rather than doing a custom view every time you want to have panning and [...]
[...] Java Pan / Zoom listener for Android [...]