ZoomableImageView
The ZoomableImageView class is a custom ImageView that allows zooming (pinch-to-zoom, double-tap zoom) and panning (dragging) gestures.
To add a ZoomableImageView to your android project, follow the steps given below.
1. Add a java file ZoomableImageView with following codes.
package com.test.myview;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.ImageView;
public class ZoomableImageView extends ImageView {
private static final int NONE = 0;
private static final int DRAG = 1;
private static final int ZOOM = 2;
private static final int CLICK_THRESHOLD = 10;
private final Matrix matrix = new Matrix();
private final float[] matrixValues = new float[9];
private final PointF lastTouch = new PointF();
private final PointF startTouch = new PointF();
private final RectF imageRect = new RectF();
private ScaleGestureDetector scaleDetector;
private GestureDetector gestureDetector;
private int mode = NONE;
private float minScale = 1.0f;
private float maxScale = 4.0f;
private float saveScale = 1.0f;
private float viewWidth, viewHeight;
private float origWidth, origHeight;
private float redundantXSpace, redundantYSpace;
public ZoomableImageView(Context context) {
super(context);
init(context);
}
public ZoomableImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
super.setClickable(true);
setScaleType(ScaleType.MATRIX);
scaleDetector = new ScaleGestureDetector(context, new ScaleListener());
gestureDetector = new GestureDetector(context, new GestureListener());
matrix.setTranslate(1f, 1f);
setImageMatrix(matrix);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
scaleDetector.onTouchEvent(event);
gestureDetector.onTouchEvent(event);
PointF currentTouch = new PointF(event.getX(), event.getY());
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
lastTouch.set(currentTouch);
startTouch.set(lastTouch);
mode = DRAG;
break;
case MotionEvent.ACTION_POINTER_DOWN:
lastTouch.set(currentTouch);
startTouch.set(lastTouch);
mode = ZOOM;
break;
case MotionEvent.ACTION_MOVE:
if (mode == DRAG && saveScale > minScale) {
handleDrag(currentTouch);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
mode = NONE;
break;
}
setImageMatrix(matrix);
invalidate();
return true;
}
private void handleDrag(PointF currentTouch) {
float deltaX = currentTouch.x - lastTouch.x;
float deltaY = currentTouch.y - lastTouch.y;
matrix.getValues(matrixValues);
float transX = matrixValues[Matrix.MTRANS_X];
float transY = matrixValues[Matrix.MTRANS_Y];
// Calculate current image dimensions
float scaleWidth = Math.round(origWidth * saveScale);
float scaleHeight = Math.round(origHeight * saveScale);
// Apply bounds checking
float[] adjustedDeltas = getAdjustedDeltas(deltaX, deltaY, transX, transY, scaleWidth, scaleHeight);
matrix.postTranslate(adjustedDeltas[0], adjustedDeltas[1]);
lastTouch.set(currentTouch);
}
private float[] getAdjustedDeltas(float deltaX, float deltaY, float transX, float transY,
float scaleWidth, float scaleHeight) {
float newDeltaX = deltaX;
float newDeltaY = deltaY;
// Horizontal bounds checking
if (scaleWidth > viewWidth) {
if (transX + deltaX > 0) {
newDeltaX = -transX;
} else if (transX + deltaX < viewWidth - scaleWidth) {
newDeltaX = viewWidth - scaleWidth - transX;
}
} else {
newDeltaX = 0;
}
// Vertical bounds checking
if (scaleHeight > viewHeight) {
if (transY + deltaY > 0) {
newDeltaY = -transY;
} else if (transY + deltaY < viewHeight - scaleHeight) {
newDeltaY = viewHeight - scaleHeight - transY;
}
} else {
newDeltaY = 0;
}
return new float[]{newDeltaX, newDeltaY};
}
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scaleFactor = detector.getScaleFactor();
float originalScale = saveScale;
saveScale *= scaleFactor;
saveScale = Math.max(minScale, Math.min(saveScale, maxScale));
scaleFactor = saveScale / originalScale;
matrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());
centerImage();
return true;
}
}
private class GestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDoubleTap(MotionEvent e) {
if (saveScale == minScale) {
// Zoom in
saveScale = maxScale;
matrix.postScale(maxScale / minScale, maxScale / minScale, e.getX(), e.getY());
} else {
// Zoom out to fit screen
resetZoom();
}
centerImage();
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
performClick();
return true;
}
}
private void centerImage() {
matrix.getValues(matrixValues);
float transX = matrixValues[Matrix.MTRANS_X];
float transY = matrixValues[Matrix.MTRANS_Y];
float scaleWidth = Math.round(origWidth * saveScale);
float scaleHeight = Math.round(origHeight * saveScale);
float deltaX = 0, deltaY = 0;
if (scaleWidth < viewWidth) {
deltaX = (viewWidth - scaleWidth) / 2 - transX;
} else if (transX > 0) {
deltaX = -transX;
} else if (transX < viewWidth - scaleWidth) {
deltaX = viewWidth - scaleWidth - transX;
}
if (scaleHeight < viewHeight) {
deltaY = (viewHeight - scaleHeight) / 2 - transY;
} else if (transY > 0) {
deltaY = -transY;
} else if (transY < viewHeight - scaleHeight) {
deltaY = viewHeight - scaleHeight - transY;
}
matrix.postTranslate(deltaX, deltaY);
}
private void resetZoom() {
saveScale = minScale;
matrix.reset();
float scale = Math.min(viewWidth / origWidth, viewHeight / origHeight);
matrix.setScale(scale, scale);
redundantXSpace = (viewWidth - (scale * origWidth)) / 2;
redundantYSpace = (viewHeight - (scale * origHeight)) / 2;
matrix.postTranslate(redundantXSpace, redundantYSpace);
origWidth = viewWidth - 2 * redundantXSpace;
origHeight = viewHeight - 2 * redundantYSpace;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
viewWidth = MeasureSpec.getSize(widthMeasureSpec);
viewHeight = MeasureSpec.getSize(heightMeasureSpec);
if (getDrawable() != null) {
setupInitialScale();
}
}
private void setupInitialScale() {
float bmWidth = getDrawable().getIntrinsicWidth();
float bmHeight = getDrawable().getIntrinsicHeight();
float scale = Math.min(viewWidth / bmWidth, viewHeight / bmHeight);
matrix.setScale(scale, scale);
redundantXSpace = (viewWidth - (scale * bmWidth)) / 2;
redundantYSpace = (viewHeight - (scale * bmHeight)) / 2;
matrix.postTranslate(redundantXSpace, redundantYSpace);
origWidth = viewWidth - 2 * redundantXSpace;
origHeight = viewHeight - 2 * redundantYSpace;
saveScale = 1f;
setImageMatrix(matrix);
}
@Override
public boolean performClick() {
super.performClick();
return true;
}
// Public methods for configuration
public void setMaxScale(float maxScale) {
this.maxScale = maxScale;
}
public void setMinScale(float minScale) {
this.minScale = minScale;
}
}2. Add ZoomableImageView in layout file.
<com.mypdfreader.readpdf.ZoomableImageView
android:id="@+id/zoomableImageView"
android:layout_width="match_parent"
android:layout_height="400dp"
android:background="#f0f0f0"
android:contentDescription="Zoomable image" />
3. Define ZoomableImageView in Activity.
// Import ZoomableImageView
// Note: package name has to be changed as per package name of your project
import com.recognize.text.ml.ZoomableImageView;
public class MainActivity extends AppCompatActivity {
// Declare zoomableImageView
private ZoomableImageView zoomableImageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// find ZoomableImageView by id from layout file
zoomableImageView = findViewById(R.id.zoomableImageView);
// Set image of ZoomableImageView from a drawable resource file
zoomableImageView.setImageResource(R.drawable.add1);
}
}4. Set Image of ZoomableImageView.
// From drawable resource file zoomableImageView.setImageResource(R.drawable.add1); // From bitmap image Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), selectedImageUri); zoomableImageView.setImageBitmap(bitmap); // From Uri of image zoomableImageView.setImageURI(selectedImageUri);
Summary
This ZoomableImageView class overrides ImageView to:
- Apply all transformations manually through a Matrix.
- Combine gesture detectors to create an interactive image viewer:
- onTouchEvent() → handles multitouch gestures.Matrix → stores transformations.
- Matrix → stores transformations.ScaleGestureDetector → pinch zoom.
- ScaleGestureDetector → pinch zoom.GestureDetector → double tap.
- GestureDetector → double tap.Dragging restricted within screen bounds.
- Dragging restricted within screen bounds.
Essentially, it gives you Gallery-style zooming and panning behavior using Android’s low-level touch handling.
Alternatives
1. PhotoView (by Chris Banes / updated by MikeOrtiz).
implementation ‘com.github.chrisbanes:PhotoView:2.3.0’
Features automatically included:
- Pinch-to-zoom
- Double-tap zoom
- Panning with bounds control
- Rotation support
- Works seamlessly with Glide/Picasso
- Handles complex touch events internally
2. Android’s Built-in ZoomControls or ScaleGestureDetector
For basic zoom:
- Use ZoomControls widget — provides on-screen +/– buttons to zoom.
- Or use ScaleGestureDetector directly in your own ImageView
3. Use third part libraries like PhotoViewAttacher or SubsamplingScaleImageView.