Lecture 5 Material Design

This lecture discusses Material design: the design language created by Google to support mobile user interfaces. The design language focuses on mobile designs (e.g., for Android), but can also be used across platforms (e.g., on the web through css frameworks).

This lecture references code found at https://github.com/info448/lecture05-material.

Material Design support was introduced in API 21 Lollipop, and so a device running that version of Android or later is required to access all the Material features supported by Android. However, most functionality is also available via compatibility libraries (i.e., AppCompat), and so can be utilized on older devices as well.

Google is in the process of updating their Material Design implementation to use Material Design Components (MDC). As that version is still in beta and only available on Android Pie, this lecture describes how to use the “legacy” version of the Material support library.

5.1 The Material Design Language

Material design is a design language: a “vocabulary” of visual and interactive patterns that are shared across systems. Material forms both a “look and feel” for Google applications and modern (API 21+) Android apps in general, as well as providing a

A video explanation of the Material language (via https://developer.android.com/design/material/index.html)

In summary, the Material Design language is based around three main principles:

  • Material is the metaphor. The Material design language is built around presenting user interfaces as being made of virtual materials, which ara paper-like surfaces floating in space. Each surface has a uniform thickness (1dp), but different elevation (conveyed primarily through shadows and perspective). This physical metaphor helps to indicate different affordances and user interactions: for example, a button is “raised” and can be “pushed down”.

  • Motion provides meaning. Material also places great emphasis on the use of motion of these materials in order to help describe the relationships between components as well as make design overall more “delightful”. Material applications include lots of animations and flourishes, making the app’s usage continuous and connected. Surfaces are able to change shape, size, and position (as long as they stay in their plane—they cannot fold, but they can split and rejoin) in response to user input.

  • Bold, graphic, intentional. Material applications follow a particular aesthetic in terms of things such as color (not muted), imagery (usually lots of it). Material applications look like material applications, though they can still be customized to meet your particular needs.

For more information on the Material design language, see the official guidelines (click the hamburger button on the left to navigate). This documentation contains extensive examples and instructions, ranging from suggested font sizes to widget usage advice.

This lecture focuses on ways to implement specific aspects of the Material Design language in Android applications, rather than on specifics of how to obey the language guidelines.

5.2 Material Styles & Icons

The first and easiest step towards making a Material-based application is to utilize the provided Material Themes in order to “skin” your application. These themes are available on API 21 Lollipop and later; for earlier operating system, you can instead use equivalent themes in AppCompat (which are in fact the default themes for new Android apps!)

Applying themes (including Material themes) are discussed in more detail in the Styles & Themes lab.

  • You can see what specific properties are applied by these styles and themes by browsing the source code for the Android framework—check out the styles_material.xml and themes_material.xml (these will also reference values defined in the variable color and dimens resources). The AppCompat styles and themes can be source code for the v7 support library.

Let’s start by pointing to one of the most prominent visual components in the default app: the App Bar or Action Bar. This acts as the sort of “header” for your app, providing a dedicated space for navigation and interaction (e.g., through menus). The ActionBar14 is a specific type of Toolbar that is most frequently used as the App Bar, offering a particular “look and feel” common to Android applications.

While the AppCompatActivity used throughout this course automatically provides an Action Bar for the app, it is also possible to add it directly (such as if you are using a different Activity subclass). To add your own Action Bar, you specify a theme that does not include an ActionBar, and then include an <android.support.v7.window.Toolbar> element inside your layout wherever you want the toolbar to go. See Setting up the App Bar for details. This will also allow you to put the Toolbar anywhere in the application’s layout (e.g., if you want it to be stuck to the bottom).

In practice, the biggest part of utilizing a Material Theme is defining the color palette for your app. The Material design specification describes a broad color palette (with available swatches); however, it is often more useful to pick your colors using the provided color picker tool15, which allows you to easily experiment with different color combinations.

  • Pick your primary and secondary color, and then assign the resource values colorPrimary, colorPrimaryDark, and colorAccent in res/values/colors.xml. This will let you easily “brand” your application.

Material-specific attributes such as android:elevation are also available in API 21+, though making shadows visible on arbitrary elements requires some additional work.

In addition to styles, Material also includes a large set of icons for use in applications. These icons supplement the built in ic_* drawables that are built into the platform and are available for use (and show up as auto-complete options in the IDE).

Instead these icons are available as vector drawables—rather than being a .png file, images are defined as using an XML schema similar to that used in Scalable Vector Graphics (SVG).

SVG is used to represent vector graphics—that is, pictures defined in terms of the connection between points (lines and other shapes), rather than in terms of the pixels being displayed (called a raster image). In order to be shown on a screen, these lines are converted (rastered) into grids of pixels based on the size and resolution of the display. This allows SVG images to “scale” or “zoom” independent of the size of the display or the image—you never need to worry about things getting blurry as you make them larger!

In order to include a Material icon, you will need to generate the XML code for that particular image. Luckily, Android Studio includes XML definitions for all the Material icons (though you can also define your own vector drawables).

  • To create a vector drawable, select File > New > Vector Asset from the Android Studio menu. You can then click on the icon to browse for which Material icon you want to create.

  • By default the icon will be colored black. If you wish to change the color of the icon when included in your layout, specify the color through the android:tint attribute of the View in your XML (e.g., on the ImageButton).

It is also possible to define arbitrary vector shapes in API 21+. For example, the starter code includes a resource for a <shape> element that is shaped like an oval.

5.3 Design Support Libraries

In addition to the styling and resource properties available as part of the Android framework, there are also additional components available through extra support libraries.

Similar to Volley, these libraries are not built into Android and so need to be explicitly listed under dependencies in your app’s build.gradle file. For example:

//note the version number needs to match your SDK version
implementation 'com.android.support:design:28.0.0'

will include the latest version (as of this writing) of the design support library, which includes elements such as the Floating Action Button and Coordinator Layouts (described below).

Note that if you have issues installing the dependency, the setup for including support libraries was changed in July 2017 to support downloading dependency libraries through maven rather than directly from Android Studio. To support this, you will need to change the project’s build.gradle repository declaration to look like:

allprojects {
    repositories {
        jcenter()
        maven {
            url "https://maven.google.com"
        }
    }
}

Widgets

The support libraries support a number of useful widgets (specialized Views) for creating Material-styled apps.

RecyclerView

The RecyclerView is a more advanced version of a ListView, providing a more robust system for support interactive Views within the list as well as providing animations for things like adding and removing list items.

This class is part of the v7 support library (and not the Material Design library specifically), and so you will need to include it specifically in your app’s build.gradle file (the same way you included Volley):

implementation 'com.android.support:cardview-v7:28.0.0'

Implementing a RecyclerView is very similar to implementing a ListView (with the addition of the ViewHolder pattern), though you also need to declare a LayoutManager to specific if your RecyclerView should use a List or a Grid.

The best way to understand the RecyclerView is to look at the example and modify that to fit your particular item view. For example, you can change a ListView into a RecyclerView by adapting the sample code provided by Google’s official documentation16:

  • First, you will need to replace the XML View declaration with a <android.support.v7.widget.RecyclerView> element. In effect, we’re just modifying the controller for the list.

  • There are a few extra steps when setting up the RecyclerView in the Java code. In particular, you will also need to associate a LayoutManager object to with the View—for example, a LinearLayoutManager to show items in a line (a la a ListView), or a GridLayoutManager to show items in a grid (a la a GridView).

    • In Kotlin, these can be assigned using the .apply() method. This method takes a callback function, and executes each line in that callback function with the object (e.g., the recyclerView) scoped as this. This is a shortcut for calling lots of methods on the same object in a row.

      Similar shortcuts are provided by the run(), with() and let() functions. See this guide for a clear explanation.

  • All RecyclerViews require custom adapters: we cannot just use a built-in adapter such as ArrayAdapter. To do this, create a class that extends RecyclerView.Adapter<VH>.

    • The generic in this class is a class representing a View Holder. This is a pattern by which each individual View that will be inflated and referenced is stored as an individual object with the particular Views to be modified (e.g., TextView) saved as instance variables. This avoids the need for the adapter to repeatedly use findViewById(), which is an expensive operation (since it involves crawling the View hierarchy tree!)

      The ViewHolder will be another class—usually an inner class of the adapter. We can treat it as a data class (a simple “container” object, similar to a struct in C). but don’t need to declare it as such directly. If needed, you can assign these instance variables the results of the findViewById() calls when the ViewHolder object is initialized.

    • The RecyclerView (not the ViewHolder) requires you to override the onCreateViewHolder() method, which will inflate() the View and instantiate a ViewHolder for each item when that item needs to be displayed for the first time.

    • In the overridden onBindViewHolder() you can do the actual work of assigning model content to the particular View (e.g., called setText()). You don’t need to use findViewById() here, because you’ve already saved a reference to that View in the ViewHolder, so you can assign to it directly!

    • The getItemCount() is used internally for the RecyclerView to determine when you’ve gotten to the “end” of the list.

  • Finally, you can assign the custom adapter to the RecyclerView in order to associate the model and the View.

And that’s about it! While it is more code and more complex than a basic ListView, as soon as you’ve added in the ViewHolder pattern or done any other kinds of customizations, you’re basically at the same level. Using a RecyclerView instead of a ListView also enables built-in animations, as well as common user actions such as “drag-to-order” or “swipe-to-dismiss”. See this guide for a walkthrough on adding these capabilities.

Note that unlike with a ListView, we usually modify the items shown in a RecyclerView by modifying the data model (e.g., the array or ArrayList) directly. But we will need to notify the RecyclerView’s adapter of these changes by calling one of the various .notify() methods on the adapter (e.g., adapter.notifyItemInserted(position)). This will cause the adapter to “refresh” the display, and it will actually animate the changes to the list by default!

Cards

The v7 support library also provides a View for easily styling content as Cards. A CardView is basically a FrameLayout (a ViewGroup that contains one child View), but include borders and shadows that make the group look like a card.

  • You will need to load the CardView class as a gradle dependency using compile 'com.android.support:cardview-v7:26.1.0'.

To utilize a CardView, simply include it in your layout as you would any other ViewGroup:

<android.support.v7.widget.CardView
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:layout_width="@dimen/card_width"
    android:layout_height="@dimen/card_height"
    android:layout_gravity="center"
    card_view:cardCornerRadius="4dp">

    <!-- A single TextView in the card -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/card_text" />

</android.support.v7.widget.CardView>
  • Notice the card_view:cardCornerRadius attribute; this is an example of how specific Views may have their own custom properties (sometimes available via a different schema).

Since Cards are FrameLayouts, they should only contain a single child. If you want to include multiple elements inside a Card (e.g., an image, some text, and a button), you will need to nest another ViewGroup (such as a LinearLayout) inside the Card.

For design suggestions on using Cards, including spacing information, see the Material Design guidelines.

If you want to include a circular image in your card (or anywhere else in your app), the easiest solution is to include an external library that provides such a View, the most popular of which is de.hdodenhof.circleimageview.

Floating Action Buttons (FAB)

While RecyclerViews and Cards are found in the v7 support library, the most noticeable and interesting components come from the Design Support Library, which specifically includes components for supporting Material Design.

  • This library should be included in gradle as com.android.support:design:27.1.1, as in the above example.

The most common element from this library is the Floating Action Button (FAB). This is a circular button that “floats” above the content of the screen (at a higher elevation), and represents the primary action of the UI.

  • A screen should only ever have one FAB, and only if there is a single main action that should be performed. See the design guidelines for more examples on how to use (and not use) FABS.

Like a Card, you can include a FAB in your application by specifying it as an element in your XML:

<android.support.design.widget.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="end|bottom"
        android:layout_margin="@dimen/fab_margin"
        android:src="@drawable/ic_my_icon" />
  • Because FABs are a subclass of ImageButton, we can just replace an existing button with a FAB without breaking anything.

Fabs support a number of additional effects. For example, if you make the FAB clickable (via android:clickable), you can specify an app:rippleColor to give it a rippling effect when pressed. Further details will be presented below.

Snackbars

The Design Support Library also includes an alternative to Toast messages called Snackbars. This is a user-facing pop-up message that appears at the bottom of the screen (similar to a Toast).

Snackbars are shown using a similar structure to Toasts:

val snack = Snackbar.make(view, "Let's go out to the lobby!", Snackbar.LENGTH_LONG).show();
  • Instead of calling the Toast.makeText() factory method, we call the Snackbar.make() factory method. The first parameter in this case needs to be a View that the Snackbar will be “attached” to (so shown with)—however, it doesn’t really matter which View is given, since the method will search up the view hierarchy until it gets to the root content view or a special layout called a CoordinatorLayout.

  • You’ll notice that the Snackbar overlays the content (including the FAB). This will be addressed below by introducing a CoordinatorLayout.

Additionally, it is possible to give Snackbars their own action that the user can activate by clicking on the Snackbar. This allows the bar to, for example, show a delete confirmation but providing an “undo” action. The action is specified by calling the .setAction() method on the Snackbar, and passing in a title for the action as well as an OnClickListener:

mySnackbar.setAction("Click", new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        //...
    }
});
snack.setAction("Click") {
    //...
};
  • For practice, make the Snackbar .hide() the FAB, but provide an “undo” action that will .show() it!

