Last active
April 22, 2019 10:21
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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