The Android Design Support Library

In 2014, to much fanfare, Google released their first edition of the Material Design guidelines.

What was missing was an actual implementation of most of these guidelines.

Beyond the obvious question of “how do you know that it will work well if you have not tried it?”, it put Android developers in the unenviable position of being pressured to make their apps “look more material” without having anything really to do that.

In the months that followed Google I|O 2014, various developers took this implementation gap as a challenge and created their own implementations of many bits of Material Design. Much of this was released in the form of open source components, easily added to an app via dependencies added to a project’s build.gradle file (at least, for Android Studio developers and other Gradle users).

In 2015, to a bit less fanfare, Google released the Android Design Support Library. The vision is that this would be the official implementation of many Material Design core components, like floating action buttons (FABs), snackbars, and the like.

This chapter explores some components from the Android Design Support Library. This chapter also explores some independent implementations of the same components, particularly ones that seem to be superior to what Google is offering at present.

Prerequisites

Understanding this chapter requires that you have read the core chapters, particularly the one on the action bar. You also should read the chapter on the appcompat-v7 action bar backport.

Note that the examples in this chapter are clones of a couple from the core chapters. This chapter’s prose was written assuming that you were familiar with those samples, so you may need to go back and review them as needed.

One of the book samples makes use of the animator framework.

GUIs and the Support Package

Many developers think that the libraries in the Android Support Package are purely backports. They then get confused when they realize that certain classes, like ViewPager, are not part of the core Android framework for any API level and exist only in the Android Support Package.

In truth, a lot of what is in the Android Support Package consists of backports: fragments, the action bar, NotificationCompat, and so on. However, the Android Support Package really consists of code that Google wants to make available to developers that can be used right away, even on older devices.

Many pieces of the Android Support Package are GUI-related, yet are not backports:

Now, we can add the Android Design Support Library to that list. Right now, this library is focused on Material Design components, and that is likely to remain its near-term focus. It remains to be seen if other GUI components, not specifically tied to Material Design, wind up in the Android Design Support Library, in support-v4/support-v13, or in other libraries.

Adding the Library… and What Comes With It

On the surface, Android Studio users can simply add com.android.support:design:... (for some version number for ..., such as 25.1.1) as a dependency:


implementation 'com.android.support:design:25.1.1'

However, this library has a transitive dependency that pulls in appcompat-v7. Most pieces of the Android Design Support Library do indeed seem to require that you use appcompat-v7, using Theme.AppCompat, AppCompatActivity, and friends. This is true even if you planned on using Theme.Material itself, with a minSdkVersion of 21 or higher.

There are two ways to work around this appcompat-v7 requirement:

Introducing CWAC-CrossPort

CWAC-CrossPort is a library, published by the author of this book, that contains a subset of the GUI elements from the Design Support library. All references to appcompat-v7 have been removed, replaced with equivalents from Theme.Material. As such, you can use CWAC-CrossPort on apps with a minSdkVersion of 21 or higher, to get the Material Design elements from the Design Support library, but without the appcompat-v7 baggage.

CWAC-CrossPort does not include everything from the Design Support library, but it does include most of it.

As with the rest of the CWAC libraries, using it requires that you add the CWAC artifact repository to your Gradle configuration:


repositories {
    maven {
        url "https://s3.amazonaws.com/repo.commonsware.com"
    }
}

Then, add a dependency on the artifact itself, for some version:


dependencies {
    implementation 'com.commonsware.cwac:crossport:0.3.0'
}

Snackbars: Sweeter than Toasts

The Toast has been in Android since the beginning. It allows you to pop up a message to show the user, one that does not interfere with the rest of your activity layout. And, it is fairly easy to use.

However, some people get burned by Toast:

The Material Design guidelines instead call for the use of a “snackbar”, and the Design Support Library offers a Snackbar implementation of this UI pattern. In contrast to a Toast:

With that in mind, let’s take a look at some use cases for a Snackbar and how they can be implemented.

Alerts

The quintessential reason to use a Toast was to display a simple message to the user. You can use a Snackbar in the same role, with most of the same code.

Snackbar has a static make() method, mirroring the makeText() method on Toast. make() takes three parameters, only slightly different from those on makeText():

  1. A View in the activity that wishes to show the Snackbar
  2. The message, in the form of a CharSequence (e.g., a String) or a string resource ID
  3. The duration of the Snackbar, which is either Snackbar.LENGTH_SHORT, Snackbar.LENGTH_LONG, or possibly something else (the documentation is inconsistent on this point)

As with makeText() on Toast, simply calling make() on Snackbar creates a Snackbar object for you, but does not display anything. You need to call show() on the Snackbar instance to get it to appear.

The DesignSupport/Snackbar sample project is a clone of the Threads/AsyncDemo sample from earlier in the book. The app shows a list of 25 Latin words, progressively added to the list via an AsyncTask. When the list is fully populated, the original sample would display a Toast, from onPostExecute() of the AsyncTask.

The revised sample substitutes a Snackbar:

    @Override
    protected void onPostExecute(Void unused) {
      Snackbar.make(getListView(), R.string.done,
          Snackbar.LENGTH_LONG).show();

      task=null;
    }
(from DesignSupport/Snackbar/app/src/main/java/com/commonsware/android/snackbar/async/AsyncDemoFragment.java)

The Snackbar will be centered along the bottom:

Official Snackbar
Figure 492: Official Snackbar

Unfortunately, there does not seem to be much support for styling the look of the Snackbar. To do this manually, you can obtain the actual View for the Snackbar via getView(). While you should make few assumptions about what this View actually is, you should be able to call setters on that View to change things like background colors.

Also note that the user can get rid of a Snackbar via a swipe gesture, in addition to allowing the Snackbar to time out on its own. This is not possible with Toast, as a Toast is modeless.

Action Bars. No, Not Those Action Bars.

We can expand upon the user interaction with a Snackbar by adding an action to it. To do this, just call setAction() on the Snackbar after creating it, passing in the display string for the action (what the user will see on the Snackbar) and a View.OnClickListener that will get control when the user taps on that action. The look and feel of the action is up to the Snackbar implementation.

The DesignSupport/SnackbarAction sample project is a clone of the previous sample, adding one of these actions. Specifically, once the list is loaded, we want a “Restart” action to clear the list and load it again. Perhaps the user found loading the list to be exciting and wishes to see it happen all over again.

To that end, we should pull out the work of loading our list into a loadModel() method that can be used from multiple places:

  private void loadModel() {
    task=new AddStringTask();
    task.execute();
  }
(from DesignSupport/SnackbarAction/app/src/main/java/com/commonsware/android/snackbar/action/AsyncDemoFragment.java)

The onCreate() method now delegates to loadModel():

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setRetainInstance(true);

    adapter=
        new ArrayAdapter<String>(getActivity(),
            android.R.layout.simple_list_item_1,
            model);

    loadModel();
  }
(from DesignSupport/SnackbarAction/app/src/main/java/com/commonsware/android/snackbar/action/AsyncDemoFragment.java)

And, more importantly for this section, we also call loadModel() from the View.OnClickListener of the action that we add to our Snackbar:

    @Override
    protected void onPostExecute(Void unused) {
      Snackbar munchie=Snackbar.make(getListView(), R.string.done,
          Snackbar.LENGTH_LONG);

      munchie.setAction(R.string.snackbar_action_restart,
        new View.OnClickListener() {
          @Override
          public void onClick(View view) {
            adapter.clear();
            loadModel();
          }
        });

      munchie.show();

      task=null;
    }
(from DesignSupport/SnackbarAction/app/src/main/java/com/commonsware/android/snackbar/action/AsyncDemoFragment.java)

The action will appear on the Snackbar itself:

Official Snackbar, with an Action
Figure 493: Official Snackbar, with an Action

Tapping the action triggers the listener, which in our case clears the list and starts the load all over again.

CWAC-CrossPort

CWAC-CrossPort supports Snackbar, with all the same features as are in the original Design Support implementation. The DesignSupport/SnackbarActionCP sample project is a clone of the previous sample, where we have migrated to CWAC-CrossPort.

Our app/build.gradle file not only adds the CWAC-CrossPort dependency, but it sets the minSdkVersion to 21:

apply plugin: 'com.android.application'

repositories {
  maven {
    url "https://s3.amazonaws.com/repo.commonsware.com"
  }
}

dependencies {
  implementation 'com.android.support:support-fragment:27.1.1'
  implementation 'com.commonsware.cwac:crossport:0.3.0'
}

android {
    compileSdkVersion 27
    buildToolsVersion '27.0.3'

    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 27
        applicationId 'com.commonsware.android.snackbar.action.crossport'
    }
}
(from DesignSupport/SnackbarActionCP/app/build.gradle)

Removing appcompat-v7 has rippling effects across the rest of the code:

Otherwise, the code is identical, and we get identical results, though with an inverse theme and adjusted title bar, to help distinguish the original from the CWAC-CrossPort edition:

CWAC-CrossPort Snackbar
Figure 494: CWAC-CrossPort Snackbar

Absolutely FABulous

Perhaps no single element of the Material Design aesthetic has gotten more attention than has the floating action button, or FAB. These are round buttons, usually floating towards the bottom of the screen over top of the main UI:

Google Maps, with a Pair of FABs, on a Nexus 4
Figure 495: Google Maps, with a Pair of FABs, on a Nexus 4

The job of the FAB is to provide rapid access to the primary action that users might take on that particular screen. Typically, in a master/detail sort of UI, the FAB will allow creating a new item for the collection:

However, the FAB does not have to be an “add” operation. The only real limitation is that it should be a screen-level operation, not affecting only some selected item on that screen. So, for example, in the video recording app example, you would not use a FAB to play back one of the existing videos… at least on a screen listing those videos. If tapping a video in that list brings up some sort of detail screen, that screen could possibly have a FAB to play back the video.

The Design Support library has a rudimentary FAB implementation, and there are third-party alternatives that either add power or solve other FAB-related problems.

FAB Mechanics

In many respects, setting up a FAB is not that different from setting up any other widget: put it in your layout, positioned where you want it, then access it in Java code to set up listeners. In particular, Google’s FAB implementation supports a View.OnClickListener much like a regular Button.

The DesignSupport/FAB sample project is a clone of the first Snackbar sample from earlier in this chapter. The second Snackbar sample added a “restart” action to the Snackbar. In this sample, we instead have a “restart” action on a FAB.

Before, we did not need a layout resource for the AsyncDemoFragment, as it was an ordinary ListFragment and therefore would supply a ListView automatically. However, this time, we want to have a FAB as well, so we need our own layout file:

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

  <ListView
    android:id="@android:id/list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:drawSelectorOnTop="false" />

  <android.support.design.widget.FloatingActionButton
    android:id="@+id/refresh"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:layout_alignParentEnd="true"
    android:layout_alignParentRight="true"
    android:layout_marginBottom="@dimen/fab_margin"
    android:layout_marginEnd="@dimen/fab_margin"
    android:layout_marginRight="@dimen/fab_margin"
    android:src="@drawable/ic_refresh_black_24dp" />

</RelativeLayout>
(from DesignSupport/FAB/app/src/main/res/layout/main.xml)

Here, the FAB (a.k.a., android.support.design.widget.FloatingActionButton) is later in a RelativeLayout than is the ListView, so the FAB will have higher elevation and will appear to float over the ListView. The android:src attribute points to a drawable resource, much like how that attribute works on an ImageButton.

However, the interesting bit is the pair of margin attributes (android:layout_marginRight and android:layout_marginBottom). They point to a fab_margin dimension resource, one with some specific values required due to bugs (or curious implementation choices) in Google’s FAB implementation.

By default, the fab_margin in res/values/dimens.xml is used, which has a dimension of 0dp:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <dimen name="fab_margin">0dp</dimen>
</resources>
(from DesignSupport/FAB/app/src/main/res/values/dimens.xml)

You might think that this would cause the FAB to be slammed up against the side and bottom of the RelativeLayout. However, the FAB has built-in margins… on older devices.

But, for whatever reason, on API Level 21+, that automatic margin vanishes. So, we have another definition of fab_margin, in res/values-v21/dimens.xml, setting it to 16dp:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <dimen name="fab_margin">16dp</dimen>
</resources>
(from DesignSupport/FAB/app/src/main/res/values-v21/dimens.xml)

