Search This Blog

Tuesday, January 19, 2010

Android Gestures

Like it or not touch screens are becoming part of both developers and users life, i mean dont think i would buy one of Those uncool phones without touchscreen unless i really have to, would you? but the important point is that what is the Touchscreen use if applications doesn't support touchscreen interaction, in other words who is really gonna pay for a ,say, Picture management application if it does not support some gestures for switching between pictures or zooming? [ Gesture Detection in Android ].
In Android there are three levels of touch screen event handling mechanism which can be used by developers. the most low level technique is to receive all touch events and take care of all things, you can attach an 'OnTouchListener' to a view and get notified whenever there is a touch event or you can override onTouchEvent() or dispatchTouchEvent() method of your activity or view, in all these cases you would be dealing with an instance of MotionEvent every single time and you would have to detect what user is doing all on your own which will suit requirments for developing games and stuff like that. But it is just too much hassle if you only need a few simple gestures for your application.
Next approach is to use GestureDetector class along with OnGestureListener and/or OnDoubleTapListener, in this technique whenever there is a new MotionEvent you have to pass it to Gesture Detector's onTouchEvent() method, it then will analyse this event and previous events and tell you what is happening on the screen by calling some of the callback methods.
here is a simple activity which uses GestureDetector :



public class SimpleActivity extends Activity implements OnGestureListener,
OnDoubleTapListener{

private GestureDetector detector;

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

detector = new GestureDetector(this,this);
}

@Override
public boolean onTouchEvent(MotionEvent me){
this.detector.onTouchEvent(me);
return super.onTouchEvent(me);
}

@Override
public boolean onDown(MotionEvent e) {
Log.d("---onDown----",e.toString());
return false;
}

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
Log.d("---onFling---",e1.toString()+e2.toString());
return false;
}

@Override
public void onLongPress(MotionEvent e) {
Log.d("---onLongPress---",e.toString());
}

@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
Log.d("---onScroll---",e1.toString()+e2.toString());
return false;
}

@Override
public void onShowPress(MotionEvent e) {
Log.d("---onShowPress---",e.toString());
}

@Override
public boolean onSingleTapUp(MotionEvent e) {
Log.d("---onSingleTapUp---",e.toString());
return false;
}

@Override
public boolean onDoubleTap(MotionEvent e) {
Log.d("---onDoubleTap---",e.toString());
return false;
}

@Override
public boolean onDoubleTapEvent(MotionEvent e) {
Log.d("---onDoubleTapEvent---",e.toString());
return false;
}

@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
Log.d("---onSingleTapConfirmed---",e.toString());
return false;
}

}


if we want to understand all gesture types and how they work, first of all we need to know three basic MotionEvents which can combine with each other and create some gestures, these three Events are Action_Down , Action_Move and Action_Up , each time you touch the screen an Action_Down occurs and when you start moving it will create Action_Move event and finally when you take your finger off the screen an Action_Up Event will be created.
onDown() is called simply when there is an Action_Down event.
onShowPress() is called after onDown() and before any other callback method, I found out it sometimes might not get called for example when you tap on the screen so fast, but it's actually what this method all about, to make a distinction between a possible unintentional touch and an intentional one.
onSingleTapUp() is called when there is a Tap Gesture. Tap Gesture happens when an Action_Down event followed by an Action_Up event(like a Single-Click).
onDoubleTap() is called when there is two consecutive Tap gesture(like a Double-Click).
onSingleTapConfirmed() is so similar to onSingleTapUp() but it is only get called when the detected tap gesture is definitely a single Tap and not part of a double tap gesture.
Here is the sequence in which these callback methods are called when we tap on the screen:

onDown() – onShowPress() - onSingleTapUp() – onSingleTapConfirmed()


and here is when we do a double tap:


onDown() – onShowPress() - onSingleTapUp() – onDoubleTap() – onDoubleTapEvent()
onDown() – onShowPress() – onDoubleTapEvent()


onFling() is called when a Fling Gesture is detected. fling Gesture occurs when there is an Action_Down then some Action_Move events and finally an Action_Up, but they must take place with a specified movement and velocity pattern to be considered as a fling gesture. for example if you put your finger on the screen and start moving it slowly and then remove your finger gently it won’t be count as a fling gesture.

onScroll() is usually called when there is a Action_Move event so if you put your finger on the screen and move it for a few seconds there will be a method call chain like this :


onDown() – onShowPress() – onScroll() - onScroll() - onScroll() - ....... - onScroll()


or If the movement was a Fling Gesture, then there would be a call chain like this :


