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.

Saturday, September 5, 2009

Simple home screen widget

Overview

One of the great improvements in Android 1.5 is the capability of adding custom home screen widgets to you desktop. Prior to 1.5, only the standard widgets that came with the OS were available. Android 1.5 provides a simple way to create your own home screen widgets that allow functionality of your application to bleed out into other areas of the system and provide a more integrated user experience. In this tutorial however, we are going to just get something on the home screen. Once you have that, you can go ahead and add some more functionality which will be discussed in later posts. This is intended to just get you started.

Limitations

Before we begin with this brief tutorial however, you should be aware that there are some limitations to the home screen widgets. The limitation that disappointed me the most was the slim list of allowable UI widgets to choose from in building the widgets. I had hoped to build a custom search widget that would provide quick access to some information in my application, but alas, and EditText widget is not available in custom home screen widgets. With this limitation in mind, here is a complete list of compatible UI widgets that you can use:
Also note that descendants of these classes are also not allowed.

Getting Started

There are really only a few things you have to have in order to get a widget up on the home screen.
Now keep in mind that these are the bare bones minimum  requirements just to get something on the home screen. More options and functionality will be explored later.

AppWidgetProvider
First create a class that extends AppWidgetProvider. If you are using eclipse, once you have created the class you can right click in the window and select "Override/Implement methods". Go ahead and select all of the methods from AppWidgetProvider and click OK. You should have all the method stubs that you need to add any functionality you want to your widget. The main method in this class is onUpdate(). This method gets called whenever your application issues an update to the widget based on a timing definition set in you AppWidgetProviderInfo class. For now, just leave this class as it is since we won't be defining any real functionality for the widget just yet.

AppWidgetProviderInfo
This class is to be purely defined in xml. Example code is as follows:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="318px"
    android:minHeight="72px"
    android:updatePeriodMillis="86400000"
    android:initialLayout="@layout/searchwidget_layout"
    >
</appwidget-provider>

As you can see the provider info class sets up the container size and also defines the update interval for the widget. the value displayed is the millis for a whole day. Put this file into your res/xml folder.

XML Layout
This file simply defines the UI elements you are going to use and their organization. If you are not familiar with setting up layouts in XML, I would suggest you see the android developers site and learn how. The following example code uses are relative layout with 2 child, a text view and a button. Yes, unimaginative I know.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_height="wrap_content"
 android:layout_width="fill_parent"
 android:background="@drawable/widget_frame"
 android:focusable="true"
 android:textColor="#000">
 <TextView 
  android:layout_width="wrap_content"
  android:layout_height="fill_parent"
  android:layout_alignParentLeft="true"
  android:text="Click the button"
  android:layout_centerVertical="true"
  android:gravity="center"
  android:layout_marginLeft="20px"
  />
 
 <Button
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_alignParentRight="true"
  android:text="Click Me!"
  android:layout_centerVertical="true"
  android:layout_marginRight="20px"/>

</RelativeLayout>

Manifest Entry
In order for the home screen, which is the WidgetHost, to see your application's widget, you must define it in your Android Manifest file. Place the following node inside your Application tag.
<application
     ......
      .......>
<receiver android:name=".MyWidgetProviderClass" label="Name of my widget">
 <intent-filter>
  <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
 </intent-filter>
 <meta-data android:name="android.appwidget.provider"
      android:resource="@xml/widget_provider_info_file" />
</receiver>
 .....
 .....
 </application>

Wrap up
That's it. Run your application and long press on the home screen. Click on widgets and you should see your application's widget in the list. Click on it and admire it sitting there on your home screen doing . . . nothing. Yes the widget is worthless, and next time we'll actually give it some functionality, but for now, you should have a foundational knowledge of how to actually get your widget on the screen.