DesignLibrary / DrawerLayout / Material Design / NavigationView

Design Library – Part 1

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.

Although we’re going to re-write the earlier app, that only really applies to the UI layer – we’re going to re-use the RSS retrieval and parsing code (so I won’t bother explaining it here). However, we’re going to modify the data model slightly to facilitate a new navigation model that we’re going to use in this app. The basic idea is that the Navigation Drawer will contain a list of articles – each article is composed of the individual parts (which have been published as individual blog posts). When an article is selected, a ViewPager will be set up with each page within the ViewPager holding a separate part. So we need a couple of new classes:

public class Article {
    private final String title;
    private final SparseArray<Item> parts;

    public static Article newInstance(String title) {
        SparseArray<Item> parts = new SparseArray<>();
        return new Article(title, parts);
    }

    public Article(String title, SparseArray<Item> parts) {
        this.title = title;
        this.parts = parts;
    }

    public String getTitle() {
        return title;
    }

    public void addPart(int partNumber, Item part) {
        parts.put(partNumber, part);
    }

    public Item getPart(int partNumber) {
        return parts.get(partNumber);
    }
}

This represents each article. Note the use of a SparseArray here. The reason for this is that the RSS feed returns the last 10 published articles, so we are not guaranteed to receive all of the posts in each article. By using a SparseArray we can cope with a set of articles which does not start at the first post.

The other class represents a group of articles and is responsible for parsing these from the existing Feed object:

public class Articles implements Iterable<String> {
    private static final String TITLE_REGEX_STRING = "(.*)\\s+\\u2013\\s+Part\\s+([0-9]+)";
    private static final Pattern TITLE_PATTERN = Pattern.compile(TITLE_REGEX_STRING);
    private static final int TITLE_GROUP = 1;
    private static final int PART_NUMBER_GROUP = 2;

    private final Map<String, Article> articles;
    private final List<String> titles;

    public static Articles transform(Feed feed) {
        Map<String, Article> articles = new HashMap<>();
        List<String> titles = new ArrayList<>();
        for (Item item : feed.getItems()) {
            String title = item.getTitle();
            Matcher matcher = TITLE_PATTERN.matcher(title);
            if (matcher.find()) {
                String baseTitle = matcher.group(TITLE_GROUP);
                int part = Integer.parseInt(matcher.group(PART_NUMBER_GROUP));
                Article article = articles.get(baseTitle);
                if (article == null) {
                    article = Article.newInstance(baseTitle);
                    articles.put(baseTitle, article);
                    titles.add(baseTitle);
                }
                article.addPart(part, item);
             } else {
                Article article = Article.newInstance(title);
                articles.put(title, article);
                titles.add(title);
           }
        }
        Collections.sort(titles);
        return new Articles(titles, articles);
    }

    Articles(List<String> titles, Map<String, Article> articles) {
        this.titles = titles;
        this.articles = articles;
    }

    @Override
    public Iterator<String> iterator() {
        return titles.iterator();
    }

    public Article getArticle(String title) {
        return articles.get(title);
    }

    public Article getFirst() {
        return articles.get(titles.get(0));
    }
}

The basic principle here is that each article has a title of the form “My Title – Part x” and we use a regular expression to extract the base title (i.e. “My Title” in the example) and the part number (i.e. “x” in the example) and create a list of Article objects and a sorted list of title Strings which can be queried externally.

There are some standard colours and strings which I’ve created in resources, but let’s take a quick look at our app 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 extended a standard “NoActionBar” AppCompat theme but we’re going to use an ActionBar in the app. The reason for choosing this theme is that we’re going to use our own Toolbar implementation in our layout, so we don’t want the system to create an ActionBar for us. The other thing worth noting is that we declare a base theme which is used as the parent of our main theme which doesn’t override anything. The reason for this is that we need to add some additional attributes on Lollipop and later devices so we’re defining a common base:

<?xml version="1.0" encoding="utf-8"?>
<resources>

  <style name="AppTheme" parent="Base.AppTheme">
    <item name="android:windowDrawsSystemBarBackgrounds">true</item>
    <item name="android:statusBarColor">@android:color/transparent</item>
  </style>
</resources>

This gives us a transparent status bar on Lollipop and later.

Next let’s take a look at our main activity layout:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  android:id="@+id/drawer_layout"
  android:layout_height="match_parent"
  android:layout_width="match_parent"
  android:fitsSystemWindows="true">

  <include layout="@layout/include_main" />

  <android.support.design.widget.NavigationView
    android:id="@+id/nav_view"
    android:layout_height="match_parent"
    android:layout_width="wrap_content"
    android:layout_gravity="start"
    android:fitsSystemWindows="true"
    app:headerLayout="@layout/nav_header" />

</android.support.v4.widget.DrawerLayout>

This defines a DrawerLayout, the container within which both the NavigationView and the main layout live.

It is possible to specify a menu resource on NavigationView which can display a static navigation menu and is a simple yet powerful way of implementing static menus. However our menu items will be determined by the content of the RSS feed, so we’ll need to do this programatically later on.

