More Fun with Pagers

In earlier chapters, we saw basic uses of ViewPager. However, there are other ways to apply ViewPager and integrate it into the rest of your application, some of which we will examine in this chapter.

Prerequisites

This chapter assumes that you have read the core chapters, particularly the one showing how to use ViewPager.

Hosting ViewPager in a Fragment

If you have a ViewPager use fragments for pages and also be itself in a fragment, you are using nested fragments: one fragment holds another fragment. This was not originally possible with fragments, but that capability was added to the library implementation of fragments in 2014 or so.

However, using nested fragments requires a minor modification to the way we set up a PagerAdapter, as is illustrated in the ViewPager/Nested sample project. This is the same project as ViewPager/Indicator, with the twist that the pages are fragments and the ViewPager is inside a fragment.

Our activity now implements the standard add-the-fragment-if-it-does-not-exist pattern that we have seen previously:

package com.commonsware.android.pagernested;

import android.os.Bundle;
import android.support.v4.app.FragmentActivity;

public class ViewPagerIndicatorActivity extends FragmentActivity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    if (getSupportFragmentManager().findFragmentById(android.R.id.content) == null) {
      getSupportFragmentManager().beginTransaction()
                                 .add(android.R.id.content,
                                      new PagerFragment()).commit();
    }
  }
}
(from ViewPager/Nested/app/src/main/java/com/commonsware/android/pagernested/ViewPagerIndicatorActivity.java)

This loads a PagerFragment, which contains most of the logic from our original activity:

package com.commonsware.android.pagernested;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class PagerFragment extends Fragment {
  @Override
  public View onCreateView(LayoutInflater inflater,
                           ViewGroup container,
                           Bundle savedInstanceState) {
    View result=inflater.inflate(R.layout.pager, container, false);
    ViewPager pager=(ViewPager)result.findViewById(R.id.pager);

    pager.setAdapter(buildAdapter());

    return(result);
  }

  private PagerAdapter buildAdapter() {
    return(new SampleAdapter(getActivity(), getChildFragmentManager()));
  }
}
(from ViewPager/Nested/app/src/main/java/com/commonsware/android/pagernested/PagerFragment.java)

The biggest difference is that our call to the constructor of SampleAdapter no longer uses getSupportFragmentManager(). Instead, it uses getChildFragmentManager(). This allows SampleAdapter to use fragments hosted by PagerFragment, rather than ones hosted by the activity as a whole.

No other code changes are required, and from the user’s standpoint, there is no visible difference.

Pages and the Action Bar

Fragments that are pages inside a ViewPager can participate in the action bar, supplying items to appear as toolbar buttons, in the overflow menu, etc. This is not significantly different than how any fragment participates in the action bar:

ViewPager and FragmentManager will manage the contents of the action bar, based upon the currently-visible page. That page’s contributions will appear in the action bar, then will be removed when the user switches to some other page.

To see this in action, take a look at the ViewPager/ActionBar sample project. This is the same as the ViewPager/Indicator project from before, except:

  @Override
  public View onCreateView(LayoutInflater inflater,
                           ViewGroup container,
                           Bundle savedInstanceState) {
    View result=inflater.inflate(R.layout.editor, container, false);
    EditText editor=(EditText)result.findViewById(R.id.editor);
    
    position=getArguments().getInt(KEY_POSITION, -1);
    editor.setHint(getTitle(getActivity(), position));
    
    
    if ((position % 2)==0) {
      setHasOptionsMenu(true);
    }

    return(result);
  }
(from ViewPager/ActionBar/app/src/main/java/com/commonsware/android/pagerbar/EditorFragment.java)

  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    inflater.inflate(R.menu.actions, menu);

    super.onCreateOptionsMenu(menu, inflater);
  }
(from ViewPager/ActionBar/app/src/main/java/com/commonsware/android/pagerbar/EditorFragment.java)

Normally, we would also implement onOptionsItemSelected(), to find out when the action bar item was tapped, though this is skipped in this sample.

The result is that when we have an even-numbered page position — equating to an odd-numbered title and hint — we have items in the action bar:

A ViewPager, PagerTabStrip, and Action Bar Item on Android 4.1
Figure 621: A ViewPager, PagerTabStrip, and Action Bar Item on Android 4.1

…but as soon as we swipe to an odd-numbered page position — equating to an even-numbered title and hint — our action bar item is removed, as that fragment did not call setHasOptionsMenu(true):

A ViewPager and PagerTabStrip, Sans Action Bar Item on Android 4.1
Figure 622: A ViewPager and PagerTabStrip, Sans Action Bar Item on Android 4.1

ViewPagers and Scrollable Contents

There are other things in Android that can be scrolled horizontally, besides a ViewPager:

The challenge then comes in terms of dealing with horizontal swipe events. The ideal situation is for you to be able to swipe horizontally on the material inside the page, until you hit some edge (e.g., end of the HorizontalScrollView), then have swipe events move you to the adjacent page.

You can assist ViewPager in handling this scenario by subclassing it and overriding the canScroll() method. This will be called on a horizontal swipe, and it is up to you to indicate if the contents can be scrolled (returning true) or not (returning false). If the built-in logic is insufficient, tailoring canScroll() to your particular needs can help.

We will see an example of this elsewhere in the book, when we put some maps into a ViewPager.

Showing More Pages

In some cases, when the ViewPager is on a larger screen, we simply want larger pages — a digital book reader, for example, would simply have a larger page in a bigger font for easier reading.

Sometimes, though, we might not be able to take advantage of the full space offered by the large screen, particularly when our ViewPager takes up the whole screen. In cases like this, it might be useful to allow ViewPager, in some cases, to show more than one page at a time. Each “page” is then designed to be roughly phone-sized, and we choose whether to show one, two, or perhaps more pages at a time based upon the available screen space.

Mechanically, allowing ViewPager to show more than one page is fairly easy, involving overriding one more method in our PagerAdapter: getPageWidth(). To see this in action, take a look at the ViewPager/MultiView1 sample project.

Each page in this sample is simply a TextView widget, using the activity’s style’s “large appearance”, centered inside a LinearLayout:

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

  <TextView
    android:id="@+id/text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"/>

</LinearLayout>
(from ViewPager/MultiView1/app/src/main/res/layout/page.xml)

The activity, in onCreate(), gets our ViewPager from the res/layout/activity_main.xml resource, and sets its adapter to be a SampleAdapter:

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    pager=findViewById(R.id.pager);
    pager.setAdapter(new SampleAdapter());
    pager.setOffscreenPageLimit(6);
  }
(from ViewPager/MultiView1/app/src/main/java/com/commonsware/android/mvp1/MainActivity.java)

In this case, SampleAdapter is not a FragmentPagerAdapter, nor a FragmentStatePagerAdapter. Instead, it is its own implementation of the PagerAdapter interface:

  /*
   * Inspired by
   * https://gist.github.com/8cbe094bb7a783e37ad1
   */
  private class SampleAdapter extends PagerAdapter {
    @Override
    public Object instantiateItem(ViewGroup container, int position) {
      View page=
          getLayoutInflater().inflate(R.layout.page, container, false);
      TextView tv=page.findViewById(R.id.text);
      int blue=position * 25;

      final String msg=
          String.format(getString(R.string.item), position + 1);

      tv.setText(msg);
      tv.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
          Toast.makeText(MainActivity.this, msg, Toast.LENGTH_LONG)
               .show();
        }
      });

      page.setBackgroundColor(Color.argb(255, 0, 0, blue));
      container.addView(page);

      return(page);
    }

    @Override
    public void destroyItem(ViewGroup container, int position,
                            Object object) {
      container.removeView((View)object);
    }

    @Override
    public int getCount() {
      return(9);
    }

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

    @Override
    public boolean isViewFromObject(View view, Object object) {
      return(view == object);
    }
  }
(from ViewPager/MultiView1/app/src/main/java/com/commonsware/android/mvp1/MainActivity.java)

To create your own PagerAdapter, the big methods that you need to implement are:

In our case, we also override getPageWidth(). This indicates, for a given position, how much horizontal space in the ViewPager should be given to this particular page. In principle, each page could have its own unique size. The return value is a float, from 0.0f to 1.0f, indicating what fraction of the pager’s width goes to this page. In our case, we return 0.5f, to have each page take up half the pager.

The result is that we have two pages visible at a time:

Two Pages in a ViewPager on Android 4.0.3
Figure 623: Two Pages in a ViewPager on Android 4.0.3

It is probably also a good idea to call setOffscreenPageLimit() on the ViewPager, as we did in onCreate(). 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.

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 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.

Columns or Pages

Another pattern — using pages for smaller screens and having the “pages” side-by-side in columns for larger screens — will be explored later in the book.

The Grid Pattern

Yet another approach for taking advantage of larger screen sizes is to always show a full-size master and a full-size detail — perhaps using different activities — but to use a grid rather than a list for the master. This works well when the data being shown in the grid can be represented as “cards”, often dominated by some photo or other image.

The basic approach is to use fewer grid columns (e.g., 1 or 2) on smaller screen sizes and more grid columns (e.g., 3 or 4) on larger screen sizes. This way, the application flow is identical across screen sizes, yet the screen usage on larger screens is more effective. This is particularly true if you use on of the “staggered” grid widgets available from third parties, like Etsy’s AndroidStaggeredGrid or Maurycy Wojtowicz’s StaggeredGridView:

StaggeredGridView Demo (image courtesy of Maurycy Wojtowicz)
Figure 624: StaggeredGridView Demo (image courtesy of Maurycy Wojtowicz)

Columns for Large, Pages for Small

In some cases, you can take better advantage of larger screens by using ViewPager more judiciously. In a previous chapter, we explored having ViewPager itself display more than one page at a time. A variation on that same theme is to only use a ViewPager on screen sizes where you lack sufficient room for everything, and to put those same pages on the screen at the same time when you have room for all of them.