onDown() – onShowPress() – onScroll() - onScroll() - onScroll() - ....... – onFling()

If there is an Action_Move event between first tap and second tap of a doubleTap gesture it will be handled by calling onDoubleTapEvent() instead of onScroll() method. onDoubleTapEvent() receives all Action_Up events of a doubleTap gesture as well.
Remember that if we touch the screen and don’t remove our finger for a specified amount of time onLongPress() method is called and in most cases there will be no further event callback regardless of whatever we do after that, moving or removing our finger. we can easily change this behavior of detector by calling setIsLongpressEnabled() method of GestureDetector class.
although GestureDetector makes our life much easier, it still could be a real pain in the ass if we would need to handle some complicated gestures, imagine you need an application which should do task1 when there is a circle gesture, task2 for rectangle gesture and task3 for triangle gesture. obviously it would not be so pleasant to deal with such a situation with those mechanism we have seen so far, it's actually when Gesture API and Gesture Builder Application come into play.
Gesture Builder Application comes with Android emulator as a pre-installed app, It helps us to simply make new gestures and save them on the device, then we can retrieve and detect those gestures later using Gesture API. I'm not gonna talk about Gesture Builder app here,since there is a pretty good Article about it on Android Developers blog.
the only thing I'd like to mention here is GestureOverlayView class, It is actually just a transparent FrameLayout which can detect gestures but the thing is it can be used in two completely different ways, you can either put other views inside it and use it as a parent view or put it is the last child view of a FrameLayout (or any other way which causes it to be placed on top of another view).
In the first Scenario all child views will receive Touch events, therefore we will be able to press buttons or interact with other widgets as well as doing some gestures, on the other hand if GestureOverlayView has been placed on top, it will swallow all Touch events and no underlay view will be notified for any touch Event.
Although Gesture API brings many useful features for us, I personally prefer to use GestureDetector for some simple, basic gestures and honestly I feel like something is missing here, I mean , apart from games, I would say more than 70% of all gestures that might be needed in our applications are just a simple sliding in different directions or a double tap. and that's why I have decided to come up with something easy which can enable us to handle those 70% as simple as possible. how simple? you might be asking...
here is how our previous activity will look like if we use SimpleGestureFilter class :



public class SimpleActivity extends Activity implements SimpleGestureListener{

private SimpleGestureFilter detector;

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

detector = new SimpleGestureFilter(this,this);
}

@Override
public boolean dispatchTouchEvent(MotionEvent me){
this.detector.onTouchEvent(me);
return super.dispatchTouchEvent(me);
}

@Override
public void onSwipe(int direction) {
String str = "";

switch (direction) {

case SimpleGestureFilter.SWIPE_RIGHT : str = "Swipe Right";
break;
case SimpleGestureFilter.SWIPE_LEFT : str = "Swipe Left";
break;
case SimpleGestureFilter.SWIPE_DOWN : str = "Swipe Down";
break;
case SimpleGestureFilter.SWIPE_UP : str = "Swipe Up";
break;

}
Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
}

@Override
public void onDoubleTap() {
Toast.makeText(this, "Double Tap", Toast.LENGTH_SHORT).show();
}

}


and here is SimpleGestureFilter source code :



