How to use Multi-touch in Android 2: Part 6, Implementing the Pinch Zoom Gesture

How to use Multi-touch in Android 2: Part 6, Implementing the Pinch Zoom Gesture

Summary: Over the course of this series we've been working on a simple image viewer that lets you move and zoom a picture on the screen with touch gestures. This part wraps up the example with the code that implements multi-touch pinch zooming. All source code can be downloaded from the "Hello, Android" book web site.

SHARE:
TOPICS: Android, Google, Hardware
34

Welcome to the last installment in the Android multi-touch series! The purpose of this series is to show you how to develop multi-touch programs for Android-based phones, tablets, and other devices. It was excerpted with permission from Hello, Android! (3rd edition), published by the Pragmatic Bookshelf.

Over the course of this series we've been working on a simple image viewer that lets you move and zoom a picture on the screen with touch gestures. This part wraps up the example with the code that implements multi-touch pinch zooming. All source code can be downloaded from the book web site.

The Android multi-touch series:

  1. Introducing Multi-Touch
  2. Building the Touch Example
  3. Understanding Touch Events
  4. Setting up for Image Transformation
  5. Implementing the Drag Gesture
  6. Implementing the Pinch Zoom Gesture

Implementing the Pinch Zoom Gesture

The pinch zoom gesture is similar to the drag gesture, except it starts when the second finger is pressed to the screen (ACTION_POINTER_DOWN).

From Touchv1/src/org/example/touch/Touch.java:

case MotionEvent.ACTION_POINTER_DOWN:
oldDist = spacing(event);
Log.d(TAG, "oldDist=" + oldDist);
if (oldDist > 10f) {
savedMatrix.set(matrix);
midPoint(mid, event);
mode = ZOOM;
Log.d(TAG, "mode=ZOOM" );
}
break;

case MotionEvent.ACTION_MOVE: if (mode == DRAG) { // ... } else if (mode == ZOOM) { float newDist = spacing(event); Log.d(TAG, "newDist=" + newDist); if (newDist > 10f) { matrix.set(savedMatrix); float scale = newDist / oldDist; matrix.postScale(scale, scale, mid.x, mid.y); } } break;

When we get the down event for the second finger, we calculate and remember the distance between the two fingers. In my testing, Android would sometimes tell me (incorrectly) that there were two fingers pressed down in almost exactly the same position. So I added an check to ignore the event if the distance is smaller than some arbitrary number of pixels. If it’s bigger than that, we remember the current transformation matrix, calculate the midpoint of the two fingers, and start the zoom.

When a move event arrives while we’re in zoom mode, we calculate the distance between the fingers again. If it’s too small, the event is ignored, otherwise we restore the transformation matrix and scale the image around the midpoint.

The scale is simply the ratio of the new distance divided by the old distance. If the new distance is bigger (that is, the fingers have gotten further apart), then the scale will be greater than 1, making the image bigger. If it’s smaller (fingers closer together), then the scale will be less than one, making the image smaller. And of course if everything is the same, the scale is equal to 1 and the image is not changed.

Now let’s define the spacing( ) and midPoint( ) methods.

Distance Between Two Points

To find out how far apart two fingers are, we first construct a vector (x, y) which is the difference between the two points. Then we use the formula for Euclidean distance to calculate the spacing:

From Touchv1/src/org/example/touch/Touch.java:

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);
}
The order of the points doesn’t matter because any negative signs will be lost when we square them. Note that all math is done using Java’s float type. While some Android devices may not have floating point hardware, we’re not doing this often enough to worry about its performance.

Midpoint of Two Points

Calculating a point in the middle of two points is even easier:

From Touchv1/src/org/example/touch/Touch.java:

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);
}
All we do is take the average of their X and Y coordinates. To avoid garbage collections that can cause noticeable pauses in the application, we reuse an existing object to store the result rather than allocating and returning a new one each time.

Try running the program now on your phone. Drag the image with one finger, and zoom it by pinching two fingers in or out. For best results, don’t let your fingers get closer than an inch or so apart. Otherwise you’ll start to run into some of those bugs in the API I mentioned earlier.

