Detecting gestures on Android via GestureDetector

Detecting gestures on Android via GestureDetector

The term gesture is used to define a contiguous sequence of interactions between the touch screen and the user. A typical gesture begins at the point that the screen is first touched and ends when the last finger or pointing device leaves the display surface. When correctly harnessed, gestures can be implemented as a form of communication between user and application.

It's straightforward to detect various touch-related events in Android. The Android SDK provides mechanisms for the detection of both common and custom gestures within an application. Common gestures involve interactions such as a tap, double tap, long press or a swiping motion in either a horizontal or a vertical direction (referred to in Android nomenclature as a fling).

At the hardware level, a touch screen is made up of special materials that can pick up pressure and convert that to screen coordinates. The information about the touch is turned into data, and that data is passed to the software to deal with it.

Following infographic can be great to keep in mind how users can interact with applications

android_gestures.jpg

You can see a visual guide of common gestures on the gestures design patterns guide.

The recommended approach is to capture events from the specific View object that users interact with. The View class provides the means to do so, which works well because every UI component in Android is a subclass of the View class.

The base class for touch support is the MotionEvent class which is passed to Views via the onTouchEvent() method. To react to touch events you override the onTouchEvent() method.

The View class belongs to the Android package android.view, and the following list contains some of the touch-related methods in the View

  • onTouchEvent(MotionEvent event)
  • onDoubleTap(MotionEvent event)
  • onDoubleTapEvent(MotionEvent event)
  • onSingleTapConfirmed(MotionEvent event)
  • onTouch(View v, MotionEvent event)public
  • void onClick(View v)
  • boolean onKey(View v, int keyCode, KeyEvent event)
  • boolean onDown(MotionEvent event)
  • void onLongPress(MotionEvent event)
  • boolean onScroll(MotionEvent e1, MotionEvent e2,...)
  • void onShowPress(MotionEvent event)
  • boolean onSingleTapUp(MotionEvent event)

The View class contains the OnTouchListener interface that defines the method onTouchEvent() that you must implement in your custom code. This method has two arguments: the first has type View (the affected component) and the second has type MotionEvent (with information about the user gesture). This method returns true if the touch event has been handled by the view. Android tries to find the deepest view which returns true to handles the touch event. If the view is part of another view (parent view), the parent can claim the event by returning true from the onInterceptTouchEvent() method. This would send an MotionEvent.ACTION_CANCEL event to the view which received previously the touch events.

At the heart of all gestures is the onTouchListener and the onTouch method which has access to MotionEvent data. Every view has an onTouchListener which can be specified:

myView.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        // interpret MotionEvent data
        // handle touch here
        return true;
    }
});

Each onTouch event has access to the MotionEvent which describe movements in terms of an action code and a set of axis values. The action code specifies the state change that occurred such as a pointer going down or up. The axis values describe the position and other movement properties:

  • getAction() method returns an integer constant such as MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE, and MotionEvent.ACTION_UP.
  • getX() method returns the x coordinate of the touch event.
  • getY() method returns the y coordinate of the touch event

Note that every touch event can be propagated through the entire affected view hierarchy.

Within an onTouch event, we can then use a GestureDetector to understand gestures based on a series of motion events. The Android package android.view contains the GestureDetector class that detects various gestures and events using the supplied MotionEvent instance. These detectors and their own listeners are capable of recognizing several of the simplest and most commonly used gestures, such as long presses, double-taps, and flings. At the heart of all touchscreen events is the MotionEvent class, which handles the individual elements of a gesture, such as when and where a finger is placed or removed from the screen or view.

The GestureDetector class contains the following nested interfaces and class:

  • the interface GestureDetector.OnDoubleTapListener, which is used to notify the occurrence of a double-tap or a confirmed single-tap.
  • the interface GestureDetector.OnGestureListener, which is used to notify the occurrence of gestures.
  • the class GestureDetector.SimpleOnGestureListener that you can extend when you want to listen for only a subset of all the gestures.

You use the GestureDetector class as follows:

  1. Create an instance of the GestureDetector for your View.
  2. In the onTouchEvent(MotionEvent) method ensure you call the method onTouchEvent(MotionEvent).

