Swiping with ViewPager

Android, over the years, has put increasing emphasis on UI design and having a fluid and consistent user experience (UX). While some mobile operating systems take “the stick” approach to UX (forcing you to abide by certain patterns or be forbidden to distribute your app), Android takes “the carrot” approach, offering widgets and containers that embody particular patterns that they espouse. The action bar, for example, grew out of this and is now the backbone of many Android activities.

Another example is the ViewPager, which allows the user to swipe horizontally to move between different portions of your content. However, ViewPager is not distributed as part of the firmware, but rather via the Android Support package. Hence, even though ViewPager is a relatively new widget, you can use it on Android 1.6 and up.

This chapter will focus on where you should apply a ViewPager and how to set one up.

Pieces of a Pager

AdapterView classes, like ListView, work with Adapter objects, like ArrayAdapter. ViewPager, however, is not an AdapterView, despite adopting many of the patterns from AdapterView. ViewPager, therefore, does not work with an Adapter, but instead with a PagerAdapter, which has a slightly different API.

Android ships two PagerAdapter implementations in the Android Support package: FragmentPagerAdapter and FragmentStatePagerAdapter. The former is good for small numbers of fragments, where holding them all in memory at once will work. FragmentStatePagerAdapter is for cases where holding all possible fragments to be viewed in the ViewPager would be too much, where Android will discard fragments as needed and hold onto the (presumably smaller) states of those fragments instead.

Paging Fragments

The simplest way to use a ViewPager is to have it page fragments in and out of the screen based on user swipes.

To see this in action, this section will examine the ViewPager/Fragments sample project.

The project has a dependency on the Android Support package, in order to be able to use ViewPager. In Android Studio, this is a compile statement in the dependencies closure of build.gradle:

dependencies {
  compile 'com.android.support:support-v13:21.0.3'
}

(from ViewPager/Fragments/app/build.gradle)

The Activity Layout

The layout used by the activity just contains the ViewPager. Note that since ViewPager is not in the android.widget package, we need to fully-qualify the class name in the element:

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

(from ViewPager/Fragments/app/src/main/res/layout/main.xml)

Note that ViewPager is not available for drag-and-drop in the IDE graphical designers, probably because it comes from the Android Support package and therefore is not available to all projects.

The Activity

As you see, the ViewPagerFragmentDemoActivity itself is blissfully small:

package com.commonsware.android.pager;

import android.app.Activity;
import android.os.Bundle;
import android.support.v4.view.ViewPager;

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

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

    pager.setAdapter(new SampleAdapter(getFragmentManager()));
  }
}
(from ViewPager/Fragments/app/src/main/java/com/commonsware/android/pager/ViewPagerFragmentDemoActivity.java)

All we do is load the layout, retrieve the ViewPager via findViewById(), and provide a SampleAdapter to the ViewPager via setAdapter().

The PagerAdapter

Our SampleAdapter inherits from FragmentPagerAdapter and implements two required callback methods:

package com.commonsware.android.pager;

import android.app.Fragment;
import android.app.FragmentManager;
import android.support.v13.app.FragmentPagerAdapter;

public class SampleAdapter extends FragmentPagerAdapter {
  public SampleAdapter(FragmentManager mgr) {
    super(mgr);
  }

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

  @Override
  public Fragment getItem(int position) {
    return(EditorFragment.newInstance(position));
  }
}
(from ViewPager/Fragments/app/src/main/java/com/commonsware/android/pager/SampleAdapter.java)

Here, we say that there will be 10 pages total, each of which will be an instance of an EditorFragment. In this case, rather than use the constructor for EditorFragment, we are using a newInstance() factory method. The rationale for that will be explained in the next section.

The Fragment

EditorFragment will host a full-screen EditText widget, for the user to enter in a chunk of prose, as is defined in the res/layout/editor.xml resource:

<EditText xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/editor"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:inputType="textMultiLine"
  android:gravity="left|top"
  />
(from ViewPager/Fragments/app/src/main/res/layout/editor.xml)

