How To Make Android Multi-Touch Coding Simpler with GestureDetector

646

Multi-touch screens are a killer feature of Android, so you may well want to enable multi-touch on your app. As discussed in the last tutorial,”How to Handle Multi-Touch,” you can write your own code to parse multiple pointer events manually. However, in the case of common multi-touch gestures, like double-tap, scroll, and fling, you can also simplify matters by taking advantage of the built-in GestureDetector. Read Android robot logoon for more about detecting gestures with GestureDetector.

Implementing GestureDetector

Here’s a very basic first implementation of GestureDetector, which just sends gesture information to the log:

public class GestureExample extends Activity  
                            implements GestureDetector.OnGestureListener, 
                                       GestureDetector.OnDoubleTapListener {
	
private GestureDetector detector;
  private String TAG = "GestureExample";
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_gesture_example);
    detector = new GestureDetector(this, this);
    detector.setOnDoubleTapListener(this);
  }
  public boolean onTouchEvent(MotionEvent e){ 
    this.detector.onTouchEvent(e);
    return super.onTouchEvent(e);
  }
  public boolean onDown(MotionEvent e) { 
    Log.d(TAG ,"onDown: " + e.toString()); 
    return true;
  }
  public boolean onFling(MotionEvent e1, MotionEvent e2, 
                         float velocityX, float velocityY) {
    Log.d(TAG, "onFling: " + e1.toString() + e2.toString());
    return true;
  }
  public void onLongPress(MotionEvent e) {
    Log.d(TAG, "onLongPress: " + e.toString()); 
  }
  public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
            float distanceY) {
    Log.d(TAG, "onScroll: " + e1.toString() + e2.toString());
    return true;
  }
  public void onShowPress(MotionEvent e) {
    Log.d(TAG, "onShowPress: " + e.toString());
  }
  public boolean onSingleTapUp(MotionEvent e) {
    Log.d(TAG, "onSingleTapUp: " + e.toString());
    return true;
  }
  public boolean onDoubleTap(MotionEvent e) {
    Log.d(TAG, "onDoubleTap: " + e.toString());
    return true;
  }
  public boolean onDoubleTapEvent(MotionEvent e) {
    Log.d(TAG, "onDoubleTapEvent: " + e.toString());
    return true;
  }
  public boolean onSingleTapConfirmed(MotionEvent e) {
    Log.d(TAG, "onSingleTapConfirmed: " + e.toString());
    return true;
  }
}

There’s quite a lot of code here; but most of it is handling the many gesture events. In onCreate(), you need to create a new GestureDetector, and set it as an OnDoubleTapListener (it’s automatically set as an OnGestureListener). Then, in onTouchEvent(), the this.detector.onTouchEvent(e) line is crucial. Without that, none of the events will be sent to the detector, and none of your carefully-crafted gesture methods will run. It’s also vital to call the superclass implementation after that.

The various gesture methods are split between OnGestureListener (onDown()onFling()onLongPress()onScroll()onShowPress(), and onSingleTapUp()) and OnDoubleTapListener (onDoubleTap()onDoubleTapEvent(), and onSingleTapConfirmed). The double tap events are used to distinguish between a double tap and a definite single tap (ie a single tap which is not about to be followed by a second tap). You can also act on the various different parts of a double tap event: the onDoubleTapEvent() method fires on any event which forms part of a double tap (including down, up, and move events), whereas onDoubleTap() refers to the whole set of events that create the gesture, although the MotionEvent parameter is the first down-tap only. Experimenting with these can help you fit your app accurately to the user’s expectations.

SimpleOnGestureListener

To use OnGestureListener and OnDoubleTapListener, you need to create all these methods, even if you don’t use them. If you only actually want some of them, you can extendSimpleOnGestureListener instead. All this does is to implement the methods in both Listener classes and return false for all of them. You just write your own method only for those gestures that you care about. Note that you must implement onDown() to return true, otherwise other gestures may be ignored, as onDown() happens at the start of all gestures. If it returns false, the event is ‘consumed’ (not passed on to other gesture methods).

Here’s an example of using SimpleOnGestureListener:

public class GestureExample extends Activity { 
	
  private GestureDetector detector;
  private String TAG = "GestureExample";
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_gesture_example);
    detector = new GestureDetector(this, new MyExampleGestureListener());
  }
  public boolean onTouchEvent(MotionEvent e){ 
    this.detector.onTouchEvent(e);
    return super.onTouchEvent(e);
    }
    
  class MyExampleGestureListener extends 
        GestureDetector.SimpleOnGestureListener {
    private static final String TAG = "MEGL"; 
        
    public boolean onDown(MotionEvent e) { 
            Log.d(TAG,"onDown: " + e.toString()); 
            return true;
        }
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
                            float distanceY) {
      Log.d(TAG, "onScroll: " + e1.toString() + e2.toString());
      return true;
   }
  } 
}

For the rest of this tutorial, you can use either of the sets of code above to implement your methods.

Using events

We’re now going to reuse some of the code from the BubbleMove program in previous tutorials, to draw a bubble and then make it respond to a scroll gesture. We’ll do this with SimpleOnGestureListener, to avoid having to implement all the other methods, but you could use OnGestureListener as discussed above. We’re going to move the gesture detecting code out of the main activity and into the View. So GestureExample now looks like this:

public class GestureExample extends Activity  {
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(new GestureExampleView(this));
  }
}

and all the work is done in GestureExampleView:

public class GestureExampleView extends SurfaceView  
                                implements SurfaceHolder.Callback {
	
  private SurfaceHolder sh;
  private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
  private GestureDetector detector;
  private int bubbleX = 100;
  private int bubbleY = 200;
  private int bubbleDiam = 50;
  public GestureExampleView(Context context) {
    super(context);
    sh = getHolder();
    sh.addCallback(this);
    setLongClickable(true);
    paint.setColor(Color.BLUE);
    paint.setStyle(Style.FILL);
    detector = new GestureDetector(context, new MyExampleGestureListener());
  }
  public void surfaceCreated(SurfaceHolder holder) {
    drawBubble();
  }
  // also implement surfaceChanged() and surfaceDestroyed() (can be blank)
  
  private void drawBubble() {
    Canvas canvas = sh.lockCanvas();
    canvas.drawColor(Color.BLACK);
    canvas.drawCircle(bubbleX, bubbleY, bubbleDiam, paint);
    sh.unlockCanvasAndPost(canvas);	  
  }
  public boolean onTouchEvent(MotionEvent e){ 
    this.detector.onTouchEvent(e);
    return super.onTouchEvent(e);
  }
  
  class MyExampleGestureListener 
      extends GestureDetector.SimpleOnGestureListener {
    public boolean onDown(MotionEvent e) { 
      return true;
    }
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
                     		float distanceY) {
      bubbleY = bubbleY - (int)distanceY;
      drawBubble();
      return true;
    }
  }
}

There’s one particularly important line in the GestureExampleView constructor:

setLongClickable(true);

Without this, the only motion event you’ll see will be onDown(); everything else will be ignored. (This line is only necessary for a custom View, not for a regular main Activity.)

Other than this, everything should be familiar from either this tutorial or a previous one; we create a surface, draw a bubble, and then set up the GestureListener. Again, remember that it’s important to implement onDown() to return true. We then use the Y distance to move the bubble’s Y, and redraw it, giving the illusion of scrolling. Note that distanceY is the distance scrolled along the Y axis since the last call to onScroll(), whereas e1 is the very first down motion event that started the scrolling, and e2 is the current motion event that triggered the current onScroll() call. Since a scroll consists of a series of onScroll() calls, distanceY is not the same as the Y distance between e1 and e2.

To move the bubble sideways as well as up and down, you could of course add just a single line:

bubbleX = bubbleX - (int)distanceX;

You can play around more with the other events to see how they work — you might want to move the bubble for a double-tap but not a single-tap, for example.

GestureDetectorCompat

In fact, although the above code works fine for recent Android versions, it is now recommended that, where possible, you use GestureDetectorCompat from the Support Library, to increase compatibility with older versions of Android. Check back here for the next tutorial in this series to learn more about the Support Library and how to bundle it with your app.