public class SimpleGestureFilter extends SimpleOnGestureListener{

public final static int SWIPE_UP = 1;
public final static int SWIPE_DOWN = 2;
public final static int SWIPE_LEFT = 3;
public final static int SWIPE_RIGHT = 4;

public final static int MODE_TRANSPARENT = 0;
public final static int MODE_SOLID = 1;
public final static int MODE_DYNAMIC = 2;

private final static int ACTION_FAKE = -13; //just an unlikely number
private int swipe_Min_Distance = 100;
private int swipe_Max_Distance = 350;
private int swipe_Min_Velocity = 100;

private int mode = MODE_DYNAMIC;
private boolean running = true;
private boolean tapIndicator = false;

private Activity context;
private GestureDetector detector;
private SimpleGestureListener listener;


public SimpleGestureFilter(Activity context,SimpleGestureListener sgl) {

this.context = context;
this.detector = new GestureDetector(context, this);
this.listener = sgl;
}

public void onTouchEvent(MotionEvent event){

if(!this.running)
return;

boolean result = this.detector.onTouchEvent(event);

if(this.mode == MODE_SOLID)
event.setAction(MotionEvent.ACTION_CANCEL);
else if (this.mode == MODE_DYNAMIC) {

if(event.getAction() == ACTION_FAKE)
event.setAction(MotionEvent.ACTION_UP);
else if (result)
event.setAction(MotionEvent.ACTION_CANCEL);
else if(this.tapIndicator){
event.setAction(MotionEvent.ACTION_DOWN);
this.tapIndicator = false;
}

}
//else just do nothing, it's Transparent
}

public void setMode(int m){
this.mode = m;
}

public int getMode(){
return this.mode;
}

public void setEnabled(boolean status){
this.running = status;
}

public void setSwipeMaxDistance(int distance){
this.swipe_Max_Distance = distance;
}

public void setSwipeMinDistance(int distance){
this.swipe_Min_Distance = distance;
}

public void setSwipeMinVelocity(int distance){
this.swipe_Min_Velocity = distance;
}

public int getSwipeMaxDistance(){
return this.swipe_Max_Distance;
}

public int getSwipeMinDistance(){
return this.swipe_Min_Distance;
}

public int getSwipeMinVelocity(){
return this.swipe_Min_Velocity;
}


@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {

final float xDistance = Math.abs(e1.getX() - e2.getX());
final float yDistance = Math.abs(e1.getY() - e2.getY());

if(xDistance > this.swipe_Max_Distance || yDistance > this.swipe_Max_Distance)
return false;

velocityX = Math.abs(velocityX);
velocityY = Math.abs(velocityY);
boolean result = false;

if(velocityX > this.swipe_Min_Velocity && xDistance > this.swipe_Min_Distance){
if(e1.getX() > e2.getX()) // right to left
this.listener.onSwipe(SWIPE_LEFT);
else
this.listener.onSwipe(SWIPE_RIGHT);

result = true;
}
else if(velocityY > this.swipe_Min_Velocity && yDistance > this.swipe_Min_Distance){
if(e1.getY() > e2.getY()) // bottom to up
this.listener.onSwipe(SWIPE_UP);
else
this.listener.onSwipe(SWIPE_DOWN);

result = true;
}

return result;
}

@Override
public boolean onSingleTapUp(MotionEvent e) {
this.tapIndicator = true;
return false;
}

@Override
public boolean onDoubleTap(MotionEvent arg0) {
this.listener.onDoubleTap();;
return true;
}

@Override
public boolean onDoubleTapEvent(MotionEvent arg0) {
return true;
}

@Override
public boolean onSingleTapConfirmed(MotionEvent arg0) {

if(this.mode == MODE_DYNAMIC){ // we owe an ACTION_UP, so we fake an
arg0.setAction(ACTION_FAKE); //action which will be converted to an ACTION_UP later.
this.context.dispatchTouchEvent(arg0);
}

return false;
}


static interface SimpleGestureListener{
void onSwipe(int direction);
void onDoubleTap();
}

}


as you can see clients of these class can determine the minimum and maximum distance and also minimum velocity which is required for a movement on screen to be considered as a Swipe Gesture, I also thought it would be great if our filter can behave differently like what GestureOverlayView can do and even more than that!
this Filter can run in three different mode: Transparent, Solid and Dynamic. in Transparent mode it will work just like when we have a GestureOverlayView as parent: all views will receive Touch events; Solid mode works like when we put a GestureOverlayView as a child view: no one will receive TouchEvent, it is not as efficient as GestureOverlayView is, since we actually let all events get passed but what we do is we literally kill them when they are passing through our filter ;).
the last mode is Dynamic mode, the primary purpose of this mode is to have a bit smarter gesture detection, i mean there has been sometimes that i wanted to slide from one page to another, but a button get pressed and something else happens. it does not happen so much but it is really annoying. what i tried to do in Dynamic mode is to distinguish between a swipe/double tap gesture and a movement which is neither of them. so if you have a view full of buttons and other interactive stuff and user does a swipe or double tap gesture, it is guaranteed (although i believe there is no such thing as guarantee in life ;) ) that no other event callback will be called but only onSwipe() or onDoubleTap().
Anyway that's what i came up with to take the pain away in those circumstances when we just need to handle some simple Gestures.
hope it will be helpful for you and can make your life a bit easier.

40 comments:

Anonymous said...

Very nice code !!! THANK YOU!! Works as advertised... which is getting rare these days :) :)

Anonymous said...

Amir, with:

detector.setEnabled(true);
and
detector.setMode(SimpleGestureFilter.MODE_TRANSPARENT);

the SimpleGestureListener still consumes all events - buttons and views behind don't recieve anything. Am I using it right? thanks.

Amir said...

Yes...I think I stuffed up ;)
Actually when you override dispatchTouchEvent() method, you have to call super.dispatchTouchEvent() not super.onTouchEvent()!!!
I changed the code so it should be OK now.
I have really no idea how it could have happened...but anyway thank you for letting me know.
hope you can use it without any problem.

