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
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
andthemes_material.xml
(these will also reference values defined in the variablecolor
anddimens
resources). TheAppCompat
styles and themes can be source code for thev7
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 ActionBar
14 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
, andcolorAccent
inres/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 theImageButton
).
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 aLayoutManager
object to with the View—for example, aLinearLayoutManager
to show items in a line (a la a ListView), or aGridLayoutManager
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., therecyclerView
) scoped asthis
. This is a shortcut for calling lots of methods on the same object in a row.Similar shortcuts are provided by the
run()
,with()
andlet()
functions. See this guide for a clear explanation.
All
RecyclerViews
require custom adapters: we cannot just use a built-in adapter such asArrayAdapter
. To do this, create a class thatextends 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 usefindViewById()
, 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 adata
class (a simple “container” object, similar to astruct
in C). but don’t need to declare it as such directly. If needed, you can assign these instance variables the results of thefindViewById()
calls when the ViewHolder object is initialized.The
RecyclerView
(not theViewHolder
) requires you to override theonCreateViewHolder()
method, which willinflate()
the View and instantiate aViewHolder
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., calledsetText()
). You don’t need to usefindViewById()
here, because you’ve already saved a reference to that View in theViewHolder
, 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 usingcompile '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
.
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 theSnackbar.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 aCoordinatorLayout
.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 theapp: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 thescrollFlags
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, anImageView
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 aNestedScrollView
(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!
- Notice that this element includes an
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; theapp: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, theFloatingActionButton
who is receiving the change, and which dependency had it’s View changed. We check that the dependency is actually aSnackbar
(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 thechild
(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:
- enter transitions, or how Views in an Activity enter the screen
- exit transitions, or how Views in an Activity leave the screen
- 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
- Material Design Guidelines check the whole document (via the hamburger menu on the left).
- Material Design for Developers (Google) official documentation for implementing material design
- Material Design Primer (CodePath) excellent compiled documentation and examples for implementing Material patterns (CodePath in general is an excellent resource).
- Android Design Support Library (Google Blog) an introduction to the support library features
- Mastering the Coordinator Layout (Blog) a great set of examples of how to use CoordinatorLayout.
Using CoordinatorLayout in Android Apps (Blog) another good explanation of CoordinatorLayout
- https://lab.getbase.com/introduction-to-coordinator-layout-on-android/
https://medium.com/@andkulikov/animate-all-the-things-transitions-in-android-914af5477d50