ViewPager / ViewPagerIndicator

ViewPager – Part 3

Important note: There is a more recent article which covers a newer version of ViewPagerIndicator. Please refer to this in conjunction with this article

At the end of part 2 of this series, we had added ListView controls to each of our ViewPager views, but we lost the scroll position when we moved away from the view. Let’s look at how we can implement the scroll state persistence.

We need to store the scroll position for each of our views within the ViewPager, and we’ll create a simple integer array to hold these values, which we initialise in our constructor:

[java] public class ViewPagerAdapter extends PagerAdapter implements TitleProvider
{
.
.
.

private int[] scrollPosition = new int[titles.length];

public ViewPagerAdapter( Context context )
{
this.context = context;
for ( int i = 0; i < titles.length; i++ ) { scrollPosition[i] = 0; } } . . . } [/java] Next we need to add an OnScrollListener to each ListView which will detect scroll events, and save the first visible item when the scroll changes (lines 21-37). Also we need to set the scroll position using setSelection() on the ListView when we instantiate a new one (line 20). This is all done in the instantiateItem() method of ViewPagerAdapter:

[java] @Override
public Object instantiateItem( View pager, final int position )
{
ListView v = new ListView( context );
String[] from = new String[] { “str” };
int[] to = new int[] { android.R.id.text1 };
List> items =
new ArrayList>();
for ( int i = 0; i < 20; i++ ) { Map map =
new HashMap();
map.put( “str”, String.format( “Item %d”, i + 1 ) );
items.add( map );
}
SimpleAdapter adapter = new SimpleAdapter( context, items,
android.R.layout.simple_list_item_1, from, to );
v.setAdapter( adapter );
( (ViewPager) pager ).addView( v, 0 );
v.setSelection( scrollPosition[ position ] );
v.setOnScrollListener( new OnScrollListener()
{
@Override
public void onScrollStateChanged( AbsListView view,
int scrollState )
{
}

@Override
public void onScroll( AbsListView view,
int firstVisibleItem,
int visibleItemCount,
int totalItemCount )
{
scrollPosition[ position ] = firstVisibleItem;
}
} );
return v;
}
[/java]

If you try this, it does what we wanted: If you scroll the ListView on Page 1, then switch to Page 2, then Page 3, then Page 2, and back to Page 1, the first visible item in the ListView on Page 1 is preserved.

For completeness, we should really persist our scroll positions so that we remember the scroll positions if the Activity holding our ViewPager is destroyed and restored.

Firstly, we need to declare a new Parcelable class which will store our scroll state. Let’s call this ScrollState:

[java] package com.stylingandroid.viewpager;

import android.os.Parcel;
import android.os.Parcelable;

public class ScrollState implements Parcelable
{
private int[] scrollPos;

public static Parcelable.Creator CREATOR =
new Parcelable.Creator()
{

@Override
public ScrollState createFromParcel( Parcel source )
{
int size = source.readInt();
int[] scrollPos = new int[ size ];
source.readIntArray( scrollPos );
return new ScrollState( scrollPos );
}

@Override
public ScrollState[] newArray( int size )
{
return new ScrollState[ size ];
}
};
public ScrollState( int[] scrollPos )
{
this.scrollPos = scrollPos;
}

@Override
public int describeContents()
{
return 0;
}

@Override
public void writeToParcel( Parcel dest, int flags )
{
dest.writeInt( scrollPos.length );
dest.writeIntArray( scrollPos );
}

public int[] getScrollPos()
{
return scrollPos;
}
}
[/java]

This is effectively a simple Java Bean which implements Parcelable and declares the required CREATOR method. If you are unfamiliar with how to implement Parcelable, there is a good description here.

Now that we have a mechanism for persisting state, let’s add implementations for saveState() and restoreState() to our ViewPagerAdapter:

[java] @Override
public void restoreState( Parcelable p, ClassLoader c )
{
if ( p instanceof ScrollState )
{
scrollPosition = ( (ScrollState) p ).getScrollPos();
}
}

@Override
public Parcelable saveState()
{
return new ScrollState( scrollPosition );
}
[/java]

And we’re done. This will now persist our scroll positions even if the parent Activity is destroyed and re-created. It is worth noting that this implementation does not store the precise scroll position, it simply ensures that the first visible item in the ListView remains visible.

The final thing that we’ll look at is how to make the ViewPagerIndicator look a little more like the swipey tabs which feature in the current version of the Android Market app.

Before we proceed, it is worth mentioning that Jake Wharton’s ViewPagerIndicator does not provide the exact behaviour that Kirill Grouchnikov explained in his swipey tabs article. For example, some of the subtleties such as the translucent fading of titles at the edge of the page are not supported in ViewPagerIndicator. However, it offers us a pretty close approximation of the general behaviour, and with some very simple style changes we can get something which looks quite similar to the Android Market. (NOTE: The shade of green that I’m using here does not precisely match the Android Market one, but is more consistent with other code published on this blog.). In res/values/styles.xml:

[xml]


[/xml]

