How to take advantage of the pinch-to-zoom feature in your Xperia™ X10 apps – Part 2

Recently Sony Ericsson rolled out an update to the Android™ 2.1 operating system in its Xperia™ X10 phones. One of the important new features in the update is support for a multi-touch gesture called pinch-to-zoom. The first part of this two-part tutorial showed how to take advantage of the pinch-to-zoom feature in your apps. In this second part of the tutorial, you’ll examine a code example that uses the pinch-to-zoom feature.

A pinch-to-zoom code example

Now that we’ve looked at some of the basics of implementing pinch-to-zoom, let’s examine a pinch-to-zoom listener class named PinchZoomListener that puts those basics into action. The PinchZoomListener class is part of a project which you can download and use to build the application.

Download the pinch-to-zoom sample project (0.66 MB)

Remember that the application can recognize and process pinch-to-zoom gestures in Xperia™ X10 only if you install the pinch-to-zoom support in the device.

Implementing the OnTouchListener interface

The first thing to notice about PinchZoomListener is that it implements the View.OnTouchListener interface. This is indicated in the class definition, as shown below.

public class PinchZoomListener implements View.OnTouchListener {

    …

{

By implementing the View.OnTouchListener interface, PinchZoomListener is invoked when a user touches the screen.

Registering the listener

Remember that the listener needs to be registered by an activity. In the application, it is registered in an activity class, TutorialZoomActivity4, as follows:

import com.sonyericsson.zoom.ImageZoomView;
import com.sonyericsson.zoom.PinchZoomListener;
import android.view.View;

public class TutorialZoomActivity4 extends Activity {

private ImageZoomView mZoomView;
private PinchZoomListener mPinchZoomListener;

   @Override
      public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);  

         mPinchZoomListener = new PinchZoomListener(getApplicationContext());
         mZoomView.setOnTouchListener(mPinchZoomListener);
         …
      }

Notice the use of the ImageZoomView class. This class provides a View that is capable of drawing an image at different zoom levels. The ImageZoomControl class is explained in Android one finger zoom tutorial- Part 1 that was published earlier in this blog.

If you’re familiar with the one finger zoom sample project that accompanies the Android one finger zoom tutorial, you’ll notice that the pinch-to-zoom sample is architected quite similarly to the one finger zoom application. The major difference is the addition of the PinchZoomListener.

Defining listener modes

The next thing to notice in PinchZoomListener is that it defines three listener modes, each indicating a touch state. Here is the definition:

private enum Mode {
    UNDEFINED, PAN, PINCHZOOM
}

private Mode mMode = Mode.UNDEFINED;

The three listener mode constants and their meanings are:

