Android - zoom in/out RelativeLayout with spread/pinch
Solution 1:
So I created a subclass of RelativeLayout
as described in the above mentioned topics. It looks like this:
public class ZoomableRelativeLayout extends RelativeLayout {
float mScaleFactor = 1;
float mPivotX;
float mPivotY;
public ZoomableRelativeLayout(Context context) {
super(context);
// TODO Auto-generated constructor stub
}
public ZoomableRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
public ZoomableRelativeLayout(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
}
protected void dispatchDraw(Canvas canvas) {
canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.scale(mScaleFactor, mScaleFactor, mPivotX, mPivotY);
super.dispatchDraw(canvas);
canvas.restore();
}
public void scale(float scaleFactor, float pivotX, float pivotY) {
mScaleFactor = scaleFactor;
mPivotX = pivotX;
mPivotY = pivotY;
this.invalidate();
}
public void restore() {
mScaleFactor = 1;
this.invalidate();
}
}
My implementation of the SimpleOnScaleGestureListener
looks like this:
private class OnPinchListener extends SimpleOnScaleGestureListener {
float startingSpan;
float endSpan;
float startFocusX;
float startFocusY;
public boolean onScaleBegin(ScaleGestureDetector detector) {
startingSpan = detector.getCurrentSpan();
startFocusX = detector.getFocusX();
startFocusY = detector.getFocusY();
return true;
}
public boolean onScale(ScaleGestureDetector detector) {
mZoomableRelativeLayout.scale(detector.getCurrentSpan()/startingSpan, startFocusX, startFocusY);
return true;
}
public void onScaleEnd(ScaleGestureDetector detector) {
mZoomableRelativeLayout.restore();
}
}
Hope this helps!
Update:
You can integrate OnPinchListener
for your ZoomableRelativelayout
by using ScaleGestureDetector
:
ScaleGestureDetector scaleGestureDetector = new ScaleGestureDetector(this, new OnPinchListener());
And you are required to bind touch listener of Zoomable layout with the touch listener of ScaleGestureDetector:
mZoomableLayout.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub
scaleGestureDetector.onTouchEvent(event);
return true;
}
});
Solution 2:
Create class called Zoomlayout which extends any layout that you want to zoom in my case it is Relative Layout.
public class ZoomLayout extends RelativeLayout 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;
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);
}
public void init(Context context) {
final ScaleGestureDetector scaleDetector = new ScaleGestureDetector(context, this);
this.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "DOWN");
if (scale > MIN_ZOOM) {
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.DRAG;
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);
}
}
After this add ZoomLayout in xml that has only one child.For example
<?xml version="1.0" encoding="utf-8"?>
<com.focusmedica.digitalatlas.headandneck.ZoomLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:id="@+id/zoomLayout"
android:background="#000000"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:paddingTop="5dp"
android:textColor="#ffffff"
android:text="Heading"
android:gravity="center"
android:textAlignment="textStart"
android:paddingLeft="5dp"
android:textSize="20sp"
android:textStyle="bold"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/tvSubtitle2"
android:layout_toLeftOf="@+id/ivOn"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />
<ImageView
android:id="@+id/ivOff"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/off_txt"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true" />
<ImageView
android:id="@+id/ivOn"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/on_txt"
android:layout_alignParentTop="true"
android:layout_alignLeft="@+id/pinOn"
android:layout_alignStart="@+id/pinOn" />
<ImageView
android:id="@+id/pinOff"
android:visibility="invisible"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/pin_off"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true" />
<ImageView
android:id="@+id/pinOn"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/pin_on"
android:layout_alignParentTop="true"
android:layout_toLeftOf="@+id/ivOff"
android:layout_toStartOf="@+id/ivOff" />
<RelativeLayout
android:id="@+id/linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true">
<ImageView
android:src="@drawable/wait"
android:layout_width="match_parent"
android:layout_height="300dp"
android:id="@+id/fullIVideo"/>
<ImageView
android:src="@drawable/wait"
android:layout_width="match_parent"
android:layout_height="300dp"
android:id="@+id/colorCode"/>
<ImageView
android:src="@drawable/wait"
android:layout_width="match_parent"
android:layout_height="300dp"
android:id="@+id/labelText"/>
<ImageView
android:src="@drawable/download"
android:layout_marginTop="91dp"
android:layout_width="100dp"
android:layout_height="100dp"
android:id="@+id/label_play"
android:layout_alignTop="@+id/fullIVideo"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true" />
</RelativeLayout>
<LinearLayout
android:orientation="vertical"
android:id="@+id/custom_toast_layout"
android:layout_width="300dp"
android:layout_above="@+id/up"
android:background="@drawable/rectangle_frame"
android:paddingLeft="10dp"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:paddingRight="10dp"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_height="wrap_content">
<TextView
android:textSize="15sp"
android:textColor="#ffffff"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Medium Text"
android:id="@+id/tvLabel" />
<TextView
android:textColor="#ffffff"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="New Text"
android:layout_gravity="center"
android:id="@+id/tvLabelDescription" />
</LinearLayout>
<ImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/up"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:id="@+id/up" />
</RelativeLayout>
</com.focusmedica.digitalatlas.headandneck.ZoomLayout>
Now in MainActivity create object of ZoomLayout and define id.Like
ZoomLayout zoomlayout= findViewbyId(R.id.zoomLayout);
zoomlayout.setOnTouchListener(FullScreenVideoActivity.this);
public boolean onTouch(View v, MotionEvent event) {
linear.init(FullScreenVideoActivity.this);
return false;
}
Solution 3:
I think I managed to improve Schnodahipfe's answer a little bit. I added two methods to the ZoomableRelativeLayout class.
public void relativeScale(float scaleFactor, float pivotX, float pivotY)
{
mScaleFactor *= scaleFactor;
if(scaleFactor >= 1)
{
mPivotX = mPivotX + (pivotX - mPivotX) * (1 - 1 / scaleFactor);
mPivotY = mPivotY + (pivotY - mPivotY) * (1 - 1 / scaleFactor);
}
else
{
pivotX = getWidth()/2;
pivotY = getHeight()/2;
mPivotX = mPivotX + (pivotX - mPivotX) * (1 - scaleFactor);
mPivotY = mPivotY + (pivotY - mPivotY) * (1 - scaleFactor);
}
this.invalidate();
}
public void release()
{
if(mScaleFactor < MIN_SCALE)
{
final float startScaleFactor = mScaleFactor;
Animation a = new Animation()
{
@Override
protected void applyTransformation(float interpolatedTime, Transformation t)
{
scale(startScaleFactor + (MIN_SCALE - startScaleFactor)*interpolatedTime,mPivotX,mPivotY);
}
};
a.setDuration(300);
startAnimation(a);
}
else if(mScaleFactor > MAX_SCALE)
{
final float startScaleFactor = mScaleFactor;
Animation a = new Animation()
{
@Override
protected void applyTransformation(float interpolatedTime, Transformation t)
{
scale(startScaleFactor + (MAX_SCALE - startScaleFactor)*interpolatedTime,mPivotX,mPivotY);
}
};
a.setDuration(300);
startAnimation(a);
}
}
and rewrote the OnPinchListener class like this
private class OnPinchListener extends ScaleGestureDetector.SimpleOnScaleGestureListener
{
float currentSpan;
float startFocusX;
float startFocusY;
public boolean onScaleBegin(ScaleGestureDetector detector)
{
currentSpan = detector.getCurrentSpan();
startFocusX = detector.getFocusX();
startFocusY = detector.getFocusY();
return true;
}
public boolean onScale(ScaleGestureDetector detector)
{
ZoomableRelativeLayout zoomableRelativeLayout= (ZoomableRelativeLayout) ImageFullScreenActivity.this.findViewById(R.id.imageWrapper);
zoomableRelativeLayout.relativeScale(detector.getCurrentSpan() / currentSpan, startFocusX, startFocusY);
currentSpan = detector.getCurrentSpan();
return true;
}
public void onScaleEnd(ScaleGestureDetector detector)
{
ZoomableRelativeLayout zoomableRelativeLayout= (ZoomableRelativeLayout) ImageFullScreenActivity.this.findViewById(R.id.imageWrapper);
zoomableRelativeLayout.release();
}
}
The original answer would reset the scale every time the touch event ended, but like this you can zoom in and out multiple times.
Solution 4:
for fragments you just need to pass getActivity() instead of Activity Name
final ZoomLayout zoomlayout = (ZoomLayout) findViewById(R.id.zoomLayout);
zoomlayout.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
zoomlayout.init(getActivity());
return false;
}
});