Android - zoom in/out RelativeLayout with spread/pinch

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.


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;
 }

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;
                }
            });