Skip to content

Instantly share code, notes, and snippets.

@AhmedMousa7
Last active April 22, 2019 10:21
Show Gist options
  • Save AhmedMousa7/4cf13972514fc973c18afdb90b62641d to your computer and use it in GitHub Desktop.
Save AhmedMousa7/4cf13972514fc973c18afdb90b62641d to your computer and use it in GitHub Desktop.
Pinch-zoomable Android frame layout with functionality to move child view inside parent boundaries
/**
* Adapted from anorth at https://gist.github.com/anorth/9845602.
* by Ahmed Mousa on Feb 14 - 2018.
*/
import android.content.Context;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.FrameLayout;
/**
*Pinch-zoomable Android frame layout with functionality to move it's child inside it's boundaries
*/
public class ZoomMoveLayout extends FrameLayout implements ScaleGestureDetector.OnScaleGestureListener {
private enum Mode {
NONE,
DRAG,
ZOOM
}
private static final String TAG = "ZoomMoveLayout";
private static final float MIN_ZOOM = 0.6f;
private float maxZoom;
private Mode mode = Mode.NONE;
private float scale = 1.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;
private ScaleGestureDetector scaleDetector;
private GestureDetector singleTapDetector;
private OnClickListener onSingleTabClick;
public ZoomMoveLayout(Context context) {
super(context);
init(context);
}
public ZoomMoveLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ZoomMoveLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
scaleDetector = new ScaleGestureDetector(context, this);
singleTapDetector = new GestureDetector(context, new SingleTapConfirm());
}
public void setOnSingleTabClick(OnClickListener onSingleTabClick){
this.onSingleTabClick = onSingleTabClick;
}
private class SingleTapConfirm extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onSingleTapUp(MotionEvent event) {
return true;
}
}
@Override
public boolean onTouchEvent(MotionEvent motionEvent) {
//if one click
if (singleTapDetector.onTouchEvent(motionEvent) && onSingleTabClick != null)
onSingleTabClick.onClick(this);
else {
//move and drag
switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mode = Mode.DRAG;
startX = motionEvent.getX() - prevDx;
startY = motionEvent.getY() - prevDy;
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:
mode = Mode.NONE;
prevDx = dx;
prevDy = dy;
break;
}
scaleDetector.onTouchEvent(motionEvent);
if (mode == Mode.DRAG || mode == Mode.ZOOM) {
getParent().requestDisallowInterceptTouchEvent(true);
applyScaleAndTranslation();
}
}
return true;
}
private void applyScaleAndTranslation() {
child().setScaleX(scale);
child().setScaleY(scale);
child().animate().translationX(getPosX()).translationY(getPosY()).setDuration(0).withLayer().start();
invalidate();
}
private void setScaleValue(float newScale){
scale *= newScale;
scale = Math.max(MIN_ZOOM, Math.min(scale, getMaxZoom()));
}
private float getMaxZoom(){
if (maxZoom == 0.0f) {
//ratio according to width
float ratio = (float) child().getWidth() / child().getHeight();
// 1.7 our aspect ration 16/9
if (ratio >= ((float) 16/9))
maxZoom = (float) getWidth() / child().getWidth();
else
maxZoom = (float) getHeight() / child().getHeight();
}
return maxZoom;
}
private float getPosX(){
if (dx > getBoundaryRight()) dx = getBoundaryRight();
else if (dx < getBoundaryLeft()) dx = getBoundaryLeft();
return dx;
}
private float getPosY(){
if (dy < getBoundaryTop()) dy = getBoundaryTop();
else if (dy > getBoundaryBottom()) dy = getBoundaryBottom();
return dy;
}
private float getBoundaryRight(){
return getRight() - child().getRight() - getChildScaledX();
}
private float getBoundaryBottom(){return getBottom() - child().getBottom() - getChildScaledY();}
public float getBoundaryLeft() {
return getLeft() - child().getLeft() + getChildScaledX();
}
public float getBoundaryTop() {
return getTop() - child().getTop() + getChildScaledY();
}
private View child() {
return getChildAt(0);
}
private float getChildScaledX(){ return (child().getWidth() - (child().getWidth() / scale)) / 2 * scale;}
private float getChildScaledY(){ return (child().getHeight() - (child().getHeight() / scale))/ 2 * scale;}
public float getChildPosX(){
return child().getX() - getChildScaledX();
}
public float getChildPosY(){
return child().getY() - getChildScaledY();
}
public int getChildScaledWidth(){return (int) (child().getWidth() * scale);}
public int getChildScaledHeight(){
return (int) (child().getHeight() * scale);
}
// ScaleGestureDetector
@Override
public boolean onScaleBegin(ScaleGestureDetector scaleDetector) {
return true;
}
@Override
public boolean onScale(ScaleGestureDetector scaleDetector) {
setScaleValue(scaleDetector.getScaleFactor());
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector scaleDetector) {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment