본문 바로가기

Android

[안드로이드] Gesture

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.