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.