Monday, September 21, 2009

Implementing a swipe gesture

One of the main difficulties in developing an application on any mobile platform is screen space. You will find that it is fairly easy to implement navigation controls in your app through the use of simple buttons. However you will also find that using one button lends itself to using more buttons and soon it is that a good portion of your precious screen real estate is taken up by navigation controls. Such was the case with my application, a scripture reader. I needed a way to navigate from one chapter to the next without having to use up screen space with a button. So I decided upon a swipe gesture as if turning a page to go from one chapter to the next. To accomplish this I use a ViewSwitcher widget along with Android built in gesture detection support.

ViewSwitcher

Below is a slideshow that shows the basic theory of operation. Just click through the slides.


The layout xml code:
<ViewFlipper xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/flipper"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ScrollView android:id="@+id/scripScroll0"
android:layout_below="@id/navBar"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#fff"
android:fadingEdgeLength="6px"
>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView android:id="@+id/text0"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="15sp"
android:layout_marginLeft="5px"
android:layout_marginRight="5px"
android:textColor="#111"
android:typeface="serif"
/>
</RelativeLayout>
</ScrollView>
<ScrollView android:id="@+id/scripScroll1"
android:layout_below="@id/navBar"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#fff"
android:fadingEdgeLength="6px"
>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView android:id="@+id/text1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="15sp"
android:layout_marginLeft="5px"
android:layout_marginRight="5px"
android:textColor="#111"
android:typeface="serif"
/>
</RelativeLayout>
</ScrollView>
<ScrollView android:id="@+id/scripScroll2"
android:layout_below="@id/navBar"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#fff"
android:fadingEdgeLength="6px"
>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView android:id="@+id/text2"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="15sp"
android:layout_marginLeft="5px"
android:layout_marginRight="5px"
android:textColor="#111"
android:typeface="serif"
/>
</RelativeLayout>
</ScrollView>
</ViewFlipper> 

 
The ViewFlipper contains 3 views that each contain a scrollView widget and inside of that is a RelativeLayout with a TextView
inside of that. In your code you will have to capture the swipe gesture and have it trigger your ViewFlipper to advance
to the next view. Keeping track of your views is a little tricky and I'll show you how I do it a little later.
 
This is how you do the gesture detection. First you need to set up some constants so that we can
find out if the touch event was a legitimate horizontal swipe gesture. So place the following in your variable declaration.

private Animation slideLeftIn;
private Animation slideLeftOut;
private Animation slideRightIn;
private Animation slideRightOut;

//swipe gesture constants
private static final int SWIPE_MIN_DISTANCE = 120;
private static final int SWIPE_MAX_OFF_PATH = 250;
private static final int SWIPE_THRESHOLD_VELOCITY = 200;
private GestureDetector gestureDetector; 

 
Next, you need to set up the animations and capture your xml layout elements in your onCreate() method.

scroller0 = (ScrollView)findViewById(R.id.scripScroll0);
scroller1 = (ScrollView)findViewById(R.id.scripScroll1);
scroller2 = (ScrollView)findViewById(R.id.scripScroll2);

flipper = (ViewFlipper)findViewById(R.id.flipper);//you will need to use the flipper object later in your SimpleOnGestureListener class to flip the views
slideLeftIn = AnimationUtils.loadAnimation(this, R.anim.slide_left_in);
slideLeftIn.setAnimationListener(new ScrollLeft());
slideLeftOut = AnimationUtils.loadAnimation(this, R.anim.slide_left_out);
slideRightIn = AnimationUtils.loadAnimation(this, R.anim.slide_right_in);
slideRightIn.setAnimationListener(new ScrollRight());
slideRightOut = AnimationUtils.loadAnimation(this, R.anim.slide_right_out);


the following sets up your event listeners on your scrollers. This also goes in your onCreate method.

gestureDetector = new GestureDetector(new MyGestureDetector());

scroller0.setOnTouchListener(new View.OnTouchListener() {

@Override
public boolean onTouch(View v, MotionEvent event) {
if (gestureDetector.onTouchEvent(event))
return true;
else
return false;
}
});
scroller1.setOnTouchListener(new View.OnTouchListener() {

@Override
public boolean onTouch(View v, MotionEvent event){
if (gestureDetector.onTouchEvent(event))
return true;
else
return false;
}
});
scroller2.setOnTouchListener(new View.OnTouchListener() {

@Override
public boolean onTouch(View v, MotionEvent event) {
if (gestureDetector.onTouchEvent(event))
return true;
else
return false;
}
});


 
Create a SimpleOnGestureListener like the following. You only need to override the onFling() method. Here we use the values declared earlier to find out if it is a left or right swipe.


Gesture Detection:
class MyGestureDetector extends SimpleOnGestureListener {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
// int delta = 0;
if (Math.abs(e1.getY() - e2.getY()) > SWIPE_MAX_OFF_PATH)
return false;
else{
try {
// right to left swipe
if(e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
if(canFlipRight()){
flipper.setInAnimation(slideLeftIn);
flipper.setOutAnimation(slideLeftOut);
flipper.showNext();
}else{
return false;
}
//left to right swipe
} else if (e2.getX() - e1.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
if(canFlipLeft()){
flipper.setInAnimation(slideRightIn);
flipper.setOutAnimation(slideRightOut);
flipper.showPrevious();
}else{
return false;
}
}
} catch (Exception e) {
// nothing
}
return true;
}
}
}


Animation listener
the following is a very important component. Without it you will certainly get it to work, but it will work very slowly and your application will probably crash. This is because the Android UI framework is single threaded. No other threads are allowed to touch UI objects. As such, any processing that you need to do as a result of a UI event must therefore be done on the UI thread if it's going to affect any UI objects. In our project this is exactly the case. When you fling left or right, your code tells the flipper to change positions. Then your program needs to load the appropriate page onto the appropriate view. This work is probably very expensive if you have any SpannableStringBuilders or any HTML parsing. This means that the event will fire and all of that processing will be done before the UI changes regardless of where you stick the code. I lost many a sleepless night to this problem, so hopefully you won't have to. Luckily for us, the UI framework offers a few multi-threaded helper functions that allow you to do work outside the main UI thread, and then merge the changes back onto the UI thread. The one that I use is .postDelayed() as seen below. I'm using this particular one because for some odd reason the regular post() method ran my code just a fraction of a second too soon. So use the postDelayed() in this case. you can find documentation on all of these methods in the View object documentation . You will notice that back in our onCreate() method, these two classes were given to the flipper. The flipper fires the onAnimationEnd and then the chapter configuration method runs to guarantee that the Animation runs first -- providing a more seamless user experience.

class ScrollRight implements AnimationListener{

@Override
public void onAnimationEnd(Animation animation) {
flipper.postDelayed(new Runnable(){
@Override
public void run(){
configureChapters(-1);
}
}, 10);
}

@Override
public void onAnimationRepeat(Animation animation) {
// TODO Auto-generated method stub

}

@Override
public void onAnimationStart(Animation animation) {
// TODO Auto-generated method stub

}

}

class ScrollLeft implements AnimationListener{

@Override
public void onAnimationEnd(Animation animation) {
flipper.postDelayed(new Runnable(){
@Override
public void run(){
configureChapters(1);
}
}, 10);
}

@Override
public void onAnimationRepeat(Animation animation) {
// TODO Auto-generated method stub

}

@Override
public void onAnimationStart(Animation animation) {
// TODO Auto-generated method stub

}

}


Maybe the most difficult part about this whole feature is keeping track of what views to load and where. The following is the method I wrote for accomplishing it. The delta value can either be 0,1,or -1 for initial, right, and left movement respectively. The onCreate() method calls this when the activity loads and passes a delta value of 0 so that the initial chapter is fetched and presented and the appropriate previous and next chapters and set up. After this initial load, the ScrollRight and ScrollLeft classes call this method exclusively. It is just a matter of managing which variable points to which textView object. you either need to reset the previous or next textViews and then reset the aliases to reflect your new position. Be careful to do all of this in the correct order though so that you don't create circular references and find some strange things happening in your program.

private void configureChapters(int delta){
if(delta == 0){//this is initial loading value only


String current = getFormattedText(chapterNum);
String previous = "";
if(canFlipLeft()){
previous = getFormattedText(chapterNum - 1);
}
String next = "";
if(canFlipRight()){
next = getFormattedText(chapterNum + 1);
}
text0.setText(current);
text1.setText(next);
text2.setText(previous);

//set aliases
currentTextView = text0;
nextTextView = text1;
previousTextView = text2;
}
if(delta == 1){

int nextChapter = chapterNum +1;

TextView currTemp = currentTextView;
TextView prevTemp = previousTextView;
TextView nextTemp = nextTextView;

chapterNum = nextChapter;
String next = "";
if(canFlipRight()){
next = getFormattedText(chapterNum+1);
}

previousTextView.setText(next);

//reset aliases
previousTextView = currTemp;
currentTextView = nextTemp;
nextTextView = prevTemp;

}
if(delta == -1){
chapterNum = chapterNum - 1;

TextView currTemp = currentTextView;
TextView prevTemp = previousTextView;
TextView nextTemp = nextTextView;

String previous = "";
if(canFlipLeft()){
previous = getFormattedText(chapterNum - 1);
}

nextTextView.setText(previous);

//reselt aliases
currentTextView = prevTemp;
nextTextView = currTemp;
previousTextView = nextTemp;
}

// Set text on nav bar
navText.setText(subdiv);
if (numChapters > 1) {
navText.append(" " + chapterNum);
}
}


And that's all I have. I tried a number of tings including rewriting the Android ScrollView so that it would support horizontal scrolling(which it currently doesn't). But in the end, this was the least complicated and it works extremely fast, just give it a shot. You'll be glad you did. If anyone comes up with a better way, please post a comment and let me know how you did it. Thank you.

20 comments:

  1. Thanks for your blog, i find in a forum the solution for scroll with swipe gesture compatible :

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev){
    super.dispatchTouchEvent(ev);
    return gestureScanner.onTouchEvent(ev);
    }

    ReplyDelete
  2. Thx for this very interesting tuto. Was very helpful for me.

    ReplyDelete
  3. Can you please post the full source code ?
    I have implemented a listview where each list item is a flipper. ListView has a array adapter to customize the look and feel of each list item.

    I am trying to implement the swipe gesture for each list item, so that when I swipe the list item, only that list item flips to show additional information.
    Do you know how that can be achieved ?

    ReplyDelete
  4. Could you post the XML for the animations? slide_left_in, etc.?

    ReplyDelete
  5. This comment has been removed by the author.

    ReplyDelete
  6. the tutorial seems gr8 ..
    but can u provide the source code ..
    thanks

    ReplyDelete
  7. I checked this code.. but still Im getting a Null Pointer Exception.Can anyone pls upload a working project for this... Thank You

    ReplyDelete
  8. i am checking this code but i'm getting ...Runtime exception... can any one give source code of this one..

    ReplyDelete
  9. yes i got the solution

    this one is right as Raphael said, thanks

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev){
    super.dispatchTouchEvent(ev);
    return gestureScanner.onTouchEvent(ev);
    }

    ReplyDelete
  10. Just wondering if you might be able to share your source code? Thanks.

    ReplyDelete
  11. Too bad you don't want to share the full source code. Spending more time on completing/fixing your code than on trying to understand your ideas.

    ReplyDelete
  12. useless code , again and again , because not full source code !

    ReplyDelete
  13. Its a good explanation and i can easily to understood your information. so thanks for providing this good knowledge about android.

    Android app developer

    ReplyDelete
  14. Come on guys its a good tutorial, you should a least be motivated enough to understand basics to use it. There'd be different ways to implement his code-pieces.

    Thanks for the nice explanation, gonny try this soon :-)

    ReplyDelete
  15. hey..developer can you send me demo file contain this swipe activty..
    please mail to my id.
    its very helpfulll for make my application.
    thank you soo much in advance..
    my is is:
    shah.jai75@gmail.com

    ReplyDelete
  16. hello friend
    can u please send me the source code of it at santosh.suchit@gmail.com
    Thank you

    ReplyDelete
  17. hello friend
    can u please send me the source code of it at santosh.suchit@gmail.com
    Thank you

    ReplyDelete
  18. A Smartphone has a working framework that gives it a chance to track various provisions. Case in point, Apple's iphone run on ios. Blackberry Smartphones run on the Blackberry OS and different units run Google's Android OS, HP's web OS, and Microsoft's Windows Phone.
    Develop Android Apps // Mobile Application Development // Android Application Development

    ReplyDelete
  19. For screen resolutions, this application is great achievement in android app development in which developer easy to implement navigation controls in this app using simple buttons. It is also easy to control screen for users.

    ReplyDelete