Skip to content

Instantly share code, notes, and snippets.

@cami7ord
Last active December 9, 2023 11:03
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save cami7ord/0ce6f36a28d36bf17d96284f2cf75ae9 to your computer and use it in GitHub Desktop.
Save cami7ord/0ce6f36a28d36bf17d96284f2cf75ae9 to your computer and use it in GitHub Desktop.
Pinch-zoomable Android frame layout with double tap to zoom functionality.
/**
* Adapted from anorth at https://gist.github.com/anorth/9845602.
* by cami7ord on Sept 20 - 2017.
*/
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.FrameLayout;
public class ZoomLayout extends FrameLayout implements ScaleGestureDetector.OnScaleGestureListener {
private enum Mode {
NONE,
DRAG,
ZOOM
}
private static final String TAG = "ZoomLayout";
private static final float MIN_ZOOM = 1.0f;
private static final float MAX_ZOOM = 4.0f;
private Mode mode = Mode.NONE;
private float scale = 1.0f;
private float lastScaleFactor = 0f;
// Where the finger first touches the screen
private float startX = 0f;
private float startY = 0f;
// How much to translate the canvas
private float dx = 0f;
private float dy = 0f;
private float prevDx = 0f;
private float prevDy = 0f;
// Custom vars to handle double tap
private boolean firstTouch = false;
private long time = System.currentTimeMillis();
private boolean restore = false;
public ZoomLayout(Context context) {
super(context);
init(context);
}
public ZoomLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ZoomLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
final ScaleGestureDetector scaleDetector = new ScaleGestureDetector(context, this);
this.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
if(firstTouch && (System.currentTimeMillis() - time) <= 300) {
//do stuff here for double tap
if(restore) {
scale = 1.0f;
restore = false;
} else {
scale *= 2.0f;
restore = true;
}
mode = Mode.ZOOM;
firstTouch = false;
} else {
if (scale > MIN_ZOOM) {
mode = Mode.DRAG;
startX = motionEvent.getX() - prevDx;
startY = motionEvent.getY() - prevDy;
}
firstTouch = true;
time = System.currentTimeMillis();
}
break;
case MotionEvent.ACTION_MOVE:
if (mode == Mode.DRAG) {
dx = motionEvent.getX() - startX;
dy = motionEvent.getY() - startY;
}
break;
case MotionEvent.ACTION_POINTER_DOWN:
mode = Mode.ZOOM;
break;
case MotionEvent.ACTION_POINTER_UP:
mode = Mode.NONE;
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "UP");
mode = Mode.NONE;
prevDx = dx;
prevDy = dy;
break;
}
scaleDetector.onTouchEvent(motionEvent);
if ((mode == Mode.DRAG && scale >= MIN_ZOOM) || mode == Mode.ZOOM) {
getParent().requestDisallowInterceptTouchEvent(true);
float maxDx = (child().getWidth() - (child().getWidth() / scale)) / 2 * scale;
float maxDy = (child().getHeight() - (child().getHeight() / scale))/ 2 * scale;
dx = Math.min(Math.max(dx, -maxDx), maxDx);
dy = Math.min(Math.max(dy, -maxDy), maxDy);
Log.i(TAG, "Width: " + child().getWidth() + ", scale " + scale + ", dx " + dx
+ ", max " + maxDx);
applyScaleAndTranslation();
}
return true;
}
});
}
// ScaleGestureDetector
@Override
public boolean onScaleBegin(ScaleGestureDetector scaleDetector) {
Log.i(TAG, "onScaleBegin");
return true;
}
@Override
public boolean onScale(ScaleGestureDetector scaleDetector) {
float scaleFactor = scaleDetector.getScaleFactor();
Log.i(TAG, "onScale" + scaleFactor);
if (lastScaleFactor == 0 || (Math.signum(scaleFactor) == Math.signum(lastScaleFactor))) {
scale *= scaleFactor;
scale = Math.max(MIN_ZOOM, Math.min(scale, MAX_ZOOM));
lastScaleFactor = scaleFactor;
} else {
lastScaleFactor = 0;
}
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector scaleDetector) {
Log.i(TAG, "onScaleEnd");
}
private void applyScaleAndTranslation() {
child().setScaleX(scale);
child().setScaleY(scale);
child().setTranslationX(dx);
child().setTranslationY(dy);
}
private View child() {
return getChildAt(0);
}
}
@gersey
Copy link

gersey commented Sep 24, 2017

I have a LinearLayout in XML, and I insert buttons programmatically. I would like if this view could perform pinch zoom and drag. This is what I use. If my finger is on the button, not in the "empty" section, I cannot pinch zoom and drag. Therefore, I use TextView instead of Button. It works, but when pinch zoom and drag in on a TextView, that will be checked automatically. This is disturbing, so I want to use a delay based on:

tv.setOnTouchListener( new View.OnTouchListener() {
   @Override
   public boolean onTouch(View view, MotionEvent motionEvent) {
       int eventAction = motionEvent.getAction();
       switch (eventAction & MotionEvent.ACTION_MASK) {
           case MotionEvent.ACTION_DOWN:
               Log.i("tap", "down");
               break;
           case MotionEvent.ACTION_UP:
               Log.i("tap", "up");
               break;
           default:
               break;
       }
       return true; // this is ???
   }
});

Unfortunately it works only for the first click, then it also if my finger is on the button, not in the "empty" section, I can not pinch zoom and drag. If I use "return true;" instead of "return false;" then again good, but then the "case MotionEvent.ACTION_UP:" part will not run.

Please help me, thank you.

@monowar1993
Copy link

Hello, need some help. I want zoom the view like in which portion of the layout I put my fingers and zoom that portion will be in center of the layout.

@innerman
Copy link

innerman commented Oct 9, 2017

@cami7ord:
thanks a lot, using the ZoomLayout class for a while in my app, but the double tab feature was just what I was missing, works like charm

@amit7127
Copy link

amit7127 commented Apr 3, 2018

Inside onTouch(), motionEvent.getX() and motionEvent.getY() giving the coordinates of original image not the zoomed image.
How can I get Zoomed image coordinates, so that I can get the pixel color of zoomed image.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment