DesignLibrary / Material Design / TabLayout

Design Library – Part 2

At Google I/O 2015 the Material Design Support Library was announced, and with it creating material apps targeting API levels below API 19 suddenly got a lot easier. In this series we’ll look at taking the RSS Reader app that we used as a basis for the Material series, and re-write it to make full use of the new Design Support Library. Previously we looked at getting a basic navigation drawer working and in this post we’ll look at how to implement a tab bar.

ViewPager has been around for a while and is a well used and understood component, so I won’t bother with a full explanation of it here. But let’s begin with looking at the Fragment implementation that will represent each page in our ViewPager. The content of each item within the RSS feed is basic HTML so we’ll use a WebView to handle the rendering for us (Please don’t hate me for how horribly this renders – it is basic HTML without any CSS so it renders how it renders. The point of this article is about the stuff which happens around the content and not how the content itself is rendered).

Let’s look at the fragment layout:

<WebView xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/web_view"
  android:layout_width="match_parent"
  android:layout_height="match_parent" />

That’s pretty straightforward, so let’s also take a look at the Fragment implementation:

public class ItemFragment extends Fragment {
    private static final String KEY_ITEM = "ARG_ITEM";
    public static final String NEWLINE = "\\n";
    public static final String BR = "<br />";
    public static final String HTML_MIME_TYPE = "text/html";

    public static Fragment newInstance(Context context, Item item) {
        Bundle args = new Bundle();
        args.putSerializable(KEY_ITEM, item);
        return Fragment.instantiate(context, ItemFragment.class.getName(), args);
    }

    @Override
    @SuppressLint("SetJavaScriptEnabled")
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        Item item = (Item) getArguments().getSerializable(KEY_ITEM);
        View view = inflater.inflate(R.layout.fragment_item, container, false);
        if (item != null) {
            WebView webView = (WebView) view.findViewById(R.id.web_view);
            String html = item.getContent();
            html = html.replaceAll(NEWLINE, BR);
            webView.getSettings().setUseWideViewPort(true);
            webView.getSettings().setLoadWithOverviewMode(true);
            webView.getSettings().setJavaScriptEnabled(true);
            webView.loadData(html, HTML_MIME_TYPE, null);
        }
        return view;
    }
}

Once again, this is pretty straightforward – an Item is passed in, and the content of this is rendered in the WebView. The only things worth noting are that we replace newlines with <br /> to improve the rendering slightly. Also we call setUseViewPortMode(true) and setLoadWithOverviewMote(true) to start things zoomed out (so that the entire width of the content is displayed). This is to remove any horizontal scrolling which will affect the operation of the ViewPager container. (once again, do’t hate me for this – the ViewPager is the focus of this article so we’ll fix the WebView rendering so as not to interfere with that).

Now that we have the individual pages for the ViewPager defined, let’s take a look at the Adapter which will connect these up to the ViewPager:

 public final class ArticleViewPagerAdapter extends FragmentPagerAdapter {
    private final Article article;
    private final Context context;
    private final Resources resources;

    public static ArticleViewPagerAdapter newInstance(FragmentManager fragmentManager, Context context, Article article) {
        Resources resources = context.getResources();
        return new ArticleViewPagerAdapter(fragmentManager, context, resources, article);
    }

    private ArticleViewPagerAdapter(FragmentManager fragmentManager, Context context, Resources resources, Article article) {
        super(fragmentManager);
        this.context = context;
        this.resources = resources;
        this.article = article;
    }

    @Override
    public Fragment getItem(int position) {
        Item item = article.getPartAtPosition(position);
        return ItemFragment.newInstance(context, item);
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        FragmentManager manager = ((Fragment) object).getFragmentManager();
        FragmentTransaction trans = manager.beginTransaction();
        trans.remove((Fragment) object);
        trans.commit();
        super.destroyItem(container, position, object);
    }

    @Override
    public int getCount() {
        return article.getPartsCount();
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return resources.getString(R.string.part_title, article.getPartNumber(position));
    }
}

This is a pretty unremarkable Adapter implementation. The only thing worthy of note is the inclusion of a getPageTitle() implementation (where we generate a title in the form “Part X”). Anyone who has used PagerTitleStrip from the v4 support library will be familiar with this, and the new TabLayout implementation uses exactly the same mechanism to get the text to go in to each tab item.

Next we need to look at out main layout. Previously we defined a FrameLayout containing our Toolbar, but now we ned to add our TabLayout and ViewPager:



  

  

  