Again, see the design guidelines for more examples on how to use (and not use) Snackbars.

Coordinator Layout

In order to fix the Snackbar and Fab overlap, we’ll need to utilize one of the most powerful but complex classes in the Material support library: the CoordinatorLayout. This layout is described as “a super-powered Framelayout”, and provides support for a number of interactive and animated behaviors involves other classes. A lot of the “whizbang” effects in Material are built on top of the CoordinatorLayout (or other classes that rely on it).

To start our exploration of CoordinatorLayout, let’s begin by fixing the Snackbar overlap. To do this, we’ll take the existing layout for the activity and “wrap” it in a <android.support.design.widget.CoordinatorLayout> element:

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- Previous layout elements -->
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- etc -->

    </RelativeLayout>
</android.support.design.widget.CoordinatorLayout>

We will also need to move the FAB definition so that it is a direct child of the CoordinatorLayout. Once we have done so, we should be able to click the button and watch it move up to make room for the Snackbar!

  • How this works is that the CoordinatorLayout allows you to give Behaviors to its child views; these Behaviors will then be executed when the state of the CoordinatorLayout (or its children) changes. For example, the built-in FloatingActionButton.Behavior defines how the button should move in response to its parent changing size, but Behaviors can also be defined in response to use interactions such as swipes or other gestures.

