Multiple-View ViewPager Options

On Friday, I tweeted what I thought was a shot in the dark:

Has anyone seen any forks of ViewPager that show multiple children at once? Thinking 2-3 at a time for landscape tablets. Thx!

Juhani Lehtimaeki chimed in with what I was thinking:

It would make responsive design on UIs like that very easy.

After all, if the objective of using fragments is to be able to use them individually on smaller screens and in aggregate on larger screens, it stands to reason that we might want to do the same sort of thing with the contents of a ViewPager.

I received a number of responses, including three separate implementations of what I requested. Curiously, none of those implementations bore much resemblance to each other, beyond the basics of having multiple visible pages in a ViewPager.

In this post, I will examine each of the three approaches, so you can see how they work and what visual results you get.


The approach that most closely met what I had in mind was pointed out in tweets from Lucio Maciel, and Hello, Android’s Ed Burnette, and to use getPageWidth() on PagerAdapter.

getPageWidth() returns a floating-point number, between 0 and 1, representing the portion of the width of the ViewPager that a given page should take up. By default, the page width is 1, but by overriding this, you can have multiple pages on the screen simultaneously.

I have uploaded a project that demonstrates this to a GitHub repo, but let’s take a look at the important bits here.

The key, of course, is to override getPageWidth() in your PagerAdapter:

@Override
public float getPageWidth(int position) {
  return(0.5f);
}

It is probably also a good idea to call setOffscreenPageLimit() on the ViewPager. By default (and at minimum), ViewPager will cache three pages: the one presently visible, and one on either side. However, if you are showing more than one at a time, you should bump the limit to be 3 times the number of simultaneous pages. For a page width of 0.5f — meaning two pages at a time — you would want to call setOffscreenPageLimit(6), to make sure that you had enough pages cached for both the current visible contents and one full swipe to either side.

This will give you two pages at a time:

Two pages in a ViewPager

ViewPager even handles “partial swipes” — a careful swipe can slide the right-hand page into the left-hand position and slide in a new right-hand page. And ViewPager stops when you run out of pages, so the last page will always be on the right, no matter how many pages at a time and how many total pages you happen to have.

The biggest downside to this approach is that it will not work well with the current crop of indicators. PagerTitleStrip and PagerTabStrip (and, possibly, Jake Wharton’s ViewPagerIndicator library, though I have not tried that here) assume that there is a single selected page. While the indicator will adjust properly, the visual representation shows that the left-hand page is the one selected (e.g., the tab with the highlight), even though two or more pages are visible. You can probably overcome this with a custom indicator (e.g., highlight the selected tab and the one to its right).

Also note that this approach collides a bit with setPageMargin() on ViewPager. setPageMargin() indicates an amount of whitespace that should go in a gutter between pages. In principle, this would work great with showing multiple simultaneous pages in a ViewPager. However, ViewPager does not take the gutter into account when interpreting the getPageWidth() value. For example, suppose getPageWidth() returns 0.5f and we setPageMargin(20). On a 480-pixel-wide ViewPager, we will actually use 500 pixels: 240 for the left page, 240 for the right page, and 20 for the gutter. As a result, 20 pixels of our right-hand page are off the edge of the pager. Ideally, ViewPager would subtract out the page margin before applying the page width. One workaround is for you to derive the right getPageWidth() value based upon the ViewPager size and gutter yourself, rather than hard-coding a value. Or, build in your gutter into your page contents (e.g., using android:layout_marginLeft and android:layout_marginRight) and skip setPageMargin() entirely.


The second approach comes courtesy of Nicolas Klein, who advocated managing the sub-pages within a page yourself. While his code sample seemed to be written for perhaps a ListView (it uses a ListAdapter-style getView()), the concept can certainly be translated for ViewPager.

UPDATE: Nicolas tweeted the following:

the reason why my adapter is different is because I use a different implementatoon of ViewPager which use a normal Adapter :)

Once again, I have uploaded a project that demonstrates this to a GitHub repo.

With Nicolas’ approach, you put two children in each “page” layout:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="horizontal">

  <TextView
    android:id="@+id/text"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_weight="1"
    android:gravity="center"
    android:textAppearance="?android:attr/textAppearanceLarge"/>

  <TextView
    android:id="@+id/text2"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_weight="1"
    android:gravity="center"
    android:textAppearance="?android:attr/textAppearanceLarge"/>
  
</LinearLayout>

In your PagerAdapter instantiateItem() logic, you need to convert positions in ViewPager pages to positions in sub-pages, and update both of your sub-pages in the inflated layout, handling the case where there is an odd number of sub-pages, and so you could have only a left-hand sub-page:

@Override
public Object instantiateItem(ViewGroup container, int position) {
  View page=
      getLayoutInflater().inflate(R.layout.page, container, false);
  TextView tv=(TextView)page.findViewById(R.id.text);

  position=position * 2;

  populateTextView(tv, position);
  position++;
  tv=(TextView)page.findViewById(R.id.text2);

  if (position < getRealCount()) {
    populateTextView(tv, position);
    tv.setVisibility(View.VISIBLE);
  }
  else {
    tv.setVisibility(View.INVISIBLE);
  }

  container.addView(page);

  return(page);
}

Visually, this looks very similar to the first approach. However:

  • You cannot swipe by sub-page, only by full page. This may be a feature or a bug, depending upon your objectives.

  • It is more code.

  • It would be easier to adapt to indicators (e.g., PagerTitleStrip), as really the indicators would be indicating the page, not the set of sub-pages, and you could style and title those indicators as needed.


The third approach comes from Dave Smith, co-author of the well-regarded book Android Recipes. He went in a very different direction, using a custom container that disabled children clipping to show more than one page at a time.

His published sample code shows the whole thing in action. His container (com.example.pagercontainer.PagerContainer) wraps the ViewPager and calls setClipChildren(false); on itself, so even though the ViewPager is focused on one selected page, other pages that have coordinates beyond the ViewPager bounds are still visible, so long as they fit within the PagerContainer. By sizing the ViewPager to be smaller than the PagerContainer, the ViewPager can size its pages to that size, leaving room for other pages to be seen. PagerContainer, though, needs to help out a bit with touch events, as ViewPager will only handle swipe events on its own visible bounds, ignoring any pages visible to the sides.

Visually, his results depart significantly from the first two:

ViewPager as Gallery Replacement

While this was not what I had in mind from my Twitter inquiry, Dave’s approach does solve a different problem: how does one really replace Gallery, now that it is deprecated? Dave’s code mimics Gallery’s look fairly closely, minus selection events (which could probably be added without a ton of difficulty).

Many thanks to all who responded to my tweet!