The methods defined in your callback will be executed when the events occur. The GestureDetector class contains the following public methods:

  • isLongpressEnabled().
  • onTouchEvent(): analyzes the given motion event and if applicable, triggers the appropriate callbacks on the GestureDetector.OnGestureListener supplied.
  • setIsLongpressEnabled(): set when longpress is enabled; if this is enabled when a user presses and holds down, you get a longpress event.
  • setOnDoubleTapListener(): sets the listener that is called for double-tap and related gestures.

The MotionEvent object

A MotionEvent object contains information about where and when the touch took place, as well as other details of the touch event. The MotionEvent object is one of a sequence of events related to a touch by the user. The sequence starts when the user first touches the touch screen, continues through any movements of the finger across the surface of the touch screen, and ends when the finger is lifted from the touch screen. The initial touch (an ACTION_DOWN action), the movements sideways (ACTION_MOVE actions), and the up event (an ACTION_UP action) of the finger all create MotionEvent objects.

You could receive quite a few ACTION_MOVE events as the finger moves across the surface before you receive the final ACTION_UP event. Each MotionEvent object contains information about what action is being performed, where the touch is taking place, how much pressure was applied, how big the touch was, when the action occurred, and when the initial ACTION_DOWN occurred. There is a fourth possible action, which is ACTION_CANCEL. This action is used to indicate that a touch sequence is ending without actually doing anything.

Finally, there is ACTION_OUTSIDE, which is set in a special case where a touch occurs outside of our window but we still get to find out about it.

Let's sum up all actions of touch event:

  • ACTION_DOWN. Initial event when the first finger hits the screen. This event is always the beginning of a new gesture.
  • ACTION_MOVE. Event that occurs when one of the fingers on the screen has changed location.
  • ACTION_UP. Final event, when the last finger leaves the screen. This event is always the end of a gesture.
  • ACTION_CANCEL. Received by child views when their parent has intercepted the gesture they were currently receiving. Like ACTION_UP, this should signal the view that the gesture is over from their perspective.
  • ACTION_POINTER_DOWN. Event that occurs when an additional finger hits the screen. Useful for switching into a multitouch gesture.
  • ACTION_POINTER_UP. Event that occurs when an additional finger leaves the screen. Useful for switching out of a multitouch gesture.

Detecting simple gesture

Double Tapping

You can enable double tap events for any view within your activity using the OnDoubleTapListener. First, copy the code for OnDoubleTapListener into your application and then you can apply the listener with:

myView.setOnTouchListener(new OnDoubleTapListener(this) {
    @Override
    public void onDoubleTap(MotionEvent e) {
        Toast.makeText(MainActivity.this, "Double Tap", Toast.LENGTH_SHORT).show();
    }
});

Now that view will be able to respond to a double tap event and you can handle the event accordingly.

Swipe Gesture Detection

Detecting finger swipes in a particular direction is best done using the built-in onFling event in the GestureDetector.OnGestureListener.

A helper class that makes handling swipes as easy as possible can be found in the OnSwipeTouchListener class. Copy the OnSwipeTouchListener class to your own application and then you can use the listener to manage the swipe events with:

myView.setOnTouchListener(new OnSwipeTouchListener(this) {
    @Override
    public void onSwipeDown() {
        Toast.makeText(MainActivity.this, "Down", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onSwipeLeft() {
        Toast.makeText(MainActivity.this, "Left", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onSwipeUp() {
        Toast.makeText(MainActivity.this, "Up", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onSwipeRight() {
        Toast.makeText(MainActivity.this, "Right", Toast.LENGTH_SHORT).show();
    }
});

With that code in place, swipe gestures should be easily manageable.

For more complected gesture you should use GestureDetector class.

GestureDetector class

Android provide the GestureDetector class which allow to consume MotionEvents and to create higher level gesture events to listeners.

The GestureDetector class receives motion events that correspond to a specific set of user gestures. Such as tapping down and up, swiping vertically and horizontally (fling), long and short press, double taps and scrolls. GestureDetector is powerful for these standard user interactions and is easy to set SimpleOnGestureListeners on Android UI elements.

The simplicity of this class lays in overriding the methods you need from the GestureListener and implementing the gesture functionality required without having to manually determine what gesture the user performed.

To use GestureDetector class follow steps bellow:

  1. Declare a class which implements the GestureDetector.OnGestureListener interface including the required onFling(), onDown(), onScroll(), onShowPress(), onSingleTapUp() and onLongPress() callback methods. Note that this can be either an entirely new class, or the enclosing activity class.
  2. Override View or Activity’s onTouchEvent(MotionEvent event) and pass event to GestureDetector.onTouchEvent(event).
  3. Implement GestureDetector.OnGestureListener callbacks for gestures.
  4. Implement GestureDetector.OnDoubleTapListener callbacks for click gestures.
  5. Or implement GestureDetector.SimpleOnGestureListener callbacks if process only a few gestures above.

It's common to use SimpleOnGestureListener which only needs to implement the gestures we need.

Introduced in API Level 1, the GestureDetector class can be used to detect gestures made by a single finger. Some common single-finger gestures supported by the GestureDetector class include:

  • onDown. Called when the user first presses the touchscreen.
  • onShowPress. Called after the user first presses the touchscreen but before lifting the finger or moving it around on the screen; used to visually or audibly indicate that the press has been detected.
  • onSingleTapUp. Called when the user lifts up (using the up MotionEvent) from the touchscreen as part of a single-tap event.
  • onSingleTapConfirmed. Called when a single-tap event occurs.
  • onDoubleTap. Called when a double-tap event occurs.
  • onDoubleTapEvent. Called when an event within a double-tap gesture occurs, including any down, move, or up MotionEvent.
  • onLongPress. Similar to onSingleTapUp, but called if the user holds down a finger long enough to not be a standard click but also without any movement.
  • onScroll. Called after the user presses and then moves a finger in a steady motion before lifting the finger. This is commonly called dragging.
  • onFling. Called after the user presses and then moves a finger in an accelerating motion before lifting it. This is commonly called a flick gesture and usually results in some motion continuing after the user lifts the finger.

You can use the interfaces available with the GestureDetector class to listen for specific gestures such as single and double taps (see GestureDetector.OnDoubleTapListener), as well as scrolls and flings (see the documentation for GestureDetector.OnGestureListener). The scrolling gesture involves touching the screen and moving a finger around on it. The fling gesture, on the other hand, causes (though not automatically) the object to continue to move even after the finger has been lifted from the screen. This gives the user the impression of throwing or flicking the object around on the screen.

Adding onFling (swipe gesture) gesture detector to an ImageView

To demonstrate gesture detection in Android, we will build a simple application to display a list of images to the user. The user will be able to cycle forward and backward through the messages using the fling gesture, which involves swiping a finger or stylus across the screen from left to right or right to left.

Together, view.GestureDetector and view.View.OnTouchListener are all that are required to provide ImageView with gesture functionality. The listener contains an onTouch() callback that relays each MotionEvent to the detector.

Layout is as simple as following.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/iv"
        android:src="@drawable/cat1"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_centerInParent="true"/>
</RelativeLayout>

Following is MainActivity.java.

public class MainActivity extends AppCompatActivity {
    ImageView detailImage;
    GestureDetector detector;
    View.OnTouchListener listener;

    int[] images = {R.drawable.cat1, R.drawable.cat2, R.drawable.city};
    int MIN_DISTANCE = 150;
    int OFF_PATH = 100;
    int VELOCITY_THRESHOLD = 75;
    int imageIndex = 0;

    String TAG = "DBG";

    class GalleryGestureDetector implements GestureDetector.OnGestureListener {
        @Override
        public boolean onDown(MotionEvent motionEvent) {
            return true;
        }

        @Override
        public void onShowPress(MotionEvent motionEvent) {}

        @Override
        public boolean onSingleTapUp(MotionEvent motionEvent) {
            return false;
        }

        @Override
        public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
            return false;
        }

        @Override
        public void onLongPress(MotionEvent motionEvent) {}

        @Override
        public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) {
            Log.d(TAG, "onFling");

            if (Math.abs(event1.getY() - event2.getY()) > OFF_PATH)
                return false;

            if (images.length != 0) {
                if (event1.getX() - event2.getX() > MIN_DISTANCE && Math.abs(velocityX) > VELOCITY_THRESHOLD) {
                    // Swipe left
                    imageIndex++;
                    if (imageIndex == images.length)
                        imageIndex = 0;
                    detailImage.setImageResource(images[imageIndex]);
                } else {
                    // Swipe right
                    if (event2.getX() - event1.getX() > MIN_DISTANCE && Math.abs(velocityX) > VELOCITY_THRESHOLD) {
                        imageIndex--;
                        if (imageIndex < 0) imageIndex =
                                images.length - 1;
                        detailImage.setImageResource(images[imageIndex]);
                    }
                }
            }
            return true;
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        detector = new GestureDetector(this, new GalleryGestureDetector());
        listener = new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return detector.onTouchEvent(event);
            }
        };

        detailImage = (ImageView) findViewById(R.id.iv);
        detailImage.setOnTouchListener(listener);
    }
}

The process of gesture detection in the preceding code begins when the OnTouchListener listener's onTouch() method is called. It then passes that MotionEvent to our gesture detector class, GalleryGestureDetector, which monitors motion events, sometimes stringing them together and timing them until one of the recognized gestures is detected. At this point, we can enter our own code to control how our app responds as we did here with the onDown(), onShowPress(), and onFling() callbacks. It is worth taking a quick look at these methods in turn.

It may seem, at the first glance, that the onDown() method is redundant; after all, it's the fling gesture that we are trying to catch. In fact, overriding the onDown() method and returning true from it is essential in all gesture detections as all the gestures begin with an onDown() event. Returning true tells the operating system that your code is interested in the remaining gesture events.

The purpose of the onShowPress() method may also appear unclear as it seems to do a little more than onDown(). This method is handy for adding some form of feedback to the user, acknowledging that their touch has been received. The Material Design guidelines strongly recommend such feedback.

Without including our own code, the onFling() method will recognize almost any movement across the bounding view that ends in the user's finger being raised, regardless of direction or speed. We do not want very small or very slow motions to result in action; furthermore, we want to be able to differentiate between vertical and horizontal movement as well as left and right swipes. The MIN_DISTANCE and OFF_PATH constants are in pixels and VELOCITY_THRESHOLD is in pixels per second. These values will need tweaking according to the target device and personal preference. The first MotionEvent argument in onFling() refers to the preceding onDown() event and, like any MotionEvent, its coordinates are available through its getX() and getY() methods.

In this example, we used GestureDetector.OnGestureListener to capture our gesture. However, the GestureDetector has three such nested classes, the other two being SimpleOnGestureListener and OnDoubleTapListener. SimpleOnGestureListener provides a more convenient way to detect gestures as we only need to implement those methods that relate to the gestures we are interested in capturing.

We can capture vertical and horizontal swipe

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
    if (Math.abs(e1.getY() - e2.getY()) > OFF_PATH || Math.abs(e1.getX() - e2.getX()) > OFF_PATH)
        return false;

    if (e1.getX() - e2.getX() > MIN_DISTANCE && Math.abs(velocityX) > VELOCITY_THRESHOLD) {
        // right to left swipe
        Toast.makeText(getApplicationContext(), "Swipe right to left", Toast.LENGTH_SHORT).show();
    } else if (e2.getX() - e1.getX() > MIN_DISTANCE && Math.abs(velocityX) > VELOCITY_THRESHOLD) {
        // left to right swipe
        Toast.makeText(getApplicationContext(), "Swipe left to right", Toast.LENGTH_SHORT).show();
    } else if (e1.getY() - e2.getY() > MIN_DISTANCE && Math.abs(velocityY) > VELOCITY_THRESHOLD) {
        // bottom to top
        Toast.makeText(getApplicationContext(), "Swipe bottom to top", Toast.LENGTH_SHORT).show();
    } else if (e2.getY() - e1.getY() > MIN_DISTANCE && Math.abs(velocityY) > VELOCITY_THRESHOLD) {
        // top to bottom
        Toast.makeText(getApplicationContext(), "Swipe top to bottom", Toast.LENGTH_SHORT).show();
    }

    return true;
}

We can capture scroll

@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    Log.d("Gesture ", " onScroll");
    if (e1.getY() < e2.getY()){
        Log.d(TAG, " Scroll Down");
    }
    if(e1.getY() > e2.getY()){
        Log.d(TAG, " Scroll Up");
    }
    return true;
}

Adding onScale (pinch gesture) gesture detector to an ImageView

Another useful gesture in any app is the ability to scale UI elements. Android provides the ScaleGestureDetector class to handle pinch gestures for scaling views. The following code is an implementation of a simple Android app that uses the ScaleListener class to perform the pinch gesture on an ImageView and scale it based on finger movements.

public class MainActivity extends Activity {
    private ImageView imageView;
    private float scale = 1f;
    private ScaleGestureDetector detector;
    String TAG = "DBG";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        imageView =(ImageView) findViewById(R.id.imageView);
        detector = new ScaleGestureDetector(this, new ScaleListener());
    }

    public boolean onTouchEvent(MotionEvent event) {
        // re-route the Touch Events to the ScaleListener class
        detector.onTouchEvent(event);
        return super.onTouchEvent(event);
    }

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        float onScaleBegin = 0;
        float onScaleEnd = 0;

        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            scale *= detector.getScaleFactor();
            imageView.setScaleX(scale);
            imageView.setScaleY(scale);
            return true;
        }

        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            Log.d(TAG, "onScaleBegin");
            onScaleBegin = scale;
            return true;
        }

        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
            Log.d(TAG, "onScaleEnd");
            onScaleEnd = scale;

            if (onScaleEnd > onScaleBegin) {
                Toast.makeText(getApplicationContext(), "Scaled Up by a factor of " + String.valueOf(onScaleEnd / onScaleBegin), Toast.LENGTH_SHORT).show();
            }

            if (onScaleEnd < onScaleBegin) {
                Toast.makeText(getApplicationContext(), "Scaled Down by a factor of " + String.valueOf(onScaleBegin / onScaleEnd), Toast.LENGTH_SHORT).show();
            }

            super.onScaleEnd(detector);
        }
    }
}