The NavigationView also specifies a headerLayout which will be used as the header above the navigation list. Typically this could hold user information, but in our case we’ll just hold a static Styling Android identifier as all of the content is from a Styling Android RSS feed:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  style="@style/TextAppearance.AppCompat.Title.Inverse"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:layout_gravity="bottom|center"
  android:background="?attr/colorPrimary"
  android:drawableLeft="@mipmap/ic_launcher"
  android:gravity="center_vertical|start"
  android:minHeight="100dp"
  android:paddingLeft="8dp"
  android:paddingTop="25dp"
  android:text="@string/styling_android"
  tools:ignore="Overdraw,RtlHardcoded,RtlSymmetry" />

The other layout included in the activity layout is include_main:

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

  <android.support.v7.widget.Toolbar
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="?attr/colorPrimary"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />

</FrameLayout>

This is pretty simple for now – we’ll add to it later. It contains our Toolbar with the background colour set to the primary colour from our theme, and with an appropriate ActionBar theme applied.

We have a couple of classes which link the Activity to the data – DataFragment (which was discussed in depth in Material – Part 1) and ArticlesConsumer which is the equivalent of FeedConsumer from the Material app. I won’t bother covering these here.

The remaining thing to cover is our MainActivity:

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 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);

        setupToolbar();
        setupNavigationView();
        setupDataFragment();
    }

This is pretty straightforward, we set out content view, finds instances of our DrawerLayout, and NavigationView objects, and set up the Toolbar, NavigationView and DataFragment. Let’s look at each of these in turn:

private void setupToolbar() {
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    ActionBar actionBar = getSupportActionBar();
    if (actionBar != null) {
        actionBar.setHomeAsUpIndicator(R.drawable.ic_menu);
        actionBar.setDisplayHomeAsUpEnabled(true);
    }
}

This finds the instance of our Toolbar from the layout sets it as the support ActionBar, and then set it up to show a hamburger menu icon.

private void setupNavigationView() {
    navigationView.setNavigationItemSelectedListener(
            new NavigationView.OnNavigationItemSelectedListener() {
                @Override
                public boolean onNavigationItemSelected(MenuItem menuItem) {
                    menuItem.setChecked(true);
                    drawerLayout.closeDrawers();
                    selectArticle(menuItem.getTitle());
                    return true;
                }
            });
}

This set up an item click listener which will get called whenever an item clicked. When this happens it will set the item as checked, close the drawer, and trigger a change in the selected article (more in this shortly).

private void setupDataFragment() {
    DataFragment dataFragment = (DataFragment) getSupportFragmentManager().findFragmentByTag(DATA_FRAGMENT_TAG);
    if (dataFragment == null) {
        dataFragment = (DataFragment) Fragment.instantiate(this, DataFragment.class.getName());
        dataFragment.setRetainInstance(true);
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        transaction.add(dataFragment, DATA_FRAGMENT_TAG);
        transaction.commit();
    }
}

This checks to see if there is already an instance of the DataFragment in the FragmentManager and creates a new instance (and stores is) if there isn’t.

Next we need to add a handler for the hamburger menu (which we have already set up) being pressed:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case android.R.id.home:
            drawerLayout.openDrawer(GravityCompat.START);
            return true;
        default:
            //NO-OP
            break;
    }
    return super.onOptionsItemSelected(item);
}

All that remains is to implement the ArticlesConsumer methods (as the activity implements this interface):

@Override
public void setArticles(Articles articles) {
    Menu menu = navigationView.getMenu();
    menu.clear();
    this.articles = articles;
    int item = 0;
    for (String articleTitle : articles) {
        menu.add(MENU_GROUP, item, item, articleTitle);
        if (item == 0) {
            menu.getItem(0).setChecked(true);
        }
        item++;
    }
    menu.setGroupCheckable(MENU_GROUP, true, true);
    setCurrentArticle(articles.getFirst());
}

@Override
public void handleError(String message) {
    Snackbar.make(drawerLayout, message, Snackbar.LENGTH_LONG).show();
}

private void selectArticle(CharSequence title) {
    Article article = articles.getArticle(title.toString());
    setCurrentArticle(article);
}

private void setCurrentArticle(Article article) {
    setTitle(article.getTitle());
}

When the list of Articles is updated, setArticles() is called which constructs the new menu for our NavigationView and check the first one which will will display by default.

In the event of an error, handleError() gets called, and we construct a SnackBar to display the error. SnackBar is constructed in much the same way as a Toast with the addition of a View to which the SnackBar is associated. Normally this will be a child of a CoordinatorLayout (which we’ll cover later in the series) but for now we’ll just use our top level layout.

The remaining two methods set the current article – for now we’ll just change the title.

If we run this we see:

Part1-SnackBar

That’s when we remember to add the INTERNET permission to our manifest, but demonstrates that the SnackBar is working!

Adding the require permissions sees us with a fully working Navigation Drawer on API 21:

So, we have a working Navigation Drawer! In the next article we’ll Take a look at implementing the ViewPager for handling the individual article parts.

The source code for 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.

3 Comments

  1. Hi Mark, in DataFragment you initialize VolleySingleton with inflater.getContext(), but inflater will return you Activity context, so after finishing MainActivity your VolleySingleton will contain reference to Activity context. And thanks for great article.

    1. Good spot! I have now fixed it. I use the Application Context instead.

      Thanks for letting me know.

  2. Hy Mark,
    This is good and informative post. But i am getting error in activity_main.xml ‘headerlayout’ attribute is not found.

    Thanks
    Faisal Ahsan

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.