We want to pass the position number of the fragment within the ViewPager, simply to customize the hint displayed in the EditText before the user types in anything. With normal Java objects, you might pass this in via the constructor, but it is not a good idea to implement a constructor on a Fragment. Instead, the recipe is to create a static factory method (typically named newInstance()) that will create the Fragment and provide the parameters to it by updating the fragment’s “arguments” (a Bundle):

  static EditorFragment newInstance(int position) {
    EditorFragment frag=new EditorFragment();
    Bundle args=new Bundle();

    args.putInt(KEY_POSITION, position);
    frag.setArguments(args);

    return(frag);
  }

(from ViewPager/Fragments/app/src/main/java/com/commonsware/android/pager/EditorFragment.java)

You might be wondering why we are bothering with this Bundle, instead of just using a regular data member. The arguments Bundle is part of our “saved instance state”, for dealing with things like screen rotations — a concept we will get into later in the book. For the moment, take it on faith that this is a good idea.

In onCreateView() we inflate our R.layout.editor resource, get the EditText from it, get our position from our arguments, format a hint containing the position (using a string resource), and setting the hint on the EditText:

  @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);
    int position=getArguments().getInt(KEY_POSITION, -1);

    editor.setHint(String.format(getString(R.string.hint), position + 1));

    return(result);
  }

(from ViewPager/Fragments/app/src/main/java/com/commonsware/android/pager/EditorFragment.java)

The Result

When initially launched, the application shows the first fragment:

ViewPager on Android 4.3, Showing First Editor
Figure 254: ViewPager on Android 4.3, Showing First Editor

However, you can horizontally swipe to get to the next fragment:

A ViewPager in Use on Android 4.0.3
Figure 255: A ViewPager in Use on Android 4.0.3

Swiping works in both directions, so long as there is another page in your desired direction.

Paging Other Stuff

You do not have to use fragments inside a ViewPager. A regular PagerAdapter actually hands View objects to the ViewPager. The supplied fragment-based PagerAdapter implementations get the View from a fragment and use that, but you are welcome to create your own PagerAdapter that eschews fragments.

Hence, if you want ViewPager to page things other than fragments, the solution is to not use FragmentPagerAdapter or FragmentStatePagerAdapter, but instead create your own implementation of the PagerAdapter interface, one that avoids the use of fragments.

We will see an example of this in a later chapter, where we also examine how to have more than one page of the ViewPager be visible at a time.

Indicators

By itself, there is no visual indicator of where the user is within the set of pages contained in the ViewPager. In many instances, this will be perfectly fine, as the pages themselves will contain cues as to position. However, even in those cases, it may not be completely obvious to the user how many pages there are, which directions for swiping are active, etc.

Hence, you may wish to attach some other widget to the ViewPager that can help clue the user into where they are within “page space”.

PagerTitleStrip and PagerTabStrip

The primary built-in indicator options available to use are PagerTitleStrip and PagerTabStrip. As the name suggests, PagerTitleStrip is a strip that shows titles of your pages. PagerTabStrip is much the same, but the titles are formatted somewhat like tabs, and they are clickable (switching you to the clicked-upon page), whereas PagerTitleStrip is non-interactive.

To use either of these, you first must add it to your layout, inside your ViewPager, as shown in the res/layout/main.xml resource of the ViewPager/Indicator sample project, a clone of the ViewPager/Fragments project that adds a PagerTabStrip to our UI:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/pager"
  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/Indicator/app/src/main/res/layout/main.xml)

Here, we set the android:layout_gravity of the PagerTabStrip to top, so it appears above the pages. You could similarly set it to bottom to have it appear below the pages.

Our SampleAdapter needs another method: getPageTitle(), which will return the title to display in the PagerTabStrip for a given position:

package com.commonsware.android.pager2;

import android.app.Fragment;
import android.app.FragmentManager;
import android.content.Context;
import android.support.v13.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(10);
  }

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

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

Here, we call a static getTitle() method on EditorFragment. That is a refactored bit of code from our former onCreateView() method, where we create the string for the hint — we will use the hint text as our page title:

package com.commonsware.android.pager2;

import android.app.Fragment;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;

public class EditorFragment extends Fragment {
  private static final String KEY_POSITION="position";

  static EditorFragment newInstance(int position) {
    EditorFragment frag=new EditorFragment();
    Bundle args=new Bundle();

    args.putInt(KEY_POSITION, position);
    frag.setArguments(args);

    return(frag);
  }