For example, a Twitter client for Android could use the columns-or-pages support for displaying various streams of tweets: your timeline, your @ mentions, hashtags you follow, etc. Each stream is represented by a typical ListView, with one row per tweet. On a phone, since screen space is at a premium, those ListView widgets are set up in a ViewPager, with one list per page. Users can swipe between the lists, or use tabs to navigate the available lists. However, tablets offer more room, so the app could show three ListView widgets side-by-side in landscape mode, so you can take in three sets of content without further interaction with the screen.

The ViewPager/Columns1 sample project will demonstrate how you can accomplish the same basic approach in your own app… with some limitations.

The Layouts

Our main activity layout — cunningly named main — has a ViewPager-based definition in res/layout/main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.view.ViewPager android:id="@+id/pager"
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <android.support.v4.view.PagerTabStrip
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="top"/>

</android.support.v4.view.ViewPager>
(from ViewPager/Columns1/app/src/main/res/layout/main.xml)

However, in res/layout-large/, for 5-inch devices on up, we have a horizontal LinearLayout with three FrameLayout containers, each representing an equal-sized slot for one of our “pages”:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:baselineAligned="false"
  android:orientation="horizontal">

  <FrameLayout
    android:id="@+id/editor1"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_weight="1"/>

  <FrameLayout
    android:id="@+id/editor2"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_weight="1"/>

  <FrameLayout
    android:id="@+id/editor3"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_weight="1"/>

</LinearLayout>
(from ViewPager/Columns1/app/src/main/res/layout-large/main.xml)

Android will automatically inflate the proper layout when we call setContentView(R.layout.main).

The Activity

However, while Android handles the inflation for us, we obviously need to populate the contents a bit differently. In this sample, though, we are relying upon the fact that screen size will not change on the fly. Hence, an instance of our application will either show a ViewPager or show the horizontal LinearLayout, and not have to switch between those at runtime.

Our SampleAdapter, therefore, can remain unchanged, except for reducing the page count to 3:

package com.commonsware.android.pagercolumns;

import android.content.Context;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;

public class SampleAdapter extends FragmentPagerAdapter {
  Context ctxt=null;

  public SampleAdapter(Context ctxt, FragmentManager mgr) {
    super(mgr);
    this.ctxt=ctxt;
  }

  @Override
  public int getCount() {
    return(3);
  }

  @Override
  public Fragment getItem(int position) {
    return(EditorFragment.newInstance(position));
  }

  @Override
  public String getPageTitle(int position) {
    return(EditorFragment.getTitle(ctxt, position));
  }
}
(from ViewPager/Columns1/app/src/main/java/com/commonsware/android/pagercolumns/SampleAdapter.java)

Our MainActivity will still use the SampleAdapter, and if we have a ViewPager, it will use it the same way as before. However, if we do not have a ViewPager, we must be showing three panes of content side by side, in which case we just execute a FragmentTransaction to populate the three FrameLayout containers with the three items created by the SampleAdapter:

package com.commonsware.android.pagercolumns;

import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;

public class MainActivity extends FragmentActivity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    ViewPager pager=findViewById(R.id.pager);

    if (pager==null) {
      if (getSupportFragmentManager().findFragmentById(R.id.editor1)==null) {
        FragmentPagerAdapter adapter=buildAdapter();

        getSupportFragmentManager().beginTransaction()
                                   .add(R.id.editor1,
                                        adapter.getItem(0))
                                   .add(R.id.editor2,
                                        adapter.getItem(1))
                                   .add(R.id.editor3,
                                        adapter.getItem(2)).commit();
      }
    }
    else {
      pager.setAdapter(buildAdapter());
    }
  }

  private FragmentPagerAdapter buildAdapter() {
    return(new SampleAdapter(this, getSupportFragmentManager()));
  }
}
(from ViewPager/Columns1/app/src/main/java/com/commonsware/android/pagercolumns/MainActivity.java)

Of course, we skip the FragmentTransaction if the fragments already exist, such as due to a screen rotation configuration change.

The Results

On a phone, the ViewPager-based layout looks pretty much as it did before:

A ViewPager. Again.
Figure 625: A ViewPager. Again.

However, on a tablet, we get our three editors side-by-side:

Same App, Large-Screen Layout with Side-By-Side Editors
Figure 626: Same App, Large-Screen Layout with Side-By-Side Editors

The Limitations

The simplified large-screen layout does not contain any indicators above the three editors. This could be added by simple changes to the res/layout-large/main.xml layout resource, if desired.

The bigger limitation is that this only works if you want the same look in all configurations except screen size, and if the screen size never changes. However, it is eminently possible that you will want to have a different mix than that, such as using the three-column approach only on large-screen landscape layouts, using ViewPager everywhere else. In that case, our approach breaks down, as we will have different fragments inside the pager and outside the pager, meaning that we will lose our data on a configuration change. Addressing this issue is covered in the next two sections.