Scrolling Layouts

Indeed, the built-in behaviors can be quite complex (and wordy to implement), and the best way to understand them is to see them in action. To see an example of this, create a new Activity for your application (e.g., File > New > Activity). But instead of creating an Empty Activity as you’ve done before, you should instead create a new ScrollingActivity.

  • Modify the FAB action so that when you click on it, you send an Intent for to open up this new Activity:

    startActivity(new Intent(MainActivity.this, ScrollingActivity.class));
  • And once you’ve opened up the Activity… try scrolling! You should see the ActionBar collapse while the FAB moves up and disappears.

This is an example of a collection of behaviors built into CoordinatorLayout and other classes in the Design Support library. To get a sense for how they work, open up the newly created activity_scrolling.xml layout resource and see how this layout was constructed!

  • At the root of the layout we find the CoordinatorLayout, ready to “coordinate” all of its children and allow them to interact.

  • The first child is an AppBarLayout. This layout specifically supports responding to scroll events produced from within the CoordinatorLayout (e.g., when the user scrolls through the text content). You can control the visibility of this element based on scrolling using the app:layout_scrollFlags attribute.

    The AppBarLayout works together with its child CollapsingToolbarLayout, which does just what it says on the tin. It shows a larger title, but then shrinks down in response to scrolling. Here the scrollFlags are declared: scroll|exitUntilCollapse indicates two flags (combined with a bitwise OR |): that the content should scroll, and that it should shrink to its minimum height until it collapses.

    The Toolbar itself is finally defined as a child of CollapsingToolbarLayout, though other children could be added here as well. For example, an ImageView could be included as a child of the CollapsingToolbarLayout in order to create a collapsing image!

    (Check the documentation for details about all of the specific attributes).

  • After the collapsing AppBar, the CoordinatorLayout includes a NestedScrollView (declared in a separate file for organization). This is a scrollable view (similar to what is used in a ListView), but can both include and be included within scrollable layouts.

    • Notice that this element includes an app:layout_behavior attribute, which refers to a particular class: AppBarLayout$ScrollingViewBehavior (the $ is used to refer to a compiled nested class). This Behavior will “automatically scroll any AppBarLayout siblings”, allowing the scrolling of the page content to also cuase the AppBarLayout to scroll!
  • Finally, we have the FAB for this screen. The biggest piece to note here is how the FAB includes the app:layout_anchor attribute assigned a reference to the AppBarLayout. This indicates that the FAB should follow (scroll with) the AppBarLayout; the app:anchorGravity property indicates where it should be relative to its anchor. Moreover, the FAB’s default behavior will cause it to disappear when then is no room… and since the AppBarLayout exits on collapse, the FAB disappears as well!