  static String getTitle(Context ctxt, int position) {
    return(String.format(ctxt.getString(R.string.hint), position + 1));
  }

  @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);
    int position=getArguments().getInt(KEY_POSITION, -1);

    editor.setHint(getTitle(getActivity(), position));

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

ViewPager and PagerTabStrip on Android 4.3, Showing Second Page
Figure 256: ViewPager and PagerTabStrip on Android 4.3, Showing Second Page

Other Indicator Options

There are many other options for tab-style indicators with a ViewPager.

TabLayout is the only other one officially supported by Google. It comes from the Design Support library, which in turn requires appcompat-v7.

Most of the options are open source libraries. The chapter on the Design Support library demonstrates one such third-party library, The Android Arsenal’s roster of ViewPager add-ons mostly consists of indicators, both those formatted like tabs and those that use other approaches.

Revisiting the Containers Sampler

Earlier in the book, we looked at many different layout resources from the Containers/Sampler sample project. These were to illustrate how different layout structures work. The whole UI is wrapped up in a ViewPager with a PagerTabStrip. However, we set up the FragmentPagerAdapter and the fragments based on a roster of available sample layouts, driven by resources.

The Layout

The layout that is used for the ViewPager and PagerTabStrip is largely the same as what we saw earlier in this chapter:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/pager"
  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 Containers/Sampler/app/src/main/res/layout/main.xml)

So, we have a ViewPager named pager, with a PagerTabStrip anchored at the top via android:layout_gravity="top".

The Data

The data about what layouts to show as tabs is contained in a pair of array resources. Array resources, as the name suggests, are resources that hold onto a collection of items. The convention is that they go in res/values/arrays.xml, though the actual filename is not required to be arrays.xml.

The sample app has a res/values/arrays.xml file, containing two array resources: a <string-array> named titles and a generic <array> named layouts:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string-array name="titles">
    <item>No Container</item>
    <item>Bottom-then-Top: LinearLayout</item>
    <item>Bottom-then-Top: RelativeLayout</item>
    <item>Bottom-then-Top: ConstraintLayout</item>
    <item>Stacked Percent: LinearLayout</item>
    <item>Stacked Percent: ConstraintLayout</item>
    <item>URL Dialog: LinearLayout</item>
    <item>URL Dialog: RelativeLayout</item>
    <item>URL Dialog: TableLayout</item>
    <item>URL Dialog: ConstraintLayout</item>
    <item>Form: TableLayout</item>
    <item>Form: LinearLayout</item>
    <item>Overlap: RelativeLayout</item>
    <item>Center: RelativeLayout</item>
    <item>Center: ConstraintLayout</item>
    <item>Bias: ConstraintLayout</item>
    <item>Bias: LinearLayout</item>
    <item>Aspect: ConstraintLayout</item>
    <item>Center Align: LinearLayout</item>
    <item>Center Align: ConstraintLayout</item>
  </string-array>
  <array name="layouts">
    <item>@layout/no_container</item>
    <item>@layout/bottom_then_top_ll</item>
    <item>@layout/bottom_then_top_rl</item>
    <item>@layout/bottom_then_top_cl</item>
    <item>@layout/stacked_percent_ll</item>
    <item>@layout/stacked_percent_cl</item>
    <item>@layout/url_dialog_ll</item>
    <item>@layout/url_dialog_rl</item>
    <item>@layout/url_dialog_tl</item>
    <item>@layout/url_dialog_cl</item>
    <item>@layout/form_tl</item>
    <item>@layout/form_ll</item>
    <item>@layout/overlap_rl</item>
    <item>@layout/center_rl</item>
    <item>@layout/center_cl</item>
    <item>@layout/bias_cl</item>
    <item>@layout/bias_ll</item>
    <item>@layout/aspect_cl</item>
    <item>@layout/center_align_ll</item>
    <item>@layout/center_align_cl</item>
  </array>
</resources>

(from Containers/Sampler/app/src/main/res/values/arrays.xml)

As the name suggests, a <string-array> holds strings. Here, it holds literal strings. Alternatively, those could be references to string resources, using the same @string/... naming convention that we have seen elsewhere in the book.

The layouts array holds references to the corresponding layout resources, using @layout/... syntax to identify those resources.