Fast-Forward >>

In this chapter we learned how to use the multi-touch API to create a pinch zoom gesture. There’s a nice site called GestureWorks that describes a whole library of gestures that have been implemented on the Adobe Flash platform. If you’re willing to push the limits of Android’s quirky multi-touch support, then perhaps you can find ideas there for other gestures to implement in your Android programs.

Because multi-touch code uses new methods that didn’t exist before Android 2.0, if you try to run the Touch example on earlier versions it will fail with a “Force close” error. Luckily there are ways around this limitation (described later in the book - Ed). You can’t teach an old phone new tricks, but you can at least keep it from crashing.

In the next chapter we’ll investigate home screen extensions, including live wallpaper.

Copyright notice: This is an excerpt from Hello, Android 3rd edition, published by the Pragmatic Bookshelf. For more information or to purchase a paperback or PDF copy, please visit http://www.pragprog.com/titles/eband3.

Copyright © 2010 The Pragmatic Programmers, LLC. All rights reserved.

No part of this publication may be reproduced, stored in a retrieval system, or transmitted, in any form, or by any means, electronic, mechanical, photocopying, recording, or otherwise, without the prior consent of the publisher.

Topics: Android, Google, Hardware

Ed Burnette

About Ed Burnette

Ed Burnette is a software industry veteran with more than 25 years of experience as a programmer, author, and speaker. He has written numerous technical articles and books, most recently "Hello, Android: Introducing Google's Mobile Development Platform" from the Pragmatic Programmers.

Kick off your day with ZDNet's daily email newsletter. It's the freshest tech news and opinion, served hot. Get it.

Talkback