In sum: the NestedScrollingView has a Behavior that will cause the AppBarLayout to scroll with it. The AppBarLayout has Behaviors that allow it to collapse, and the FAB is connected to that layout to moves up with it and eventually disappear.

Custom Behaviors

We can also create our own custom behaviors if we want to change the way elements interact with the CoordinatorLayout. For example, we can create a Behavior so that the FAB on the MainActivity shrinks and disappears when the Snackbar is shown, rather than moving up out of the way!

First, we will create a new Java class to represent our ShrinkBehavior. This class will need to extend CoordinatorLayout.Behavior<FloatingActionButton> (because it is a CoordinatorLayout.Behavior and it will be applied to a FloatingActionButton).

  • We will need also need to override the constructor so that we can declare/instantiate this class from the XML:

    public ShrinkBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

The next step is to make sure the Behavior is able to react to changes in the Snackbar. To do this we have to make the Behavior report that is has the Snackbar as a dependency. This way when the CoordinatorLayout is propagating events and changes to all of its children, it will know that it should also inform the FAB about changes to the Snackbar. We do this by overriding the layoutDependsOn() method:

public boolean layoutDependsOn(CoordinatorLayout parent,
                               FloatingActionButton child, View dependency) {
    //add SnackbarLayout to the dependency list (if any)
    return dependency instanceof Snackbar.SnackbarLayout ||
        super.layoutDependsOn(parent, child, dependency);
}
  • (Technically the super class doesn’t have any other dependencies, but it’s still good practice to call up the tree).