In here we’ve added our TabLayout – this control is going to do all of the hard work for us! We have a choice of operation modes – fixed, where the tabs are dynamically sized so that the TabLayout exactly fits the space available; and scrollable, where the tabs themselves are a fixed size, and if they exceed the space available the user will be able to scroll the tabs to get to them all.

A general rule of thumb is that if you have a relatively small number of tabs then fixed mode works really well, but a large number of tabs works best with scrollable. In this app the number of items in each article will vary and may sometimes be quite large (Dirty Phrasebook in the example videos consist of 6 parts), so I’ve gone for scrollable.

Because I elected to go with scrollable mode, I actually want the left hand edge of the first tab text to align to the title text in the Toolbar so I gave a left padding to achieve this. By setting android:clipToPadding="false" the tabs will scroll in to the blank area left by this margin when we scroll the tabs.

The only remaining component in the layout is our ViewPager.

All that remains is to wire this all together in our Activity:

public class MainActivity extends AppCompatActivity implements ArticlesConsumer {
    private static final String DATA_FRAGMENT_TAG = DataFragment.class.getCanonicalName();
    private static final int MENU_GROUP = 0;

    private DrawerLayout drawerLayout;
    private NavigationView navigationView;
    private ViewPager viewPager;
    private TabLayout tabLayout;

    private Articles articles;

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

        drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
        navigationView = (NavigationView) findViewById(R.id.nav_view);
        viewPager = (ViewPager) findViewById(R.id.viewpager);
        tabLayout = (TabLayout) findViewById(R.id.tab_layout);

        setupToolbar();
        setupNavigationView();
        setupDataFragment();
    }
    .
    .
    .
    private void setCurrentArticle(Article article) {
        setTitle(article.getTitle());
        ArticleViewPagerAdapter adapter = ArticleViewPagerAdapter.newInstance(getSupportFragmentManager(), this, article);
        viewPager.setAdapter(adapter);
        if (article.getPartsCount() <= 1) {
            tabLayout.setVisibility(View.GONE);
        } else {
            tabLayout.setVisibility(View.VISIBLE);
        }
        tabLayout.setupWithViewPager(viewPager);
    }
}

All of the real work is done in setCurrentArticle() when a new Article is selected. First we set up a new ArticleViewPagerAdapter instance based on the new Article, and we apply this to the ViewPager. Next we do a quick check to either hide the TabLayout if there is only a single part in the article, or show it otherwise. Finally we hook the TabLayout to the ViewPager which initialises the tabs, and hooks up all of the paging / tab selection logic for us. That is one powerful method call!

If we run this we can see we have a fully implemented tab bar:

There is, however, one subtly problem here. The tab bar is supposed to have an indicator bar at the bottom of the selected tab. The reason that this is missing is because of the colorAccent value that we defined in our theme:

<resources>

  <!-- Base application theme. -->
  <style name="AppTheme" parent="Base.AppTheme" />

  <style name="Base.AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <item name="colorPrimary">@color/sa_green</item>
    <item name="colorPrimaryDark">@color/sa_green_dark</item>
    <item name="colorAccent">@color/sa_green</item>
    <item name="colorControlHighlight">@color/sa_green_transparent</item>
  </style>

</resources>

We’ve declared this as the same colour as our colorPrimary. colorPrimary will be used as the background colour of our AppBarLayout, and colorAccent will be used for the indicator bar colour. As these are the same, our indicator bar is not showing. For now we’ll set this to white (but this will cause us problems further down the line with our FloatingActionButton but we’ll deal with that when we get to it):

<resources>

  <!-- Base application theme. -->
  <style name="AppTheme" parent="Base.AppTheme" />

  <style name="Base.AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <item name="colorPrimary">@color/sa_green</item>
    <item name="colorPrimaryDark">@color/sa_green_dark</item>
    <item name="colorAccent">@android:color/white</item>
    <item name="colorControlHighlight">@color/sa_green_transparent</item>
  </style>

</resources>

Now we have a working indicator bar:

That’s looking pretty good, but wouldn’t it be nice if we could do a very material-like thing and scroll the Toolbar away as we scroll down, and provide a quick return as the user scrolls back up? In the next article we’ll do precisely that.

The source code for this article is available here.

© 2015, Mark Allison. All rights reserved.

Copyright © 2015 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

1 Comment

  1. Just a note that you can use app:tabContentStart on the TabLayout instead of android:clipToPadding and android:paddingLeft. This way you can use the proper keyline (72dp).

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.