These two arrays are set up in the same order, so the first title is used for the first layout, the second title is used for the second layout, and so on.

The Fragment

The fragment for the pages in the ViewPager is a static nested class within the MainActivity, named LayoutFragment. This is atypical; usually a fragment will be in its own Java class. That is because usually a fragment has a lot of code associated with it. In this case, LayoutFragment is rather short:

  public static class LayoutFragment extends Fragment {
    private static final String ARG_LAYOUT="layout";

    static LayoutFragment newInstance(int layoutId) {
      LayoutFragment result=new LayoutFragment();
      Bundle args=new Bundle();

      args.putInt(ARG_LAYOUT, layoutId);
      result.setArguments(args);

      return(result);
    }

    @Override
    public View onCreateView(LayoutInflater inflater,
                             ViewGroup container,
                             Bundle savedInstanceState) {
      return(inflater.inflate(getArguments().getInt(ARG_LAYOUT),
        container, false));
    }
  }

(from Containers/Sampler/app/src/main/java/com/commonsware/android/containers/sampler/MainActivity.java)

When we create an instance of LayoutFragment through the newInstance() factory method, we pass in a layout resource ID. Through the arguments Bundle, that becomes available to our onCreateView() method, which simply inflates that layout and returns it.

The fragment is so short because we are not doing anything with the widgets inside of the inflated layout, such as filling them with data or setting up event listeners.

The Activity

The MainActivity that contains LayoutFragment has only one method of its own: onCreate(), which inflates the res/layout/main.xml resource shown above, finds the ViewPager, and sets its adapter to be a SampleAdapter:

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

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

    pager.setAdapter(new SampleAdapter(getFragmentManager()));
  }

(from Containers/Sampler/app/src/main/java/com/commonsware/android/containers/sampler/MainActivity.java)

In other words, this onCreate() is pretty much the same as the others seen in this chapter. Where the fun lies is in this sample app’s edition of SampleAdapter.

The PagerAdapter

SampleAdapter uses the contents of those two array resources to determine how many pages there are, what to show in the tabs, and what layout resource ID to pass to the LayoutFragment:

  private class SampleAdapter extends FragmentPagerAdapter {
    private int[] layouts;
    private String[] titles;

    SampleAdapter(FragmentManager mgr) {
      super(mgr);
      layouts=getLayoutsArray(R.array.layouts);
      titles=getResources().getStringArray(R.array.titles);
    }

    @Override
    public int getCount() {
      return(titles.length);
    }

    @Override
    public Fragment getItem(int position) {
      return(LayoutFragment.newInstance(layouts[position]));
    }

    @Override
    public CharSequence getPageTitle(int position) {
      return(titles[position]);
    }

    int[] getLayoutsArray(int arrayResourceId) {
      TypedArray typedArray=
        getResources().obtainTypedArray(arrayResourceId);
      int[] result=new int[typedArray.length()];

      for (int i=0;i<typedArray.length();i++) {
        result[i]=typedArray.getResourceId(i, -1);
      }

      return(result);
    }
  }

(from Containers/Sampler/app/src/main/java/com/commonsware/android/containers/sampler/MainActivity.java)

SampleAdapter holds onto a String array of the titles. This is populated by getResources().getStringArray() in the SampleAdapter constructor, which reads in the titles <string-array> resource. Those strings are then used for the getPageTitle() method, and the length of the strings array is used for getCount().

Getting the equivalent array of the int values for the layout resource IDs is more cumbersome. We could try calling getIntArray() on the Resources object returned by getResources(). However, that only works for <int-array> resources. We cannot use one of those in res/values/arrays.xml because Android considers @layout/... to be a resource ID, not just a simple integer.

To work with a generic <array> resource, we need to call obtainTypedArray() on the Resources object, which we do in the getLayoutsArray() helper method. This returns a TypedArray, which effectively is an array of arbitrary resource types. We know that our array should be all layout resource IDs, so we allocate an int array to be our result (based upon the length() of the TypedArray), then iterate over all of the entries and retrieve the resource ID for that array position (using getResourceId()). In the end, result has an array of resource IDs, and those get used by getItem() to supply LayoutFragment with the appropriate layout resource ID for this page’s position.

Visit the Trails!

There is a chapter on advanced ViewPager techniques that may interest you!