Finally, we can specify what should happen when one of the dependency’s Views change by overriding the onDependentViewChange() callback:

public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child, View dependency) {
        if(dependency instanceof Snackbar.SnackbarLayout){
            //calculate how much Snackbar we see
            float snackbarOffset = 0;
            if(parent.doViewsOverlap(child, dependency)){
                snackbarOffset = Math.min(snackbarOffset, dependency.getTranslationY() - dependency.getHeight());
            }
            float scaleFactor = 1 - (-snackbarOffset/dependency.getHeight());

            child.setScaleX(scaleFactor);
            child.setScaleY(scaleFactor);
            return true;
        }else {
            return super.onDependentViewChanged(parent, child, dependency);
        }
    }
  • This method will be passed a reference to the CoordinatorLayout that is managing the changes, the FloatingActionButton who is receiving the change, and which dependency had it’s View changed. We check that the dependency is actually a Snackbar (since we might have multiple dependencies and want to respond differently to each one), and then call some getters on that dependency to figure out how tall it is (and thus how much we should shrink by). Finally, we use setters to change the scaling of the child (the FAB), thereby having it scale!

  • And because this scale is dependent on the Snackbar’s height, the FAB will also “grow back” when the Snackbar goes away!

This is about the simplest form of Behavior we can have: more complex behaviors can be based on scroll or fling events, utilizing different state attributes for different dependencies!