What we are doing here is to change the indicator style from a triangle to an underline (line 6) setting the heights of the line and the underline (lines 7 & 8), padding (line 9) and the colour of the selected title (line 10).

If we run this we can see that it now looks quite similar to the Android Market implementation:

Android Market styling on ViewPagerIndicator
Android Market styling on ViewPagerIndicator

That concludes our exploration of ViewPager and ViewPagerIndicator. Massive thanks to Jake Wharton for writing ViewPagerIndicator, and Richard Hyndman and Kirill Grouchnikov for writing the articles on ViewPager and swipey tabs respectively, which inspired this series of articles.

The source code for this article can be found here.

© 2011 – 2012, Mark Allison. All rights reserved.

Copyright © 2011 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

27 Comments

    1. I’ve answered on stackoverflow as others are more likely to see the answer there than in the comments of my blog.

  1. Good day

    I am waiting on fire for an example with activities. I am taking out that it is some how difficult to be achieved. Thanks

    1. You can only have one Activity at any given time, and that is the container of your ViewPager & ViewPagerIndicator. Maybe you misunderstand what an Activity is if you want to host them inside the ViewPager.

      1. Good day Mark
        Thanks for the answer at first. What i meant is that we can have an activity at every page or have got it wrong? Thanks

        1. Hi Christopher,

          As I said before, you can only have one Activity at any one time. This must wrap the ViewPager & ViewPagerIndicator. So, no, you can’t have a separate Activity for each page. Maybe you should consider using Fragments instead.

  2. Thanks for the guides! Really helped me a lot in doing the swipey. But one question… Is it possible to have both textview and list view in the Viewpager and the text in the textview changes when it’s swipe to the other titles?..

    1. Hi Alvin,

      Try implementing an OnPageChangeListener and set this to your ViewPager.setOnPageChangeListener(). In the onPageSelected() method you can do something to trigger the required update to your TextView. I’ve not tried this, so it’ll probably require a bit of trial and error on your part to get it working.

      I hope this helps.

      Regards,

      Mark

  3. Two things i’d really like to see is maybe a part 4 where you could show how you would show your list view in one page and then a text view in the other and then how to add your own items to the list view

    Nonetheless great write up should be helpful

      1. I’m currently writing a series of articles specifically covering ListView. They will be published in the coming weeks. Hopefully they will answer the questions that you have.

  4. Hey Mark, love the blog. I’m having trouble getting the PagerAdapter to refresh the pages. The limited docs say that it has a method notifyDataSetChanged() just like how BaseAdapter (for listviews, gridviews, etc) has one. But calling the method does not cause the current page (and possibly the surrounding 2 pages) to be recreated.

    My pages are populated with images loaded from the internet. And sometimes I need to replace the pages with a new set of images. I can’t figure out how to get around this other than to remove and recreate a new ViewPager along with a PagerAdapter.

    On a side note, I agree with George in that it would be nice to see how to populate a ViewPager with heterogeneous content similar to how the new Market app has it.

    Some explanation of the other inherited methods would also be enlightening.

  5. Thanks for this guide.
    I don’t know if it’s a device-specific problem, but I have had the problem that the position was sometimes reset to 0 when switching pages. So I started playing around and found out that it seems to be cured when using the onScrollStateChanged method for saving the position, such as follows:

    @Override
    public void onScrollStateChanged( AbsListView view, int scrollState ) {
    switch(scrollState) {
    case SCROLL_STATE_IDLE:
    scrollPosition[position] = view.getFirstVisiblePosition();
    break;
    }
    }

    Might also be slightly more performant as the value will be saved only once per fling. Although I am unsure about trackballs as the docs indicate that trackball navigation takes completely place in the idle state.

  6. Nice series of posts…Hats off for all the hard work going into integrating the stuff from Jake Wharton,Richard Hyndman and Kirill Grouchnikov (thanks to them as well)…Second the opinions by George and Tony…Looking forward to the next series…

  7. Hi, great post, its been very helpful o me.

    I have been able to work with the tutorial to render a custom listview in the pageadapter. I have five pages and all pages are properly displayed. The list supports clicking of items, its a news feed to enable users view the news body. I realise that while on page two, if i click a news item in page 3 its the news content displayed will be that of the adjacent news item i.e page 2 or 3.

    I noticed that the reason for this is because the instantiate item method creates adjacent pages when the user changes views so as to aid fast loading. I handled my click events inside the instantiate item method so what is happening is that when the adjacent view is instantiated and the user clicks one of the list items in the current view, the adjacent view content is what is displayed. I thought to ask the appropriate way to handle events for multiple listviews in the viewpager.

    thanks

  8. Hi….nice posts.

    I would like to add ListView on 1st Tab, Google Map on 2nd tab and so on. Different controls on different tabs. I modified your instantiateItem() to display ListView only on 1st tab. Trying to add Google map on 2nd tab. How to attach MapActivity to this 2nd Tab/pager? Pager doesn’t allow starting intent to MapView class. Any hints?

    Thanks and regards

  9. Hi, i’m asking if there is a method to animate the viewpager making it change the tab by code! THANKS!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.