android imageView: setting drag and pinch zoom parameters

I am currently developing for Android (my first app) an application which lets users see the subway map and be able to pinch zoom and drag around.

I am currently modifying the code found in Hello Android, 3rd Edition and got the pinch zooming and dragging to work. I'm using Matrix as my layout scale.

However I now have 3 problems:

  1. I tried many things to limit the drag parameters but I can't seem to stop it being dragged off the parent view (and can actually disappear from view). I've tried setting layout parameters in the XML file and it just doesn't work.

  2. I can pinch zoom fine but I have trouble, again, limiting the amount of zoom. I'm trying to play around with setting a max_zoom and min_zoom to limit the scaling value (i will post my code after)

  3. I also have trouble trying to map a coordinate on my image so that people can click on certain parts (the whole point of this is to let users click a station on the map and view information about it)

I have a feeling i'm having trouble because I'm using the matrix scale.

Here is my current code:

Touch.java

package org.example.touch;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.os.Bundle;
import android.util.FloatMath;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.GridView;
import android.widget.ImageView;

public class Touch extends Activity implements OnTouchListener {
private static final String TAG = "Touch";

private static final float MIN_ZOOM = 1.0f;
private static final float MAX_ZOOM = 5.0f;

// These matrices will be used to move and zoom image
Matrix matrix = new Matrix();
Matrix savedMatrix = new Matrix();

// We can be in one of these 3 states
static final int NONE = 0;
static final int DRAG = 1;
static final int ZOOM = 2;
int mode = NONE;

// Remember some things for zooming
PointF start = new PointF();
PointF mid = new PointF();
float oldDist = 1f;

@Override
public void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.main);
   ImageView view = (ImageView) findViewById(R.id.imageView);
   //view.setLayoutParams(new GridView.LayoutParams(85, 85));
   view.setScaleType(ImageView.ScaleType.FIT_CENTER);
   view.setOnTouchListener(this);   
}

public boolean onTouch(View v, MotionEvent event) {
   ImageView view = (ImageView) v;
   view.setScaleType(ImageView.ScaleType.MATRIX);
   float scale;

   // Dump touch event to log
   dumpEvent(event);

   // Handle touch events here...
   switch (event.getAction() & MotionEvent.ACTION_MASK) {

   case MotionEvent.ACTION_DOWN: //first finger down only
      savedMatrix.set(matrix);
      start.set(event.getX(), event.getY());
      Log.d(TAG, "mode=DRAG" );
      mode = DRAG;
      break;
   case MotionEvent.ACTION_UP: //first finger lifted
   case MotionEvent.ACTION_POINTER_UP: //second finger lifted
      mode = NONE;
      Log.d(TAG, "mode=NONE" );
      break;
   case MotionEvent.ACTION_POINTER_DOWN: //second finger down
      oldDist = spacing(event);
      Log.d(TAG, "oldDist=" + oldDist);
      if (oldDist > 5f) {
         savedMatrix.set(matrix);
         midPoint(mid, event);
         mode = ZOOM;
         Log.d(TAG, "mode=ZOOM" );
      }
      break;

   case MotionEvent.ACTION_MOVE: 
      if (mode == DRAG) { //movement of first finger
         matrix.set(savedMatrix);
         if (view.getLeft() >= -392){
            matrix.postTranslate(event.getX() - start.x, event.getY() - start.y);
         }
      }
      else if (mode == ZOOM) { //pinch zooming
         float newDist = spacing(event);
         Log.d(TAG, "newDist=" + newDist);
         if (newDist > 5f) {
            matrix.set(savedMatrix);
            scale = newDist / oldDist; **//thinking i need to play around with this value to limit it**
            matrix.postScale(scale, scale, mid.x, mid.y);
         }
      }
      break;
   }

   // Perform the transformation
   view.setImageMatrix(matrix);

   return true; // indicate event was handled
}

private float spacing(MotionEvent event) {
   float x = event.getX(0) - event.getX(1);
   float y = event.getY(0) - event.getY(1);
   return FloatMath.sqrt(x * x + y * y);
}

private void midPoint(PointF point, MotionEvent event) {
   float x = event.getX(0) + event.getX(1);
   float y = event.getY(0) + event.getY(1);
   point.set(x / 2, y / 2);
}