Custom behaviors are very tricky to write and design; the overridden functions are not very well documented, and by definition these behaviors involve coordinating lots of different classes! Most custom behaviors are designed by reading the Android source code (e.g., for FloatingActionButton.Behavior) and modifying that as an example. My suggestion is to search online for behaviors similar to the one you’re trying to achieve, and work from there.

5.4 Animations

One of the key principles of Material Design was the use of motion: many of the previous examples have involved adding animated changes to elements (e.g., moving or scrolling). The Material theme available in API 21+ provides a number of different techniques for including animations in your application—in particular, using animation to give user feedback and provide connections between elements when the display changes. As a final example, this section will cover how to add simple Activity Transitions so that Views “morph” from one Activity to the next.

In order to utilize Activity Transitions, you will need to enable them in your application’s theme by adding an addition <item> in the theme declaration (in res/values/styles.xml):

<!-- in style: enable window content transitions -->
<item name="android:windowActivityTransitions">true</item>

There are three different kinds of Activity Transitions we can specify:

  1. enter transitions, or how Views in an Activity enter the screen
  2. exit transitions, or how Views in an Activity leave the screen
  3. shared element transitions, or how Views that are shared between Activities change

We’ll talk about the later, though the previous two follow a similar process.

In order to animate a shared element between two Activities, we need to give them matching identifiers so that the transition framework knows to animate them. We do this by giving each of the shared elements an android:transitionName attribute, making sure that they have the same value.

  • For example, we can give the FAB in each activity an android:transitionName="fab".

  • Because the FAB is anchored, we actually need to do some extra work (because FAB will morph to the “unanchored” point and then get moved once the anchorage is calculated). The easiest workaround for this is to wrap the anchored FAB inside an anchored FrameLayout—the FAB just then becomes a normal element with the FrameLayout handling the scrolling behavior.

    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_anchor="@id/app_bar"
        app:layout_anchorGravity="bottom|end"
        android:elevation="12dp"
        >
    
        <android.support.design.widget.FloatingActionButton
            android:id="@+id/fab"
            android:transitionName="same_fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/fab_margin"
            app:srcCompat="@android:drawable/ic_dialog_email" />
    </FrameLayout>

Finally, we need to make sure that the Intent used to start the Activity also starts up the transition animation. We do this be including an additional argument to the startActivity() method: an options Bundle containing details about the animation:

//transition this single item
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(MainActivity.this, button, "fab");

// start the new activity
startActivity(new Intent(MainActivity.this, ScrollingActivity.class), options.toBundle());

This should cause the FAB to “morph” between Activities (and even morph back when you hit the “back” button)!

For more about what makes effective animation, see the Material deisgn guidelines.

I find Activity Transitions to be slick, but finicky: getting them to work properly takes a lot of effort and practice. Most professional apps that utilize Material Design use extensive custom animations for generating these transitions. For examples of more complex Material Design animations and patterns, check out sample applications such as cheesesquare or plaid.

Resources