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
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 View
s via the onTouchEvent()
method. To react to touch events you override the onTouchEvent()
method. This method is called whenever a touch needs to be handled - it returns true
if the view that calls onTouchEvent
handles the touch.
The View
class belongs to the Android package android.view
, and the following list contains some of the touch-related methods in the View
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 eventNote 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:
GestureDetector.OnDoubleTapListener
, which is used to notify the occurrence of a double-tap or a confirmed single-tap.GestureDetector.OnGestureListener
, which is used to notify the occurrence of gestures.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:
GestureDetector
for your View
.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.Selected methods of the MotionEvent
class:
getRawX()
returns the x-coordinate of the touch within the screen.getRawY()
returns the y-coordinate of the touch within the screen.getX()
returns the x-coordinate of the touch within the View
where it happened.getY()
returns the y-coordinate of the touch within the View
where it happened.getAction()
returns the type of action that occurred within the touch event.The getX()
and getY()
methods return the x- and y-coordinates relative to the View
where the event happened, which is the View v
passed to onTouch
as the first parameter. The getRawX
and getRawY
return the x- and y-coordinates within the screen, including the screen decorations such as the status bar and the action bar. If we use the getRawX()
and getRawY()
methods and we want the x- and y-coordinates relative to the content View
, we can use the following code inside onTouch to convert them:
int [] location = new int[2]; // view represents the content view for the activity view.getLocationInWindow(location); float relativeX = event.getRawX() - location[0]; float relativeY = event.getRawY() - location[1];
The getLocationInWindow()
method of the View
class accepts an array of two ints as a parameter, and sets its two values to the x- and y-coordinates of the top left corner of the View
calling that method within the window. We can then subtract their values from the values returned by getRawX()
and getRawY()
to obtain the relative x- and y-coordinates of the touch event within the content view of the activity.
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:
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.View
or Activity
’s onTouchEvent(MotionEvent event)
and pass event to GestureDetector.onTouchEvent(event)
.GestureDetector.OnGestureListener
callbacks for gestures.GestureDetector.OnDoubleTapListener
callbacks for click gestures.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).
Swipe Down to Dismiss
Many apps use the swipe down to dismiss. To implement this, first, you want to detect where the finger started on the screen. We capture the initial Y coordinate so that we can know if the user moved down, to the side, or up.
If the user lifts their finger while it was still in the view, we then calculate the difference between the initial Y and ending Y, and if it’s greater than a specific range, we start the animation to set the view down.
@Override public boolean onTouchEvent(MotionEvent motionEvent) { if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { initialDownYCoord = motionEvent.getRawY(); } if (motionEvent.getAction() == MotionEvent.ACTION_UP && isTouchInView(motionEvent)) { if (motionEvent.getRawY() - initialDownYCoord > SWIPE_DOWN_RANGE) { startAnimation(slide_down); return true; } } return super.onTouchEvent(motionEvent); }
dispatchTouchEvent() method
Another method to consider when we do touches and views is the dispatchTouchEvent
. dispatchTouchEvent
passes a MotionEvent
to the target view and returns true if it’s handled by the current view and false otherwise. And it can be used to listen to events, spy on it and do stuff accordingly.
For every MotionEvent
, dispatchTouchEvent
is called from the top of the tree to the bottom of the tree, which means every single view will call dispatchTouchEvent
if nothing else is intercepted.
Useful links