Anonymous said...

YES!!! Thank you that was it. Works perfectly now. You the man Amir. Great work

Louenas said...

Great post. thanks a lot for sharing
Louenas.

Umar said...

it is the best code i have ever seen..

i was struggling with left and right movement and finally you gave me this solution

one again thank you very very much.

Jeff said...

very tutorial, thanks your sharing.

SCJP_ANIL said...

Nice tutorial buddy.... Very helpful

Anonymous said...

Hi, awesome tutorial. Unfortunately, using your code makes the buttons I have on my activity extremely unresponsive (Nexus one 2.2.1).

I haven't changed anything in your code. It is just weird, only once in a while does a button generate an onclick event. It does dispatch a touch event on every release. Any ideas what I can try?

Anonymous said...

Hi again. My buttons are textual and not very big. I just increased the amount of whitespace around them and now it works fine. So nothing to do with your excellent code ;)

Randy McEoin said...

Thank you for the post. I used it to solve a problem I had with swiping database entries.

Randy

badri said...

Hi Amir

I have a grid view(3X3) consisting of buttons.

Whenever the user moves his fingers on this grid I want to know the
button underneath his motion event at any give instant.

basically what I am trying to do is...each button is associated with
1 .wav file. SO whenever user makes gestures on this grid , I want to
detect the button and play that sound.

Please let me know how it can be done.


Thanks
badri

Anonymous said...

Wonderfull !! very good example

Girish H

Anonymous said...

Hi Amir,
really wonderful post out there. ur idea of creating a custom class to filter out the non- required touch events is really good. but i have a small problem when i am trying to implement ur idea into my android proj. i have an activity which inflates main.xml during startup. this main. xml has a gestureOverlayView as a parent View , so that i can draw gestures. here when i double or single tap , the required onSingleTap / onDoubleTap methods are not even called. it seems as if when the gestureOverlayView is laid on a xml. all touch events are dispatched to the gestureListener of gestureOverlayView. how do i then call methods on doubleTap and all.. pls help me here..

potato0 said...

Fantastic tutorial. The class you've written here is by far the best solution to gesture detection I've seen. Thanks!

Omar Rehman said...

Great example Amir. Thanks. However, i am having problem when the my Activity is added on a TabActivity. All the events are working properly when the Activity is opened separately however when Activity is added in a TabActivity the events are not triggered. Can you please help how can i pass my events from TabActivity to the child Activity?

Omar Rehman said...

In response to my comment above. I solved the issue by adding a scrollview in my layout file and all the events are now available in child activity of my tab. Might be helpful for someone else. :)

Unknown said...
This comment has been removed by the author.
Anonymous said...

man, this is really helpful and cool.
Gianpaolo

Anonymous said...

The code works great but I have one issue. I have an ImageView with a picture and when I swipe, I want the picture to change. Even when I play around with different modes (Transparent, Solid, etc.) when I gesture over the image, it doesn't seem to register. When I gesture outside the bounds of the image, it works fine. Any help? Thanks!!!

Anonymous said...

Thanks a lot this one is really helpfull

moncherion said...

I have tried making use of the setSelector method, but no luck.

Anonymous said...

What ist "SimpleGestureListener"? I can't implement this.

Peter said...

Excellent tutorial, thanks Amir, your code works great and is easy to understand! WIN

joker said...

Thanks a lot..it works perfectly...before this i tried many things to acheive this functionality..i couldn't..this what i exactly need

Eric JOYÉ said...

Thank you very much for sharing! I won a few precious hours or even days! Good luck to you!

Merci beaucoup pour ce partage! J'ai gagné quelques heures ou même jours. Bonne chance à toi !

Morgane said...

Thanks a lot for sharing this! It was very helpful to me :)

Anonymous said...

Great, thank you very much. Fortunately there are people like you!

Android app developer said...

I like your blog effort .This is one of the suitable post.Good .Keep it up.

Anonymous said...

THANK U

Punit said...

Thanks a lot Amir for sharing the article. Came here to learn about onFling() , learned a lot more :)

Hadi said...

Nice article... Thanks a lot

Anonymous said...

thanks you for this. It really helped me.

Anonymous said...

Really good. Thanks!

Kashinoda said...

Thank you so much!!

Disiento con usted said...

Thank you very much!!!

Anonymous said...

Thanks, great code. This is helped me a lot and saved my time.

acidkool said...

Thank you, this is what I'm looking for. Cheers :) :thumbsup:

Anonymous said...

Helped me a lot, you made an awesome work!
Thx!

Anonymous said...

thanku