The ScaleListener class overrides three methods onScale, onScaleBegin and onScaleEnd. During the execution of the onScale method, the resizing of the imageView is performed based on the scale factor of the finger movement on the device screen.

Layout for this example is below.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="400dp"
        android:layout_height="400dp"
        android:layout_gravity="center"
        android:src="@drawable/cat1"/>

</LinearLayout>

How to draw on custom View

This snippet demonstrates the handling of (single) touch events within a custom view.

Create the following DrawOnTouchView class which implements a View which supports single touch.

public class DrawOnTouchView extends View {
    private Paint paint = new Paint();
    private Path path = new Path();
    Context context;
    String TAG = "DBG";

    GestureDetector gestureDetector;

    public TouchEventView(Context context, AttributeSet attrs) {
        super(context, attrs);
        gestureDetector = new GestureDetector(context, new GestureListener());
        this.context = context;

        paint.setAntiAlias(true);
        paint.setStrokeWidth(6f);
        paint.setColor(Color.BLACK);

        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeJoin(Paint.Join.ROUND);
    }

    private class GestureListener extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onDoubleTap(MotionEvent e) {
            float x = e.getX();
            float y = e.getY();

            // clean drawing area on double tap
            path.reset();
            Log.d(TAG, "Position: (" + x + "," + y + ")");

            return true;
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawPath(path, paint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float eventX = event.getX();
        float eventY = event.getY();

        gestureDetector.onTouchEvent(event);

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                path.moveTo(eventX, eventY);
                //Log.d(TAG, "ACTION_DOWN");
                return true;
            case MotionEvent.ACTION_MOVE:
                //Log.d(TAG, "ACTION_MOVE");
                path.lineTo(eventX, eventY);
                break;
            case MotionEvent.ACTION_UP:
                //Log.d(TAG, "ACTION_UP");
                break;
            default:
                return false;
        }
        invalidate();
        return true;
    }
}

Adjust the activity_main.xml layout file to the following.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <me.proft.todofirebase.DrawOnTouchView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </me.proft.todofirebase.DrawOnTouchView>

</LinearLayout>

Add this view to your activity.

public class MainActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new DrawOnTouchView(this, null));
    }
}

If you run your application you will be able to draw on the screen with your finger (or with the mouse in the emulator).

android_draw_on_touch.png

Useful links

comments powered by Disqus