Furthermore, Google’s Material Design docs state that there should be 24dp margin on tablets, not 16dp. So, we have a third definition of fab_margin, in res/values-sw720dp-v21, to set the margin to 24dp:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <dimen name="fab_margin">24dp</dimen>
</resources>
(from DesignSupport/FAB/app/src/main/res/values-sw720dp-v21/dimens.xml)

It is possible that a full implementation of this would need a fourth fab_margin value, for Android 4.x tablets, where fab_margin would be set to something that gives a 24dp margin but takes into account the automatic margin that the FAB seems to have prior to API Level 21. This sample avoids this, going with the automatic margin on all tablets, regardless of API level.

The Java code is fairly straightforward, retrieving the FAB in onViewCreated() and hooking up a View.OnClickListener to the FAB, where that listener is the AsyncDemoFragment itself:

  @Override
  public void onViewCreated(View v, Bundle savedInstanceState) {
    super.onViewCreated(v, savedInstanceState);

    getListView().setScrollbarFadingEnabled(false);
    setListAdapter(adapter);

    FloatingActionButton fab=v.findViewById(R.id.refresh);

    fab.setOnClickListener(this);
  }
(from DesignSupport/FAB/app/src/main/java/com/commonsware/android/fab/async/AsyncDemoFragment.java)

In onClick(), if the AsyncTask is still running, we cancel() it. Then, we clear the list and kick off a fresh task via the same sort of loadModel() method as seen in the second Snackbar example:

  @Override
  public void onClick(View view) {
    if (task!=null) {
      task.cancel(false);
    }

    adapter.clear();
    loadModel();
  }

  void loadModel() {
    task=new AddStringTask();
    task.execute();
  }
(from DesignSupport/FAB/app/src/main/java/com/commonsware/android/fab/async/AsyncDemoFragment.java)

This gives us our FAB:

Coordinating with Snackbars

However, what the above screenshots do not illustrate is what happens when our Snackbar appears:

Official FAB, Conflicting with the Snackbar
Figure 496: Official FAB, Conflicting with the Snackbar

The fact that the Snackbar overlaps the FAB should not be much of a surprise. After all, the Snackbar overlaps the ListView as well. A Toast would also overlap the list and FAB. Hence, to some extent, the fact that there is this lack of coordination between the Snackbar and the FAB seems to be fairly normal.

That being said, the Design Support library has a container designed for coordinating between different children as those children animate and scroll. This container — CoordinatorLayout — is a subclass of FrameLayout, meaning other than Z-axis ordering (elevation) and gravity, it has no other notable layout rules. It merely exists to perform this sort of coordination.

As it turns out, CoordinatorLayout has special awareness of Snackbar and the FAB, so simply using CoordinatorLayout will cause the FAB to slide upwards to make room for the Snackbar.

The DesignSupport/CoordinatedFAB sample project is a clone of the previous FAB example, except that we switch from a RelativeLayout root container to a CoordinatorLayout:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <ListView
    android:id="@android:id/list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:drawSelectorOnTop="false" />

  <android.support.design.widget.FloatingActionButton
    android:id="@+id/refresh"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|end"
    android:layout_marginBottom="@dimen/fab_margin"
    android:layout_marginEnd="@dimen/fab_margin"
    android:layout_marginRight="@dimen/fab_margin"
    android:src="@drawable/ic_refresh_black_24dp" />

</android.support.design.widget.CoordinatorLayout>
(from DesignSupport/CoordinatedFAB/app/src/main/res/layout/main.xml)

Since CoordinatorLayout is based on FrameLayout, not RelativeLayout, we have to adjust the layout rules on the FAB to match, using android:layout_gravity to position the FAB towards the bottom right corner.

With no other changes, we now get coordinated movements of the FAB and the Snackbar as the Snackbar appears and disappears:

Official FAB, Coordinated with the Snackbar
Figure 497: Official FAB, Coordinated with the Snackbar

CWAC-CrossPort

CWAC-CrossPort also has ports of FloatingActionButton and CoordinatorLayout, as can be seen in the DesignSupport/CoordinatedFABCP sample project.

In addition to the sorts of changes needed before (e.g., replace appcompat-v7 with crossport, change imports, change the theme), our layout now uses crossport classes:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <ListView
    android:id="@android:id/list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:drawSelectorOnTop="false"
    />

  <com.commonsware.cwac.crossport.design.widget.FloatingActionButton
    android:id="@+id/refresh"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|end"
    android:layout_marginBottom="@dimen/fab_margin"
    android:layout_marginEnd="@dimen/fab_margin"
    android:src="@drawable/ic_refresh_black_24dp"/>

</android.support.design.widget.CoordinatorLayout>
(from DesignSupport/CoordinatedFABCP/app/src/main/res/layout/main.xml)

And, as before, we get the same results as with the official implements, just without the appcompat-v7 overhead:

Coordinated FAB CrossPort Demo
Figure 498: Coordinated FAB CrossPort Demo

Third-Party FABs… and FAMs

The Design Support implementation of a FAB works, and it works nicely “out of the box” with CoordinatorLayout. However, it implements only a subset of the Material Design FAB capabilities, let alone related structures like the floating action menu (FAM).

As a result, there are many FAB projects listed on the Android Arsenal, offering other implementations of a FAB.

In addition to complete FAB and FAM implementations, developers have been publishing libraries that add on other Material Design features to existing FAB implementations, such as:

Material Tabs with TabLayout

Android has had a myriad of tab implementations over the years:

The Design Support library adds yet another tab implementation: TabLayout. Specifically, this implementation’s claim to fame is a faithful implementation of a subset of Google’s Material Design guidelines for how tabs should look and behave.

TabLayout can be used with or without a ViewPager. If you elect to skip the ViewPager, TabLayout works in a form reminiscent of action bar tabs, where it is responsible for the tab UI and you are responsible for updating the rest of your UI based upon the chosen tab (e.g., commit a FragmentTransaction). If you elect to use a ViewPager, TabLayout can lightly integrate with the ViewPager, so navigating by one means (e.g., swiping the pager) updates the other UI (e.g., changing the selected tab).

From a layout standpoint, you use TabLayout much like you would use TabWidget: put it where the tabs should go. Since Material Design wants the tabs on top, that means that typically you would put TabLayout inside a vertical LinearLayout, with the actual tabbed content beneath the TabLayout.

This is illustrated in the DesignSupport/TabLayout sample project. It is based on the original ViewPager samples, showing a set of editors in pages, this time using a TabLayout as the tab implementation (as opposed to PagerTabStrip or the TabPagerIndicator from the ViewPagerIndicator library).

The layout loaded by the activity has a setup much as described above: a vertical LinearLayout wrapped around a TabLayout and our ViewPager:

<?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:orientation="vertical">

  <android.support.design.widget.TabLayout
    android:id="@+id/tabs"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>

  <android.support.v4.view.ViewPager
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
  </android.support.v4.view.ViewPager>
</LinearLayout>
(from DesignSupport/TabLayout/app/src/main/res/layout/main.xml)

TabLayout can have its tabs operate in one of two modes: fixed and scrollable. With fixed tabs, all tabs will be on the screen at all times, where they divide the available horizontal space between them. This works fine for just a few tabs. But for lots of tabs, each tab becomes very small, making it unlikely that the user can read the tab caption. Scrollable tabs each take up as much room as their caption requires, and if the roster of tabs becomes too wide for the screen, the user can swipe the tabs.

The sample app demonstrates both of these approaches, using a checkable action bar item to toggle between three editors with fixed tabs or ten editors with scrollable tabs. The default state is to be in fixed-tab mode:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">
  <item
    android:id="@+id/fixed"
    android:title="@string/menu_fixed"
    android:checkable="true"
    android:checked="true"
    app:showAsAction="never"/>
</menu>
(from DesignSupport/TabLayout/app/src/main/res/menu/actions.xml)

The entire app was switched over to use AppCompatActivity and the fragment backport, as that is what the Design Support library requires. Beyond that, the EditorFragment is pretty much unchanged from the original implementations, just showing a large EditText widget with a hint based on the page number.

Our PagerAdapterSamplePagerAdapter — has one change beyond the switch to the fragment backport. To accommodate switching between fixed and scrollable tabs, rather than hard-coding the number of pages, the adapter offers a setPageCount() method to stipulate the number of pages. The page count defaults to 3.

package com.commonsware.android.tablayout;

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 {
  private final Context ctxt;
  private int pageCount=3;

  public SampleAdapter(Context ctxt, FragmentManager mgr) {
    super(mgr);

    this.ctxt=ctxt;
  }

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

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

  @Override
  public String getPageTitle(int position) {
    return(EditorFragment.getTitle(ctxt, position));
  }

  void setPageCount(int pageCount) {
    this.pageCount=pageCount;
  }
}
(from DesignSupport/TabLayout/app/src/main/java/com/commonsware/android/tablayout/SampleAdapter.java)

In MainActivity, in onCreate(), we set up the ViewPager and the TabLayout:

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

    pager=(ViewPager)findViewById(R.id.pager);
    adapter=new SampleAdapter(this, getSupportFragmentManager());
    pager.setAdapter(adapter);

    tabs=(TabLayout)findViewById(R.id.tabs);
    tabs.setupWithViewPager(pager);
    tabs.setTabMode(TabLayout.MODE_FIXED);
  }
(from DesignSupport/TabLayout/app/src/main/java/com/commonsware/android/tablayout/MainActivity.java)

The bulk of the TabLayout setup work is handled with one call to setupWithViewPager(). This:

We also call setTabMode(TabLayout.MODE_FIXED), as we are going with fixed tabs at the outset.

This gives us our three tabs:

TabLayout Sample, As Initially Launched, Showing Three Fixed Tabs
Figure 499: TabLayout Sample, As Initially Launched, Showing Three Fixed Tabs

But we also have that menu resource, to allow the user to switch between fixed and scrollable tabs. We inflate() that resource in onCreateOptionsMenu() as usual, and we handle the checked state change in onOptionsItemSelected():

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.actions, menu);

    return(super.onCreateOptionsMenu(menu));
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    if (item.getItemId()==R.id.fixed) {
      item.setChecked(!item.isChecked());

      if (item.isChecked()) {
        adapter.setPageCount(3);
        tabs.setTabMode(TabLayout.MODE_FIXED);
      }
      else {
        adapter.setPageCount(10);
        tabs.setTabMode(TabLayout.MODE_SCROLLABLE);
      }

      adapter.notifyDataSetChanged();

      if (pager.getCurrentItem()>=3) {
        pager.setCurrentItem(2);
      }

      return(true);
    }

    return(super.onOptionsItemSelected(item));
  }
(from DesignSupport/TabLayout/app/src/main/java/com/commonsware/android/tablayout/MainActivity.java)

If the user taps on our checkable overflow item, we invert the item’s checked state (which, unfortunately, does not happen automatically). Then, we call setPageCount() on the SampleAdapter and setTabMode() on the TabLayout based on the now-current checked state, to either have three fixed tabs or ten scrollable tabs.

Changing the page count of the SampleAdapter requires calling notifyDataSetChanged() to alert the ViewPager that the data set changed and it needs to repaint. However, while the associated TabLayout repaints, the selected page is left alone. That is fine when going from 3 pages to 10, but it could be a problem when going from 10 pages to 3, if the selected page is after those three. So, we use setCurrentItem() to manually move the selection to the last valid page, if that situation occurs.

Clicking on the “Fixed” checkable overflow item, and thereby unchecking it from its initial checked state, gives us ten scrollable tabs:

TabLayout Sample, Showing Scrollable Tabs
Figure 500: TabLayout Sample, Showing Scrollable Tabs

Note that while this particular sample app shows TabLayout working with a ViewPager, a ViewPager is not required to be able to use TabLayout. You can simply have the TabLayout plus your own system for whatever the tabs switch in your UI. Then, you can use methods like addTab() and setOnTabSelectedListener() to set up tabs and find out when the user taps on them, so you can adjust your UI to match the selected tab. That being said, many users may come to expect that they can horizontally swipe to move between pages of content, and so definitely consider using a ViewPager if practical.

CWAC-CrossPort

CWAC-CrossPort has a port of TabLayout. The DesignSupport/TabLayoutCP sample project is identical to the DesignSupport/TabLayout sample app, except that it uses CWAC-CrossPort. The same sorts of modifications as seen earlier in this chapter are required, such as changing the layout to refer to the appropriate package for the ported TabLayout:

<?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:orientation="vertical">

  <com.commonsware.cwac.crossport.design.widget.TabLayout
    android:id="@+id/tabs"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>

  <android.support.v4.view.ViewPager
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
  </android.support.v4.view.ViewPager>
</LinearLayout>
(from DesignSupport/TabLayoutCP/app/src/main/res/layout/main.xml)

In this particular sample, since we are using menu resources, we have one additional change to make: switching from app:showAsAction to android:showAsAction, as we are using the native action bar:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">
  <item
    android:id="@+id/fixed"
    android:title="@string/menu_fixed"
    android:checkable="true"
    android:checked="true"
    android:showAsAction="never"/>
</menu>
(from DesignSupport/TabLayoutCP/app/src/main/res/menu/actions.xml)

Floating Labels

The EditText widget supports the android:hint attribute. The hint is shown in the EditText when the EditText is otherwise empty. However, if the EditText has actual text in it (whether typed by the user, loaded from a database, or whatever), the hint is not shown. This saves screen space compared to having a TextView label always visible; the hint itself serves as the label.

However, the hint-as-label pattern has a major drawback: the hint is not visible if there is text in the EditText. In the long term, as the user learns your UI, this is not a big problem. However, particularly early on, the user might look at a filled-in field and wonder what that field is for. This is even more likely in cases where the user is not the one who typed the text into the field in the first place, such as editing a database entry pulled from a server, where somebody (or something) else had created the entry in the first place.

The “floating label” pattern starts with a hint in the field. However, when the field is used, the hint animates out of the field itself and “floats” above the field in a shrunken form. This way, the label is always visible. However, in its smaller floating state, it takes up less screen space, yet while the field is otherwise empty, we can take advantage of that space to offer a “full-size” label instead.

The Design Support library offers TextInputLayout as a way of implementing the floating label pattern. This is not a subclass of EditText, but rather a ViewGroup that is wrapped around the EditText. This is convenient, insofar as it allows developers to use other EditText subclasses and still get the floating-label behavior.

TextInputLayout also supports an error state, where we can optionally show an error message below the EditText, such as an indication of an invalid bit of data entry.

Using TextInputLayout

The DesignSupport/FloatingLabel sample project is a clone of an earlier sample where we allowed the user to enter in a URL and then, upon a button click, would parse the URL into a Uri, wrap that in an ACTION_VIEW Intent, then try to start an activity for that Intent.

The original sample’s layout looks like:

<?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:orientation="vertical">

  <EditText
    android:id="@+id/url"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="@string/url"
    android:inputType="textUri"/>

  <Button
    android:id="@+id/browse"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="showMe"
    android:text="@string/show_me"/>

</LinearLayout>
(from Activities/LaunchWeb/app/src/main/res/layout/main.xml)

In this revised sample, the original EditText is augmented with a TextInputLayout:

<?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:orientation="vertical">

  <Button
    android:id="@+id/browse"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="showMe"
    android:text="@string/show_me">

    <requestFocus/>
  </Button>

  <android.support.design.widget.TextInputLayout
    android:id="@+id/til"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <EditText
      android:id="@+id/url"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:hint="@string/url"
      android:inputType="textUri"/>
  </android.support.design.widget.TextInputLayout>

</LinearLayout>
(from DesignSupport/FloatingLabel/app/src/main/res/layout/main.xml)

You will notice that there are a few changes here:

Those latter two changes are due to one major limitation with TextInputLayout: the hint moves out of the EditText into the floating position when either there is text in the EditText or the EditText gains the focus. Strangely, simply putting the Button before the EditText is insufficient, as is simply adding [requestFocus/] on the Button. Both have to be implemented to cause the TextInputLayout to show the hint in its default location at the outset.

The Java code is also augmented a bit from the original sample, to take advantage of the error-reporting feature of TextInputLayout:

package com.commonsware.android.design.til;

import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.design.widget.TextInputLayout;
import android.support.v7.app.AppCompatActivity;
import android.util.Patterns;
import android.view.View;
import android.widget.EditText;

public class LaunchDemo extends AppCompatActivity {
  private TextInputLayout til;

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

    til=(TextInputLayout)findViewById(R.id.til);
    til.setErrorEnabled(true);
  }

  public void showMe(View v) {
    EditText urlField=(EditText)findViewById(R.id.url);
    String url=urlField.getText().toString();

    if (Patterns.WEB_URL.matcher(url).matches()) {
      startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
    }
    else {
      til.setError(getString(R.string.til_error));
    }
  }
}
(from DesignSupport/FloatingLabel/app/src/main/java/com/commonsware/android/design/til/LaunchDemo.java)

The Patterns class in Android contains a series of stock regular expressions. One, WEB_URL, is designed to see if the URL that was entered looks like a Web URL. When the user taps the button, if the pattern matches what the user entered in the field, we go ahead and try to start the activity. If not, we show an error.

To show the error, we need to do two things:

  1. Up front, we call setErrorEnabled(), to tell TextInputLayout to reserve some space for an error message
  2. At the point where we want to show the error, we call setError() on the TextInputLayout

When we run the app, the TextInputLayout leaves the hint in the EditText itself, as the EditText is empty and does not have the focus:

FloatingLabel Sample, As Initially Launched
Figure 501: FloatingLabel Sample, As Initially Launched

Once the user taps on the field, though, the hint “floats” above the EditText:

FloatingLabel Sample, After Focus Change
Figure 502: FloatingLabel Sample, After Focus Change

And, if the user tries entering an invalid URL, the error message appears when the user taps the button to try to visit the invalid URL:

FloatingLabel Sample, After Erroneous Data Entry
Figure 503: FloatingLabel Sample, After Erroneous Data Entry

CWAC-CrossPort

CWAC-CrossPort has a port of TextInputLayout and TextInputEditText, the latter being the EditText subclass that actually winds up being used with a TextInputLayout.

The DesignSupport/FloatingLabelCP sample project is a port of the FloatingLabel sample, converted to using CWAC-CrossPort. The standard sorts of changes seen earlier in this chapter, such as artifact dependencies and themes, are needed. In addition, the layout needs to change to use the CWAC-CrossPort editions of the classes:

<?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:orientation="vertical">

  <Button
    android:id="@+id/browse"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="showMe"
    android:text="@string/show_me">

    <requestFocus />
  </Button>

  <com.commonsware.cwac.crossport.design.widget.TextInputLayout
    android:id="@+id/til"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <com.commonsware.cwac.crossport.design.widget.TextInputEditText
      android:id="@+id/url"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:hint="@string/url"
      android:inputType="textUri" />
  </com.commonsware.cwac.crossport.design.widget.TextInputLayout>

</LinearLayout>
(from DesignSupport/FloatingLabelCP/app/src/main/res/layout/main.xml)

Third-Party Floating Labels

As with the rest of the Design Support library, TextInputLayout requires appcompat-v7. There are other implementations of the floating label pattern that do not require appcompat-v7, or perhaps offer additional features that you may want.

FloatLabeledEditText is one such implementation. It lacks the error message capability of TextInputLayout. However:

The DesignSupport/FloatingLabelNative sample project is a clone of the previous sample, where the TextInputLayout is replaced by a FloatLabeledEditText:

<?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:orientation="vertical">

  <com.wrapp.floatlabelededittext.FloatLabeledEditText
    android:id="@+id/til"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <EditText
      android:id="@+id/url"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:hint="@string/url"
      android:inputType="textUri"/>
  </com.wrapp.floatlabelededittext.FloatLabeledEditText>

  <Button
    android:id="@+id/browse"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="showMe"
    android:text="@string/show_me"/>

</LinearLayout>
(from DesignSupport/FloatingLabelNative/app/src/main/res/layout/main.xml)

As with TextInputLayout, FloatLabeledEditText is a decorating container around a regular EditText. Here, since the hint is left alone when the EditText gets focus, we have it back in its original position at the top of the form.

Visually, it is fairly similar to TextInputLayout, albeit with the native action bar:

FloatingLabelNative Sample, As Initially Launched
Figure 504: FloatingLabelNative Sample, As Initially Launched

FloatingLabelNative Sample, After Floating