  • PAN. Indicates that the user touched the screen and moved his or her finger over the view.
  • PINCHZOOM. Indicates that the user touched the screen with a second finger while continuing to touch the screen with the first finger.
  • UNDEFINED. Indicates that the state is not PAN or PINCHZOOM. The user may or may not be touching the screen. Initially the listener sets the state to UNDEFINED.

Handling gestures

The core of the processing is focused on handling user gestures. Recall from Part 1 of the tutorial that the handler uses the onTouch() method in the View.OnTouchListener interface to process touch events. Here is part of the definition of the onTouch() method in PinchZoomListener.

private VelocityTracker mVelocityTracker;
public boolean onTouch(View v, MotionEvent event) {
    final int action = event.getAction() & MotionEvent.ACTION_MASK;
    final float x = event.getX();
    final float y = event.getY();
if (mVelocityTracker == null) {
    mVelocityTracker = VelocityTracker.obtain();
   }
        mVelocityTracker.addMovement(event);
   …
 }

The onTouch() method uses the getAction() method to get the action from the MotionEvent object for the touch event. To get the action code, the onTouch() method performs a bitwise AND operation on the retrieved action and the ACTION_MASK constant. Then the method gets the X and Y coordinates of the touched position on the screen.

Notice the use of the VelocityTracker class. A VelocityTracker object will be used to track the velocity of the touch events.

Handling ACTION_DOWN events

After getting the action-related information from the MotionEvent object, your application needs to handle the action. Here is how the listener processes an ACTION_DOWN event, that is, when the user touches the screen with one finger:

switch (action) {
    case MotionEvent.ACTION_DOWN:
        mZoomControl.stopFling();
        mDownX = x;
        mDownY = y;
        mX = x;
        mY = y;
        break;

As its name implies, mZoomControl controls zooming in the application. It is assigned a DynamicZoomControl object that is provided in the setZoomControl() method of the application, as shown below.

public void setZoomControl(DynamicZoomControl control) {
    mZoomControl = control;
}

The DynamicZoomControl class is fully explained in Android one finger zoom tutorial – Part 4, published earlier in this blog. The stopFling() method of DynamicZoomControl stops the animation associated with a fling gesture. A fling gesture means that the user dragged an object across the screen and then removed his or her finger from the screen. This result is an animation in which the object continues to move but slows over time. The ACTION_DOWN event invokes stopFling() to stop processing the fling gesture. This handles the situation where the user made a fling gesture before retouching the screen.

The mX and mY variables are used to record the X and Y coordinates, respectively, of the current touch on the screen. The mDownX and mDownY variables are used to record the X and Y coordinates, respectively, of the previous touch on the screen. The four variables are set to the coordinate values for the current ACTION_DOWN event.

Handling ACTION_POINTER_DOWN events

Now let’s look at the code in PinchZoomListener that processes an ACTION_POINTER_DOWN event.

case MotionEvent.ACTION_POINTER_DOWN:
    if (event.getPointerCount() > 1) {
        oldDist = spacing(event);
        if (oldDist > 10f) {
            midPoint(mMidPoint, event);
            mMode = Mode.PINCHZOOM;
        }
    }
    break;

In this case, the user should have two fingers touching the screen. If so, the listener calculates the distance between the two fingers using a spacing() method, as follows.

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

You might wonder why the distance between the fingers is tested, that is, if(oldDist > 10f) {…}. The test is needed because the support in Android for multi-touch events such as pinch-to-zoom is still not optimal (see Limitations below). For example, sometimes Android incorrectly reports that two fingers are touching in almost the same position. To guard against anomalous situations such as this, the listener ignores events where the distance between the two fingers is less than a certain threshold (in this case 10f). If the distance is greater than the threshold, the listener finds the midpoint of the distance using a midPoint() method, as shown below. The midpoint will be used as the center point of the zoom.

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

With the two fingers now touching the screen, the listener sets the mode to PINCHZOOM.

Handling ACTION_MOVE events

Next, let’s look at the code in PinchZoomListener that handles an ACTION_MOVE event, where the user moves a finger across the screen.

case MotionEvent.ACTION_MOVE:
    final float dx = (x - mX) / v.getWidth();
    final float dy = (y - mY) / v.getHeight();

    if (mMode == Mode.PAN) {
        mZoomControl.pan(-dx, -dy);
    } else if (mMode == Mode.PINCHZOOM) {
        float newDist = spacing(event);
        if (newDist > 10f) {
            final float scale = newDist / oldDist;
            final float xx = mMidPoint.x / v.getWidth();
            final float yy = mMidPoint.y / v.getHeight();
            mZoomControl.zoom(scale, xx, yy);
            oldDist = newDist;
        }
    } else {
        final float scrollX = mDownX - x;
        final float scrollY = mDownY - y;

        final float dist =
            (float)Math.sqrt(scrollX * scrollX + scrollY * scrollY);

        if (dist >= mScaledTouchSlop ){
            mMode = Mode.PAN;
        }
    }

    mX = x;
    mY = y;
    break;

Here, the listener determines if the action is part of a pan gesture or a pinch-to-zoom gesture. In either case, it uses the dynamic zoom control to handle the gesture. The zoom action is handled by the zoom() method in the DynamicZoomControl class. The method performs its actions based on the scale of the zoom, an invariant X coordinate position, and an invariant Y coordinate position. The scale of the zoom and the invariant coordinates are defined as follows:

scale of the zoom The distance between the finger before the move (oldDist) divided by the distance between the fingers after the move (newDist).
invariant X coordinate The midpoint of the distance between the fingers divided by the width of the view.
invariant Y coordinate The midpoint of the distance between the fingers divided by the height of the view.

Here, the term invariant means the zoom method ensures that the coordinate value does not change during the course of the zoom.

Notice that the listener ignores the event if the mode is PINCHZOOM and the distance between the points is less than the space threshold. Again, this is to protect against anomalous results.

If the node is not PAN or PINCHZOOM, the listener tests to see if the move distance exceeds the distance at which the movement is considered scrolling in pixels. The listener sets the threshold from the current context using the getScaledTouchSlop() method. Here is the call to getScaledTouchSlop() in the listener.

private final int mScaledTouchSlop;

public PinchZoomListener(Context context) {
    mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    …
}

If the move distance does exceed the threshold, the listener sets the mode to PAN.

Handling ACTION_POINTER_UP events

The next event type to examine is ACTION_POINTER_UP, the case where the user releases one finger while continuing to touch the screen with the other finger. Here is the code that handles that case.

case MotionEvent.ACTION_POINTER_UP:
    if(event.getPointerCount() > 1 &&  mMode == Mode.PINCHZOOM){
       panAfterPinchTimeout = System.currentTimeMillis() + 100;
    }
    mMode = Mode.UNDEFINED;
    break;

There is no zoom processing to perform for this event, so the listener simply sets a timeout threshold that it will use if the user subsequently pans across the screen. It then sets the mode to UNDEFINED.

Handling ACTION_UP events

Last, here is the way the ACTION_UP event is handled in the PinchZoomListener.

case MotionEvent.ACTION_UP:
    if (mMode == Mode.PAN) {
        final long now = System.currentTimeMillis();
        if(panAfterPinchTimeout < now){
           mVelocityTracker.computeCurrentVelocity(1000,
                  mScaledMaximumFlingVelocity);
           mZoomControl.startFling(
                  -mVelocityTracker.getXVelocity() / v.getWidth(),
                  -mVelocityTracker.getYVelocity() / v.getHeight());
        }
        } else if(mMode != Mode.PINCHZOOM) {
                  mZoomControl.startFling(0, 0);
        }
        mVelocityTracker.recycle();
        mVelocityTracker = null;

The ACTION_UP event covers the situation where the user has one finger touching the screen and then removes it from the screen. If the user was panning across the screen before removing the finger, the listener checks to see that timeout threshold wasn’t reached. If the threshold wasn’t reached, the listener starts a fling animation by calling the startFling() method of the Dynamic ZoomControl object.

When it makes the call to startFling(), the listener specifies the velocity in the X and Y dimensions. These velocities are used by the method in calculating the physics of the animation. If the mode is not PAN or PINCHZOOM, the listener simply releases control by calling the startFling() method and specifying 0 for the velocities in the X and Y dimensions.

Limitations

Xperia™ X10 is not yet fully optimized for multi-touch. There are some cases where a pinch-to-zoom gesture generates unusual behavior. For example, a user might not see the expected zoom in or zoom out in the field of view if he or she crosses the X or Y axis during a pinch-to-zoom gesture. For instance, the zoom in or zoom out might appear jumpy, as illustrated below.

crossing axis can be jumpy

In addition, a user might not see a smooth zoom in or zoom out movement if his or her fingers are aligned along the X or Y axis, as illustrated below.

same axis issues

Note too that the all points addressable screen in the Xperia™ X10 consists of an intersection matrix of row and column sense elements, as illustrated below.

dual layer

This arrangement is termed dual layer.

The dual layer hardware registers the touch position of a finger in terms of a row and column. For two fingers, it registers two rows and two columns. However, the hardware does not necessarily distinguish which finger is touching the screen at the registered row and column. Also, the hardware does not necessarily distinguish which recorded value represents the row and which the column. As a result, when two fingers touch the screen, as in a pinch-to-zoom gesture, there are four possibilities of how the combined event is registered.

Suppose, for example, that the first finger (let’s call it finger A) touches the screen at row 1 column 1, and the second finger (let’s call it finger B) touches the screen at row 3 column 6, as shown in the following image.

pinch zoom row column issues

The hardware might register the fingers in any of the following four ways:

  • Finger A at row 1 and column 1 and finger B at row 3 column 6.
  • Finger A at row 3 column 6 and finger B at row 1 column 1.
  • Finger A at row 3 column 1 and finger B at row 1 column 6.
  • Finger A at row 1 column 6 and finger B at row 3 column 1.

This type of axis flipping is also a general problem reported about the multi-touch support in Android 2.1.

Beyond the axis flipping problem, Android 2.1 sometimes records touches by multiple fingers as one finger touch or as multiple fingers touching at almost the same position. So it’s important to check for anomalous results where you can, as the PinchZoomListener in this tutorial does. Recall that the PinchZoomListener tests whether the distance between the two fingers is below a specified distance (10f), and if that distance is below the threshold, the listener ignores the touch event.

If you’re interested in testing your Xperia™ X10 device for multi-touch, you can use the MultiTouch Visualizer 2 application, which you can find in Android Market.

More information

Comments 11

Sort by: Newest | Oldest | Most popular

  1. By Viraj Dasondi

    #1

    Great tutorial..can we use it for ViewGroup instead of View and bitmap

  2. By Viraj Dasondi

    #2

    Great tutorial..can we use ZoomImageView with extending ViewGroup

  3. Pingback #3

    … [Trackback]…

    [...] Read More: developer.sonymobile.com/wp/2011/04/12/how-to-take-advantage-of-the-pinch-to-zoom-feature-in-your-xperia™-10-apps-part-2/ [...]…

  4. By Ed Ort

    #4

    @abdurahman
    We appreciate your positive feedback. As I replied earlier to @Temka, at this time there are no specific plans for additional multitouch functionality in the X10.

  5. By Ed Ort

    #5

    @Tenka
    At this time there are no specific plans for additional multitouch functionality in the X10.

  6. By Ed Ort

    #6

    @Alex.T
    Multitouch support is provided in the Xperia 10, not currently in the Xperia 8.

  7. By Abdurahman

    #7

    if you say it has not been fully optimized, does that mean we will have even better multitouch in the gingerbread update?? can’t wait!!! you guys have been supporting your devices amazingly, i will definitely get another sony ericcson phone down the road

  8. By John

    #8

    well, sony heres the thing, i will activate 24bit colour on the x10 device, yes you heard rigth, 24bit = 16.7million colour. I just need to figure some small stuff out and i wil succeed, And the device is runing 2.3.3 android. So far beyond your Arc.. And even tho this product is older and out of date. Its still competive with your newly released product.

  9. By Alex.T

    #9

    hehe
    I already have Dual-touch on X8 , Yep , X8 !!!!!

    • By Ricardo Oliveira

      #10

      How did you get the Dual touch for xperia x8?

  10. By Tenka

    #11

    Does it means X10 will get further improvement for multi-touch?

1-11 of 11 comments.