34 comments
Log in or register to join the discussion
  • Can't wait for Part 7: Apple sues your company out of existence

    Score one for consumer choice!
    NonZealot
    • Multi-touch predates Apple

      The general idea of multi-touch is not patentable or even trade-markable even given USPTO's lax standards. It has been in use since the invention of, well, fingers. Certain implementations of multi-touch such as a particular way a grid of capacitive sensors is laid out to achieve a high sensitivity may be patentable. In between there are some gray areas that will unfortunately have to be worked out. But there is so much prior art even in the computer field that I'm not very worried.
      Ed Burnette
  • RE: How to use Multi-touch in Android 2: Implementing the Pinch Zoom

    First you talk to your Lawyer, Next you download Apples SDK
    3.2. Then you copy everything Apple provides. Finally you
    steal all the Apples patented inventions that you think you
    can get away with..
    MacNewton
    • Believe it or not...

      People outside Apple can have original ideas.

      Besides, if HTC or Google or Microsoft had copied Apple's implementations, shouldn't it be working better than what they have now?
      Ed Burnette
  • WOW

    simple, usefull, well implemented, magnificent article, tanks now i dont need to download apple sdk , :P
    Draconiak
  • Scale limits on pinch zoom.

    How to set Max and Min zoom levels for Pinch-Zoom?
    Audumbar
  • Scale Limits and pan bounds

    I would also like to know how to set zoom limits as well as prevent the image from being panned off the screen. Any thoughts?
    wshamp
  • Scale limits on pinch zoom.

    hello Ed Burnette,
    Actually I wanted to create an gallery with images.
    Please help me to set zoom limits as well as prevent the image from being panned off the screen.

    Thanks.
    Audumbar
  • RE: How to use Multi-touch in Android 2: Part 6, Implementing the Pinch Zoom Gesture

    On limiting zoom and pan. This worked for me but I'm sure there are other solutions to it (especially the pan limits, felt a bit messy)

    public class Zoom extends Activity implements OnTouchListener {
    private static final String TAG = "Touch";
    // 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;

    // Limit zoomable/pannable image
    private ImageView view;
    private float[] matrixValues = new float[9];
    private float maxZoom;
    private float minZoom;
    private float height;
    private float width;
    private RectF viewRect;

    @Override
    public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    view = (ImageView) findViewById(R.id.imageView);
    view.setOnTouchListener(this);

    // Work around a Cupcake bug
    matrix.setTranslate(1f, 1f);
    view.setImageMatrix(matrix);
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if(hasFocus){
    init();
    }
    }

    private void init() {
    maxZoom = 4;
    minZoom = 0.25f;
    height = view.getDrawable().getIntrinsicHeight();
    width = view.getDrawable().getIntrinsicWidth();
    viewRect = new RectF(0, 0, view.getWidth(), view.getHeight());
    }

    @Override
    public boolean onTouch(View v, MotionEvent rawEvent) {
    WrapMotionEvent event = WrapMotionEvent.wrap(rawEvent);
    ImageView view = (ImageView) v;

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

    // Handle touch events here...
    switch (event.getAction() & MotionEvent.ACTION_MASK) {
    case MotionEvent.ACTION_DOWN:
    savedMatrix.set(matrix);
    start.set(event.getX(), event.getY());
    Log.d(TAG, "mode=DRAG");
    mode = DRAG;
    break;
    case MotionEvent.ACTION_POINTER_DOWN:
    oldDist = spacing(event);
    Log.d(TAG, "oldDist=" + oldDist);
    if (oldDist > 10f) {
    savedMatrix.set(matrix);
    midPoint(mid, event);
    mode = ZOOM;
    Log.d(TAG, "mode=ZOOM");
    }
    break;
    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_POINTER_UP:
    mode = NONE;
    Log.d(TAG, "mode=NONE");
    break;
    case MotionEvent.ACTION_MOVE:
    if (mode == DRAG) {
    matrix.set(savedMatrix);

    // limit pan
    matrix.getValues(matrixValues);
    float currentY = matrixValues[Matrix.MTRANS_Y];
    float currentX = matrixValues[Matrix.MTRANS_X];
    float currentScale = matrixValues[Matrix.MSCALE_X];
    float currentHeight = height * currentScale;
    float currentWidth = width * currentScale;
    float dx = event.getX() - start.x;
    float dy = event.getY() - start.y;
    float newX = currentX+dx;
    float newY = currentY+dy;

    RectF drawingRect = new RectF(newX, newY, newX+currentWidth, newY+currentHeight);
    float diffUp = Math.min(viewRect.bottom-drawingRect.bottom, viewRect.top-drawingRect.top);
    float diffDown = Math.max(viewRect.bottom-drawingRect.bottom, viewRect.top-drawingRect.top);
    float diffLeft = Math.min(viewRect.left-drawingRect.left, viewRect.right-drawingRect.right);
    float diffRight = Math.max(viewRect.left-drawingRect.left, viewRect.right-drawingRect.right);
    if(diffUp > 0 ){
    dy +=diffUp;
    }
    if(diffDown < 0){
    dy +=diffDown;
    }
    if( diffLeft> 0){
    dx += diffLeft;
    }
    if(diffRight < 0){
    dx += diffRight;
    }
    matrix.postTranslate(dx, dy);
    } else if (mode == ZOOM) {
    float newDist = spacing(event);
    Log.d(TAG, "newDist=" + newDist);
    if (newDist > 10f) {
    matrix.set(savedMatrix);
    float scale = newDist / oldDist;

    matrix.getValues(matrixValues);
    float currentScale = matrixValues[Matrix.MSCALE_X];

    // limit zoom
    if (scale * currentScale > maxZoom) {
    scale = maxZoom / currentScale;
    } else if (scale * currentScale < minZoom) {
    scale = minZoom / currentScale;
    }
    matrix.postScale(scale, scale, mid.x, mid.y);
    }
    }
    break;
    }

    view.setImageMatrix(matrix);
    return true; // indicate event was handled
    }

    /** Show an event in the LogCat view, for debugging */
    private void dumpEvent(WrapMotionEvent 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());
    }

    /** Determine the space between the first two fingers */
    private float spacing(WrapMotionEvent event) {
    float x = event.getX(0) - event.getX(1);
    float y = event.getY(0) - event.getY(1);
    return FloatMath.sqrt(x * x + y * y);
    }

    /** Calculate the mid point of the first two fingers */
    private void midPoint(PointF point, WrapMotionEvent event) {
    float x = event.getX(0) + event.getX(1);
    float y = event.getY(0) + event.getY(1);
    point.set(x / 2, y / 2);
    }
    }
    Phyxdevel
    • RE: How to use Multi-touch in Android 2: Part 6, Implementing the Pinch Zoom Gesture

      Thanks!
      I tried your code, its working fine.
      Yes, there should be some other way to set pan limits.

      Thank you very much.
      Audumbar
      • question

        show me the other way,please!
        grypsm
    • RE: How to use Multi-touch in Android 2: Part 6, Implementing the Pinch Zoom Gesture

      I think a better way to do this is to take the current Matrix and apply it to known shapes or vectors and see if the result is acceptable. That way you don't have to keep track of a separate scale and offset.

      For example, look at the Matrix.mapRadius() function. You could give it a radius of '1', and check the return value to see if it's in the range you like, say 0.1 to 10.0.

      For panning, you could make a test rectangle and call the MapRect() function and then check the output.

      And if you find your Matrix is almost but not quite right, then you can decide on the size and shape you want (corners of a rectangle, say), and then call setRectToRect() to adjust the Matrix so it will produce that shape.
      Ed Burnette
      • RE: How to use Multi-touch in Android 2: Part 6, Implementing the Pinch Zoom Gesture

        Is there any tutorial to set zoom limits and pan bounds for Pinch-Zoom?
        Audumbar
    • Really awesome code.........

      Its working good thanks a lot....
      shrikanthmca
    • RE: How to use Multi-touch in Android 2: Part 6, Implementing the Pinch Zoom Gesture

      @Phyxdevel , plz can u post that WrapMotionEvent class code m not understanding m a beginner to android
      rajivbm
    • implement restrictions

      how would i implement this to say the existing touch project from hello android v3 every time i do i get a load of errors, obviously im doing something wrong as others have got it working
      timhannah1988
      • Complete working code

        http://androidtrainningcenter.blogspot.in/2012/04/pinching-zoom-in-android-image-view-or.html
        AndroidTrainner
    • question

      when postScale,how limit pan
      grypsm
      • re

        i tried this,but not good..
        // 缩放限制
        matrix.getValues(matrixValues);
        float currentScale = matrixValues[Matrix.MSCALE_X];
        if (scale * currentScale > maxZoom) {
        scale = maxZoom / currentScale;
        } else if (scale * currentScale < minZoom) {
        scale = minZoom / currentScale;
        }
        float currentY = matrixValues[Matrix.MTRANS_Y];
        float currentX = matrixValues[Matrix.MTRANS_X];
        float currentHeight = height * currentScale;
        float currentWidth = width * currentScale;

        // 缩放后左上角点坐标
        PointF leftTop = new PointF();
        leftTop.x = currentX * scale + mid.x * (1 - scale);
        leftTop.y = currentY * scale + mid.y * (1 - scale);
        Log.d("cc", "左上角:" + leftTop.x + " " + leftTop.y);
        float px = mid.x;
        float py = mid.y;
        if (leftTop.x > 0) {
        px = 0;
        }
        if (leftTop.y > 0) {
        py = 0;
        }
        // 缩放后右下角点坐标
        PointF rightBottom = new PointF();
        rightBottom.x = (currentWidth + currentX) * scale + mid.x
        * (1 - scale);
        rightBottom.y = (currentHeight + currentY) * scale + mid.y
        * (1 - scale);
        Log.d("cc", "右下角:" + rightBottom.x + " " + rightBottom.y);
        if (px != 0) {
        if (rightBottom.x < width) {
        px = width;
        }
        }
        if (py != 0) {
        if (rightBottom.y < height) {
        py = height;
        }
        }
        Log.d("cc", "suof:" + px + " " + py);
        matrix.postScale(scale, scale, px, py);
        who hava other way???
        grypsm
  • RE: How to use Multi-touch in Android 2: Part 6, Implementing the Pinch Zoom Gesture

    hi all^^
    I tried the code from you ed with applied changes from Phyxdevil and it works fine. thx to you both.
    Now how can i get the width and the height of the zoomed picture that is shown in the imageview ?
    ama1985