/** Show an event in the LogCat view, for debugging */
private void dumpEvent(MotionEvent event) {
   String names[] = { "DOWN" , "UP" , "MOVE" , "CANCEL" , "OUTSIDE" ,
      "POINTER_DOWN" , "POINTER_UP" , "7?" , "8?" , "9?" };
   StringBuilder sb = new StringBuilder();
   int action = event.getAction();
   int actionCode = action & MotionEvent.ACTION_MASK;
   sb.append("event ACTION_" ).append(names[actionCode]);
   if (actionCode == MotionEvent.ACTION_POINTER_DOWN
         || actionCode == MotionEvent.ACTION_POINTER_UP) {
      sb.append("(pid " ).append(
      action >> MotionEvent.ACTION_POINTER_ID_SHIFT);
      sb.append(")" );
   }
   sb.append("[" );
   for (int i = 0; i < event.getPointerCount(); i++) {
      sb.append("#" ).append(i);
      sb.append("(pid " ).append(event.getPointerId(i));
      sb.append(")=" ).append((int) event.getX(i));
      sb.append("," ).append((int) event.getY(i));
      if (i + 1 < event.getPointerCount())
         sb.append(";" );
   }
   sb.append("]" );
   Log.d(TAG, sb.toString());
}
}

main.xml (rather simple nothing really complicated):

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent" >
<ImageView android:id="@+id/imageView"
     android:layout_width="fill_parent"
     android:layout_height="fill_parent"
     android:src="@drawable/map"
     android:scaleType="matrix" >
</ImageView>
</FrameLayout>

AndroidManifest.xml (only added the theme so there is no title bar and is full screen)

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="org.example.touch"
  android:versionCode="7"
  android:versionName="1.0" >
<application android:icon="@drawable/icon" android:label="@string/app_name" android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
  <activity android:name=".Touch"
        android:label="@string/app_name" >
     <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
     </intent-filter>
  </activity>
</application>
<uses-sdk android:minSdkVersion="3" android:targetSdkVersion="7" />
</manifest>

Solution 1:

I just created this:

https://github.com/jasonpolites/gesture-imageview

Might be useful for someone...

Solution 2:

Another option that might work for some is to use a WebView, which has built in zoom controls.

WebView webView = new WebView(this);
webView.setBackgroundColor(0xff000000);
webView.setScrollBarStyle(WebView.SCROLLBARS_OUTSIDE_OVERLAY);
webView.getSettings().setBuiltInZoomControls(true);
webView.getSettings().setSupportZoom(true);
//webView.getSettings().setDisplayZoomControls(false);  // API 11
webView.loadDataWithBaseURL(null, getHtml(), "text/html", "UTF-8", null);
mainView.addView(webView, -1, -2);

Solution 3:

I know this is old but I was looking into doing this and have a solution that works pretty well. Right after your switch statement and before you set the matrix, you can limit the zoom like so:

private void limitZoom(Matrix m) {

    float[] values = new float[9];
    m.getValues(values);
    float scaleX = values[Matrix.MSCALE_X];
    float scaleY = values[Matrix.MSCALE_Y];
    if(scaleX > MAX_ZOOM) {
        scaleX = MAX_ZOOM;
    } else if(scaleX < MIN_ZOOM) {
        scaleX = MIN_ZOOM;
    }

    if(scaleY > MAX_ZOOM) {
        scaleY = MAX_ZOOM;
    } else if(scaleY < MIN_ZOOM) {
        scaleY = MIN_ZOOM;
    }

    values[Matrix.MSCALE_X] = scaleX;
    values[Matrix.MSCALE_Y] = scaleY; 
    m.setValues(values);
}

I'm still working out how to limit the translation but this should work for the zoom limiting.

EDIT: Here's a solution for limiting the translation. Just as a note, I'm doing this for a full screen image view which is why I use the display width and height in my limiting factors but you could just as easily use the width and height of your view instead.

private void limitDrag(Matrix m) {
    float[] values = new float[9];
    m.getValues(values);
    float transX = values[Matrix.MTRANS_X];
    float transY = values[Matrix.MTRANS_Y];
    float scaleX = values[Matrix.MSCALE_X];
    float scaleY = values[Matrix.MSCALE_Y];

    ImageView iv = (ImageView)findViewById(R.id.photo_view);
    Rect bounds = iv.getDrawable().getBounds();
    int viewWidth = getResources().getDisplayMetrics().widthPixels;
    int viewHeight = getResources().getDisplayMetrics().heightPixels;

    int width = bounds.right - bounds.left;
    int height = bounds.bottom - bounds.top;

    float minX = (-width + 20) * scaleX; 
    float minY = (-height + 20) * scaleY;

    if(transX > (viewWidth - 20)) {
        transX = viewWidth - 20;
    } else if(transX < minX) {
        transX = minX;
    }

    if(transY > (viewHeight - 80)) {
        transY = viewHeight - 80;
    } else if(transY < minY) {
        transY = minY;
    }

    values[Matrix.MTRANS_X] = transX;
    values[Matrix.MTRANS_Y] = transY; 
    m.setValues(values);
}

Once again, this would go right after your switch statement and right before you set the matrix for the image in the view. I broke out the zoom limiting into a function as well and it is reflected above.