By now, you have just about all the major building blocks necessary to build a basic application. But sometimes, the basic building blocks just aren’t fun enough. This chapter gives you a basic overview of two of the most common navigational paradigms, the view pager and the navigation drawer.
A view pager is a way of navigating left and right through pages of data, generally using the swipe left and swipe right gestures. A view pager, like a list, takes an adapter, called a PagerAdapter
, to generate the content to show. Two subclasses are built into the support library for the PagerAdapter
: FragmentPagerAdapter
and FragmentStatePagerAdapter
. Both essentially accomplish the same thing—they show pages of fragments. Their nuances lie in how they handle non-visible fragments.
FragmentPagerAdapter
holds onto in memory each fragment that the user visits, and it keeps the views immediately available for each fragment to the left and right of the visible fragment. This can be memory intensive, so it is more suited for a tab-like navigation where you have only three or four fragments in the adapter.
FragmentStatePagerAdapter
holds onto only the savedInstanceState
for each fragment visited. This means there will be more overhead when switching back and forth between fragments, but you should be able to page an indefinite number of fragments this way.
In the last chapter, I covered how to add tabs to a fragment; in this one, I’m going to expand on that example and make these tabs swipeable using the ViewPager
.
Instead of importing this project from sample code, we are going to use the new project wizard to generate the code, and I’ll go over the key parts of what these pieces actually mean.
In Android Studio, select File > New Project; in Eclipse, select File > New > Android Application Project. Give your new project a name, and set the API minimum to 11. Click Next until you get to the screen asking for the name of your activity and your layout. In the Navigation Type menu, select Fixed Tabs + Swipe (in Eclipse) or ActionBar Tabs with ViewPager (in Android Studio). Click Finish. A new project will be created that contains all the boilerplate code for using the view pager. If you run this code as it is, you should see three tabs (Figure 11.1).
Note
The wizard requires API 11 to generate the ViewPager
code, but this is only because it uses the ActionBar
. Based on what you learned in Chapter 10, you should have the tools necessary (using AppCompat
) to convert this project to be compatible all the way down to version 8 if you need to.
You must be thinking, “This is great! The app is already halfway done!” Well, not quite, but this certainly does provide a great starting point for many projects. Let’s dissect the code in the activity and get an idea of what exactly is going on.
The onCreate
of this activity has a lot going on in it, but it is straightforward to follow. The generated code is heavily and helpfully commented, so let’s look over it. I’ve removed the comments for brevity, but the actual code should be similar.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final ActionBar actionBar = getActionBar();
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
mSectionsPagerAdapter = new SectionsPagerAdapter(
getSupportFragmentManager());
// Set up the ViewPager with the sections adapter.
mViewPager = (ViewPager) findViewById(R.id.pager);
mViewPager.setAdapter(mSectionsPagerAdapter);
mViewPager.setOnPageChangeListener(new
ViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
actionBar.setSelectedNavigationItem(position);
}
});
for (int i = 0; i < mSectionsPagerAdapter.getCount(); i++) {
actionBar.addTab(actionBar.newTab()
.setText(mSectionsPagerAdapter.getPageTitle(i))
.setTabListener(this));
}
}
Aside from the normal setup of the activity using setContentView
, the first thing it does is set the ActionBar
’s navigation mode to ActionBar.NAVIGATION_MODE_TABS
. By specifying this, we are letting the ActionBar
know that we want to show tabs, and that we want to be able to call tab-related functions. If this is not set, calling functions such as setSelectedNavigationIndex
will throw an IllegalStateException
, causing your application to crash.
Next, it creates a new class named SectionPagerAdapter
. As we will see later, SectionPagerAdapter
is a custom subclass of FragmentPagerAdapter
, which is the class that is primarily responsible for providing fragments to page through.
After creating the adapter, it goes through the familiar process of finding the ViewPager
, which is laid out in activity_main.xml
, and then setting its adapter with the one we just created. It’s similar to the list view in that if you do not set an adapter onto it, no content will be shown.
The last piece of importance here is setting a page change listener. This is where we form a correlation between the action bar tabs and the view pager. This is saying, “when I change pages, change the selected tab to the new page.”
The code should look similar to the code in Chapter 10. It’s just a simple for-loop
adding a number of tabs equal to the number of pages in the adapter.
The XML for this activity is simple. Actually, it feels too simple. It has only one item in it, and that is the ViewPager
. Remember back in Chapter 3 when we created a view programmatically instead of by using XML? Since this layout has only one view, and its parameters are relatively simple, this might be one of those times that you want to create your layout in XML. Why don’t you give it a try?
<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" />
As you can see, all that is being set here is a width, a height, and an ID. What might look different is that ViewPager
is prefaced with android.support.v4.view
. The ViewPager
actually exists only in the support library, and not in the Android framework. Because of this, you have to reference it by its fully qualified name, just as if you were declaring one of your own custom views in XML.
The critical piece of the view pager lies within the adapter you supply to it. Creating a ViewPager
subclass requires you to override two methods—getItem
and getCount
—and implement a constructor.
getItem
should return the type of item to be displayed; in this case, a fragment.
getCount
should return the number of items in the adapter. In the example code, it is returning 3, meaning three fragments will be paged through.
In the example code, only one type of fragment is used, but you can use any combination of fragments that you please.
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
Fragment fragment = new DummySectionFragment();
Bundle args = new Bundle();
args.putInt(DummySectionFragment.ARG_SECTION_NUMBER,
position + 1);
fragment.setArguments(args);
return fragment;
}
@Override
public int getCount() {
// Show 3 total pages.
return 3;
}
@Override
public CharSequence getPageTitle(int position) {
Locale l = Locale.getDefault();
switch (position) {
case 0:
String title1 = getString(R.string.title_section1);
return title1.toUpperCase(l);
case 1:
String title2 = getString(R.string.title_section2);
return title2.toUpperCase(l);
case 2:
String title3 = getString(R.string.title_section3);
return title3.toUpperCase(l);
}
return null;
}
}
The getItem
function is creating a new fragment for the position that it’s passed. getItem
is called internally by the FragmentPagerAdapter
, and you do not need to call this yourself to show any of the fragments that you wish to display. The example is adding the position as an argument to the fragment so that it can use it to display its position in the fragment’s content, but this isn’t necessary if you don’t want to do it.
Finally, the example overrides a function named getPageTitle
. While this isn’t required, it’s a nice helper function to associate a title with a position in the view pager. If you don’t override this, the default implementation will return null
, so watch out for NullPointerException
s.
DummyFragment
is basically just an empty fragment used to demonstrate that the pager holds fragments. Note that fragments can be inner classes and are not required to be a standalone class. This might prove useful for you if you have some fragments that aren’t too complex and perform very basic functionality. For example, if a fragment contains only a list and no other logic, it might make sense to have it as an inner class to the activity it’s used in, to provide more context into what its purpose is.
That’s it! The view pager is a great way to add top-level navigation to your application and add a fun and intuitive way for your users to discover content. As a rule, you should avoid using more than three tabs for your top-level navigation. If you have to go over three tabs, then it might be time to check out the navigation drawer.
The navigation drawer (Figure 11.2) is a UI pattern in which a drawer reveals itself from the left side of the screen, either because the user swiped from the left edge of the screen or because they pressed the home icon in the action bar. Users will know that a drawer is available by the presence of three lines next to the home icon. Some designers call this icon the “hamburger” because its three lines look like... well, you get it. Figure 11.3 shows what the standard three-lined hamburger icon looks like. You can customize this to any icon you’d like, but for convention, make sure it always has three lines.
The contents of the navigation drawer can be any view, but for our demo, we are going to use a list view. The code for this example is a modified example of the drawer demo code on the developer’s site, http://developer.android.com/training/implementing-navigation/nav-drawer.html. The official sample code is a great implementation, but for someone not familiar with all the Android APIs, it might be a bit overwhelming. I’ve removed all the extraneous pieces to demonstrate exactly what is necessary to get a drawer into your application.
As with all Android projects, onCreate
is the best place to figure out what’s going on. Let’s take a look. Since this is a little longer than the onCreate
for the view pager, I’m going to break it into two parts.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTitle = mDrawerTitle = getTitle();
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
mDrawerList = (ListView) findViewById(R.id.left_drawer);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1, ITEMS);
mDrawerList.setAdapter(adapter);
mDrawerList.setOnItemClickListener(
new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
selectItem(position);
}
});
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);
...
As usual, we are starting with setContentView
to inflate the views for our activity (we will take a look at the XML soon). In our onCreate
, we are referencing two views, a DrawerLayout
and a ListView
. A DrawerLayout
is a subclass of ViewGroup
, which means that its job is to hold other views. In this case, one of the views that it’s holding is the second one we are referencing, a ListView
. This ListView
is going to act as our navigation menu, and will be what the drawer will show when it is opened.
Tip
You can add a visible shadow to your navigation drawer by calling the setDrawerShadow
method of the DrawerLayout
with the drawable you’d like to use as a shadow. To use it, you would call the method like this (drawable image is included in the sample project): mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
Just like we did in Chapter 5, we are going to create a simple adapter and set it to our list view. To add interactivity to the list view, let’s set an OnItemClickListener
and give it a function to select an item. selectItem
is a function we are defining ourselves. We’ll go into the contents of the select item after we finish onCreate
.
Next, we add some flags on the action bar so that it knows what we intend to do with it. setDisplayHomeAsUpEnabled
is telling the action bar to show a carrot next to the icon, indicating that pressing the icon will exhibit some sort of “up” or “back” behavior. setHomeButtonEnabled
will simply make the home icon clickable.
...
mDrawerToggle = new ActionBarDrawerToggle(
this, mDrawerLayout, R.drawable.ic_drawer,
R.string.drawer_open, R.string.drawer_close) {
public void onDrawerClosed(View view) {
getActionBar().setTitle(mTitle);
}
public void onDrawerOpened(View drawerView) {
getActionBar().setTitle(mDrawerTitle);
}
};
mDrawerLayout.setDrawerListener(mDrawerToggle);
if (savedInstanceState == null) {
selectItem(0);
}
}
The last part of onCreate
is where things start to get interesting. According to the Android documentation, the ActionBarDrawerToggle
is “a handy way to tie together the functionality of DrawerLayout
and the framework ActionBar
to implement the recommended design for navigation drawers.”
The ActionBarToggleDrawer
takes five arguments, the first three arguably being the most important. Here are the argument descriptions from the online documentation.
Activity
. The activity hosting the drawer.
DrawerLayout
. The drawer layout to link to the given activity’s action bar.
DrawerImageResource
. A drawable to use as the drawer indicator (this is the hamburger icon mentioned earlier).
OpenDrawerContentDescription
. A string resource to describe the “open drawer” action for accessibility.
CloseDrawerContentDescription
. A string resource to describe the “close drawer” action for accessibility.
The creation of the ActionBarDrawerToggle
is unique in that we are using anonymous inner functions to declare the behavior of the drawer when it is toggled. This is a stylistic choice, and you could very easily create a custom class that extends ActionBarDrawerToggle
. I encourage you to use whichever you feel more comfortable with.
In both onDrawerClosed
and onDrawerOpened
in our example, we are adjusting the title in the action bar. This is not required, but it is a nice way to give your users a sense of where they are and what they are looking at. When the drawer is open, it should show the title of the drawer in the action bar; when the drawer is closed, it should reflect the content of what the user is looking at.
After initializing ActionBarDrawerToggle
, we set it as the listener for our drawer layout. This will make sure that when the drawer toggle is selected, the drawer slides in and out. Now that we have all the pieces set up properly, we set the currently selected item to 0. You will notice that this is wrapped in an if
statement that checks whether the saved instance state is null
. If it is null
, that means that this activity’s lifecycle is new. If it isn’t null
, that means we have some sort of previous state, and we don’t want to reinitialize anything.
The drawer layout requires a specific setup to work properly. Since it is a child of a view group, its purpose is to hold other views. In a drawer layout, the first view is meant to be the content view, and the second view is meant to be the actual drawer. Setting layout_gravity
on the second view is how you set the side of the screen the drawer emerges from. Setting layout_gravity
to start
will make the drawer emerge from the left side of the screen; setting it to end
will make it emerge from the right.
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/content_frame"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ListView
android:id="@+id/left_drawer"
android:layout_width="240dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="#FFF"
android:divider="#BBB"
android:dividerHeight="1dp" />
</android.support.v4.widget.DrawerLayout>
The navigation drawer is a great way to swap fragments around, and this is exactly what we’re doing in our app. The selectItem
function that we mentioned earlier (and set to 0) is actually doing all the fragment switching for us.
Note
We are writing this function ourselves, which means that we can assign any arbitrary behavior to an item click from the navigation drawer.
private void selectItem(int position) {
// update the main content by replacing fragments
Fragment fragment = new DummySectionFragment();
Bundle args = new Bundle();
args.putInt(DummySectionFragment.ARG_SECTION_NUMBER, position + 1);
fragment.setArguments(args);
FragmentManager fragmentManager = getFragmentManager();
fragmentManager.beginTransaction().replace(
R.id.content_frame, fragment).commit();
// update selected item and title, then close the drawer
setTitle(ITEMS[position]);
mDrawerLayout.closeDrawer(mDrawerList);
}
If this code looks similar to code you’ve used previously to add fragments, that’s because it is! We are creating a new fragment and setting an argument on it, which is used for nothing more than visually displaying which fragment it is.
Next, we use the standard fragment procedure: Get the FragmentManager
, begin a transaction, replace a fragment, and commit the change. Done! R.id.content_frame
references the frame layout that is located in the activity’s XML (mentioned in the previous section).
At the end of these transactions, we are doing a couple of things. First, we are setting the title in the action bar to the item we selected. This will ensure that the title matches the content. After that, we close the drawer as soon as the user selects an item. The user appreciates any extra work you can do for them.
Near the end of the file, you’ll notice a couple of functions being overridden, onPostCreate
and onConfigurationChanged
. These are both making state synchronization calls onto ActionBarDrawerToggle
. If you’re interested in what these calls are actually doing, a quick online search will lead you to the source code. In any case, just remember to add these calls into your activity, and your navigation drawer should behave properly.
Creating interfaces is one of the most enjoyable parts of Android development, and Android has provided some great tools that allow us to quickly create basic applications that use cutting-edge navigation patterns.
In this chapter, you learned how to use the new project wizard to build an application that already has an action bar with tabs and a view pager, and you learned about all the pieces that were generated.
You also checked out what the navigation drawer is all about. Although there is a lot you can do with a navigation drawer, there isn’t much code necessary to get it working.
From the basic view all the way up to the complex fragment, and from simple network requests to fully functional background services, you have all the tools you need to create just about any application you can imagine. Take some time to play around with all the examples we’ve created thus far, because in the next chapter, we’re going to upload one of these applications onto the Google Play Store so that anyone can download it! (Or no one, whichever you prefer.)