ZoomableImageView for android java apps

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.

Leave a Reply

Discover more from Apktutor

Subscribe now to keep reading and get access to the full archive.

Continue reading