5.2: Material Design: Lists, Cards and Colors

Contents:

This chapter introduces concepts from Google's Material Design guidelines, a series of best practices for creating visually appealing and intuitive applications. You will learn how to add and use the CardView and Floating Action Button Views, efficiently use images, as well as employ design best practices to make your user's experience delightful.

What you should already KNOW

From the previous chapters you should be able to:

  • Create and run apps in Android Studio.
  • Create and edit elements using the Layout Editor, XML, and programmatically.
  • Use a RecyclerView to display a list.

What you will LEARN

  • Recommended use of material widgets (Floating Action Button, CardView).
  • How to efficiently use images in your app.
  • Recommended best practices for designing intuitive layouts using bold colors.

What you will DO

  • Modify an application to follow Material Design guidelines.
  • Add images and styling to a RecyclerView list.
  • Implement an ItemTouchHelper to add Drag and Drop functionality to your app.

App overview

The "Material Me!" app is a mock sports news application with very poor design implementation. You will fix it up to meet the design guidelines to create a delightful user experience! Below are some screenshots of the app before and after the Material Design improvements.

Preview for the starter Material Me! app Preview for the finished Material Me! app

Task 1: Download the starter code

The complete starter app project for this practical is available at MaterialMe-Starter. In this task you will load the project into Android Studio and explore some of the app's key features.

1.1 Open and run the Material Me project

  1. Download and unzip the MaterialMe-Starter file.
  2. Open the app in Android Studio.
  3. Build and run the app.

The app shows a list of sports names with some placeholder news text for each sport. The current layout and style of the app makes it nearly unusable: each row of data is not clearly separated and there is no imagery or color to engage the user.

1.2 Explore the app

  1. Before making modifications to the app, explore its current structure. It contains the following elements:

    • Sport.java

      This class represents the data model for each row of data in the RecyclerView. Right now it contains a field for the title of the sport and a field for some information about the sport.

    • SportsAdapter.java

      This is the adapter for the RecyclerView. It uses an ArrayList of Sport objects as its data and populates each row with this data.

    • MainActivity.java

      The MainActivity initializes the RecyclerView and adapter, and creates the data from resource files.

    • strings.xml

      This resource file contains all of the data for the application, including the titles and information for each sport.

    • list_item.xml

      This layout file defines each row of the RecyclerView. It consists of three TextViews, one for each piece of data (the title and the info for each sport) and one used as a label.

Task 2: Add a CardView and images

One of the fundamental principles of Material Design is the use of bold imagery to enhance the user experience. Adding images to the RecyclerView list items is a good start for creating a dynamic and captivating user experience.

When presenting information that has mixed media (like images and text), the Material Design guidelines recommend using a CardView, which is a FrameLayout with some extra features (such as elevation and rounded corners) that give it a consistent look and feel across many different applications and platforms. CardView is a UI component found in the Android Support Libraries.

In this section, you will move each list item into a CardView and add an Image to make the app comply with Material guidelines.

2.1 Add the CardView

CardView is not included in the default Android SDK, so you must be add it as a build.gradle dependency. Do the following:

  1. In your app level build.gradle file, add the following line to the dependencies block:

    compile 'com.android.support:cardview-v7:24.1.1'
    
    Note: The version of the support library may have changed since the writing of this practical. Update it to it's most current version and sync your gradle files.
  2. In the list_item.xml file, surround the root LinearLayout with a CardView with the following attributes:

    Attribute Value
    android:layout_width "match_parent"
    android:layout_height "wrap_content"
    android:layout_margin "8dp"

    Note: You will need to move the schema declaration (xmlns:android="http://schemas.android.com/apk/res/android") from the LinearLayout to the Cardview which is now the top level View of your layout file.
  3. Run the app. Now each row item is contained inside a CardView, which is elevated above the bottom layer and casts a shadow.

2.2 Download the images

The CardView is not intended to be used exclusively with plain text: it is best for displaying a mixture of content. You have is a good opportunity to make this sports information app more exciting by adding banner images to every row!

Using images is resource intensive for your app: the Android framework has to load the entire image into memory at full resolution, even if the app only displays a small thumbnail of the image.

In this section you will learn how to use the Glide library to load large images efficiently, without draining your resources or even crashing your app due to 'Out of Memory' exceptions.

  1. Download the banner images zip file.
  2. Copy these files into the res > drawable directory of your app.
    Note: Copy the files using your file explorer, not Android Studio. Navigate to the directory where all your Android Projects are stored (It's called /AndroidStudioProjects), and go to MaterialMe/app/src/main/res/drawable and paste the images here.
    You will need an array with the path to each image so that you can include it in the Sports java object. To do this:
  3. Define an array that contains all of the paths to the drawables as items in your string.xml file. Be sure to that they are in the same order as the sports titles array:
    <array name="sports_images">
       <item>@drawable/img_baseball</item>
       <item>@drawable/img_badminton</item>
       <item>@drawable/img_basketball</item>
       <item>@drawable/img_bowling</item>
       <item>@drawable/img_cycling</item>
       <item>@drawable/img_golf</item>
       <item>@drawable/img_running</item>
       <item>@drawable/img_soccer</item>
       <item>@drawable/img_swimming</item>
       <item>@drawable/img_tabletennis</item>
       <item>@drawable/img_tennis</item>
    </array>
    

2.3 Modify the Sport object

The Sport.java object will need to include the drawable resource that correpsonds to the sport. To achieve that:

  1. Add an integer member variable to the Sport object that will contain the drawable resource:
    private final int imageResource;
    
  2. Modify the constructor so that it takes an integer as a parameter and assigns it to the member variable:
    public Sport(String title, String info, int imageResource) {
       this.title = title;
       this.info = info;
       this.imageResource = imageResource;
    }
    
  3. Create a getter for the resource integer:
    public int getImageResource() {
       return imageResource;
    }
    

2.4 Fix the initializeData() method

In MainActivity.java, the initializeData() method is now broken, because the constructor for the Sport object demands the image resource as the third parameter.

A convenient data structure to use would be a TypedArray. A TypedArray allows you to store an array of other XML resources. By using a TypedArray, you will be able to obtain the image resources as well as the sports title and information by using indexing in the same loop.

  1. In the initializeData() method, get the TypedArray of resource id's by calling getResources().obtainTypedArray(), passing in the name of the array of drawable resources you defined in your strings.xml file:
    TypedArray sportsImageResources =
        getResources().obtainTypedArray(R.array.sports_images);
    
    You can access an element at index i in the TypedArray by using the appropriate "get" method, depending on the type of resource in the array. In this specific case, it contains resource ID's, so you use the getResourceId() method.
  2. Fix the code in the loop that creates the Sports objects, adding the appropriate drawable resource ID as the third parameter by calling getResourceId() on the TypedArray:
    for(int i=0;i<sportsList.length;i++){
       mSportsData.add(new Sport(sportsList[i],sportsInfo[i],    
           sportsImageResources.getResourceId(i,0)));
    }
    
  3. Clean up the data in the typed array once you have created the Sport data ArrayList:
    sportsImageResources.recycle();
    

2.5 Add an ImageView to the list items

  1. Change the LinearLayout inside the list_item.xml file to a RelativeLayout, and delete the orientation attribute.
  2. Add an ImageView with the following attributes:
    Attribute Value
    android:layout_width "match_parent"
    android:layout_height "wrap_content"
    android:id "@+id/sportsImage"
    android:adjustViewBounds "true"
    The adjustViewBounds attribute makes the ImageView adjust its boundaries to preserve the aspect ratio of the image.
  3. Add the following attributes to the existing TextViews:
    TextView id: title Attribute Value
    android:layout_alignBottom "@id/sportsImage"
    android:theme "@style/ThemeOverlay.AppCompat.Dark"
    TextView id: newsTitle Attribute Value
    android:layout_below "@id/sportsImage"
    android:textColor "?android:textColorSecondary"
    TextView id: subTitle android:layout_below "@id/newsTitle"
    Note: The question mark in the above textColor attribute ("?android:textColorSecondary") means that the framework will apply the value from the currently applied theme. In this case, this attribute is inherited from the "Theme.AppCompat.Light.DarkActionBar" theme, which defines it as a light gray color, often used for subheadings.

2.6 Load the images using Glide

After downloading the images and setting up the ImageView, the next step is to modify the SportsAdapter to load an image into the ImageView in onBindViewHolder(). If you take this approach, you will find that your app crashes due to "Out of Memory" errors. The Android framework has to load the image into memory each time at full resolution, no matter what the display size of the ImageView is.

There are a number of ways to reduce the memory consumption when loading images, but one of the easiest approaches is to use an Image Loading Library like Glide, which you will do in this step. Glide uses background processing, as well some other complex processing, to reduce the memory requirements of loading images. It also includes some useful features like showing placeholder images while the desired images are loaded.

Note: You can learn more about reducing memory consumption in your app in the Displaying Bitmaps guide.
  1. Add the following dependency for Glide, in your app level build.gradle file:
    compile 'com.github.bumptech.glide:glide:3.5.2'
    
  2. Add a variable in the SportsAdapter class, ViewHolder class for the ImageView, and initialize it in the ViewHolder constructor:
    mSportsImage = (ImageView) itemView.findViewById(R.id.sportsImage);
    
  3. Add the following line of code to onBindViewHolder() to get the image resource from the Sport object and load it into the ImageView using Glide:
    Glide.with(mContext).load(currentSport.getImageResource()).into(holder.mSportsImage);
    
    That's all takes to load an image with Glide. Glide also has several additional features that let you resize, transform and load images in a variety of ways. Head over to the Glide github page to learn more.
  4. Run the app, your list items should now have bold graphics that make the user experience dynamic and exciting!

Task 3: Make your CardView swipeable, movable, and clickable

When users see cards in an app, they have expectations about the way the cards behave. The Material Design guidelines say that:

  • A card can be dismissed, usually by swiping it away.
  • A list of cards can be reordered by holding down and dragging the cards.
  • Tapping on card will provide further details.

You will now implement these behaviors in your app.

3.1 Implement swipe to dismiss

The Android SDK includes a class called ItemTouchHelper that is used to define what happens to RecyclerView list items when the user performs various touch actions, such as swipe, or drag and drop. Some of the common use cases are already implemented in a set of methods in ItemTouchHelper.SimpleCallback.

ItemTouchHelper.SimpleCallback lets you define which directions are supported for swiping and moving list items, and implement the swiping and moving behavior.

Do the following:

  1. Create a new ItemTouchHelper object, in the onCreate() method of MainActivity.java. For its argument, create a new instance of ItemTouchHelper.SimpleCallback and press Enter to make Android Studio fill in the required methods: onMove() and onSwiped().

    Note: If the required methods were not automatically added, click on the red light bulb and select Implement methods.
  2. The SimpleCallback constructor will be underlined in red because you have not yet provided the required parameters: the direction that you plan to support for moving and swiping list items, respectively.

    Because we are only implementing swipe to dismiss at the moment, you should pass in 0 for the supported move directions and ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT for the supported swipe directions:

    ItemTouchHelper helper = new ItemTouchHelper(new ItemTouchHelper
        .SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {}
    
  3. You must now implement the desired behavior in onSwiped(). In this case, swiping the card left or right should delete it from the list. Call remove() on the data set, passing in the appropriate index by getting the position from the ViewHolder:
    mSportsData.remove(viewHolder.getAdapterPosition());
    
  4. To allow the RecyclerView to animate the deletion properly, you must also call notifyItemRemoved(), again passing in the appropriate index by getting the position from the ViewHolder:
    mAdapter.notifyItemRemoved(viewHolder.getAdapterPosition());
    
  5. After creating the new ItemTouchHelper object in the Main Activity's onCreate() method, call attachToRecyclerView() on the ItemTouchHelper instance to add it to your RecyclerView:
    helper.attachToRecyclerView(mRecyclerView);
    
  6. Run your app, you can now swipe list items left and right to delete them!

3.2 Implement drag and drop

You can also implement drag and drop functionality using the same SimpleCallback. The first argument of the SimpleCallback determines which directions the ItemTouchHelper supports for moving the objects around. Do the following:

  1. Change the first argument of the SimpleCallback from 0 to include every direction, since we want to be able to drag and drop anywhere:
    ItemTouchHelper helper = new ItemTouchHelper(new ItemTouchHelper
    .SimpleCallback(ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT |
    ItemTouchHelper.DOWN | ItemTouchHelper.UP, ItemTouchHelper.LEFT |
    ItemTouchHelper.RIGHT) {}
    
  2. In the onMove() method, get the original and target index from the 2nd and 3rd argument passed in (corresponding to the original and target viewholders).
    int from = viewHolder.getAdapterPosition();
    int to = target.getAdapterPosition();
    
  3. Swap the items in the dataset by calling Collections.swap() and pass in the dataset, and the initial and final indexes:
    Collections.swap(mSportsData, from, to);
    
  4. Notify the adapter that the item was moved, passing in the old and new indexes:
    mAdapter.notifyItemMoved(from, to);
    
  5. Run your app. You can now delete your list items by swiping them left or right, or reorder them using a long press to activate Drag and Drop mode.

3.3 Implement the detail view

According to Material Design guidelines, a card is used to provide an entry point to more detailed information. You may find yourself tapping on the cards to see more information about the sports, because that is how you expect cards to behave. In this section, you will add a detail activity that will be launched when any list item is pressed. For this practical, the detail activity will contain the name and image of the list item you clicked, but will contain only generic placeholder detail text, so you don't have to create custom detail for each list item.

  1. Create a new activity by going to File > New > Activity > Empty Activity.
  2. Call it DetailActivity, accept all of the defaults.
  3. In the newly created layout file:
    1. Change the root view group to RelativeLayout, as you've done in previous exercises.
    2. Remove the padding from the RelativeLayout element.
  4. Copy all of the TextView and ImageView views from the list_item.xml file to the activity_detail.xml file.
  5. Add the word "Detail" to every reference to an id, in order to differentiate it from list_item ids. For example, change the ImageView ID from sportsImage to sportsImageDetail, as well as any references to this ID for relative placement such layout_below.
  6. For the subTitleDetail textview, remove the placeholder text string and paste a paragraph of generic text to substitute detail text (For example, a few paragraphs of Lorem Ispum).
  7. Change the padding on the TextViews to 16dp.
  8. Wrap the entire activity_detail.xml in a ScrollView and change the layout_height attribute of the RelativeLayout to "wrap_content".

    Note: The attributes for the ScrollView might appear red at first. This is because you must first add an attribute that defines the Android namespace. This is the attribute that shows up in all of your layout files by default: xmlns:android="http://schemas.android.com/apk/res/android".
    Simply move this declaration to the top level view and the red should go away.
  9. In the SportsAdapter class, make the ViewHolder inner class implement View.OnClickListener, and implement the required method (onClick()).

  10. Set the OnClickListener to the itemview in the constructor:
    itemView.setOnClickListener(this);
    
  11. In the onClick() method, get the Sport object for the item that was clicked using getAdapterPosition():
    Sport currentSport = mSportsData.get(getAdapterPosition());
    
  12. Create an Intent that launches the Detail activity, and put the title and image resource as extras in the Intent:
    Intent detailIntent = new Intent(mContext, DetailActivity.class);
    detailIntent.putExtra("title", currentSport.getTitle());
    detailIntent.putExtra("image_resource", currentSport.getImageResource());
    
  13. Call startActivity() on the mContext variable, passing in the new Intent.
  14. In DetailActivity.java, initialize the ImageView and title TextView in onCreate():
    TextView sportsTitle = (TextView)findViewById(R.id.titleDetail);
    ImageView sportsImage = (ImageView)findViewById(R.id.sportsImageDetail);
    
  15. Get the title from the incoming Intent and set it to the TextView:
    sportsTitle.setText(getIntent().getStringExtra("title"));
    
  16. Use Glide to load the image into the ImageView:
    Glide.with(this).load(getIntent().getIntExtra("image_resource",0))
    .into(sportsImage);
    
  17. Run the app. Tapping on a list item now launches the detail activity.

Task 4: Add the FAB and choose a Material Design color palette

One of the principles behind Material Design is using consistent elements across applications and platforms so that users recognize patterns and know how to use them. You have already used one such element: the Floating Action Button. The FAB is a circular button that floats above the rest of the UI. It is used to promote a particular action to the user, one that is very likely to be used in a given activity. In this task, you will create a FAB that resets the dataset to it's original state.

4.1 Add the FAB

The Floating Action Button is part of the Design Support Library.

  1. Add the following line of code to the app level build.gradle file to add the design support library dependency:
    compile 'com.android.support:design:24.2.1'
    
  2. Use the vector asset studio to download an icon to use in the FAB. The button will reset the contents of the RecyclerView so this icon should do: Reset Icon. Change the name to ic_reset.
  3. In activity_main.xml, add a Floating Action Button view with the following parameters:
    Attribute Value
    android:layout_width "wrap_content"
    android:layout_height "wrap_content"
    android:layout_alignParentBottom "true"
    android:layout_alignParentRight "true
    android:layout_margin "16dp"
    android:src "@drawable/ic_reset"
    android:onClick resetSports
  4. Define the resetSports() method in MainActivity.java to simply call initializeData() to reset the data.
  5. Run the app. You can now reset the data by pushing the FAB.
    Note: Because the activity is destroyed and recreated when the configuration changes, rotating the device resets the data in this implementation. In order for the changes to be persistent (as in the case of reordering or removing data), you would have to implement onSaveInstanceState() or write the changes to a persistent source (like a database or SharedPreferences).

4.2 Choose a Material Design palette

If you run your app you may notice that the FAB has a color that you didn't define anywhere. Also, the App bar (the bar that contains the title of your app) has a color that you did not explicitly set. Where are these colors defined?

  1. Navigate to your styles.xml file (located in the values directory). The AppTheme style defines three colors by default: colorPrimary, colorPrimaryDark and colorAccent. These styles are defined by values from the colors.xml file. Material Design recommends picking at least these three colors for your app:
  2. A primary color. This one is automatically used to color your App bar.
  3. A primary dark color. A darker shade of the same color. This is used for the status bar above the app bar, among other things.
  4. An accent color. A color that contrasts well with the primary color. This is used for various highlights, but it is also the default color of the FAB. You can use the Material Color Guide to pick some colors to experiment with.
  5. Pick a color from the guide to use as your primary color. It should be within the 300-700 range so that you can still pick a proper accent and dark color. Modify the colorPrimary hex value in your colors.xml file to match the color you picked.
  6. Pick a darker shade of the same color to use as your primary dark color. Again, modify the colors.xml hex value for colorPrimaryDark to match.
  7. Pick an accent color for your FAB from the colors whose values start with an A, and whose color contrasts well with the primary color (like Orange A200). Change the colorAccent value in colors.xml to match.
  8. Add the android:tint attribute to the FAB and set it equal to #FFFFFF (white) in order to change the icon color to white.
  9. Run the app. The App Bar and FAB changed to reflect the new color palette!
    Note: If you want to change the color of the FAB to something other than theme colors, use the app:backgroundTint attribute. Note that this uses the app: namespace and Android Studio will prompt you to add a statement to define the namespace.

Solution code

Android Studio project: MaterialMe

Coding challenge

Note: All coding challenges are optional and are not prerequisites for later lessons.

Challenge 1: This challenge consists of several small improvements to your application:

  • Add real details to the Sport object and pass the details to the detail view.
  • Implement a way to ensure that the state of the app is persistent across orientation changes.

Challenge 2: Create an application with 4 images arranged in a grid in the center of your layout. Make the first three solid colored backgrounds with different shapes (square, circle, line), and the fourth the Android Material Design Icon. Make each of these images respond to clicks as follows:

  1. One of the colored blocks relaunches the Activity using the Explode animation for both the enter and exit transitions.
  2. Relaunch the Activity from another colored block, this time using the Fade transition.
  3. Touching the third colored block starts an in place animation of the view (such as a rotation).
  4. Finally, touching the android icon starts a secondary activity with a Shared Element Transition swapping the Android block with one of the other blocks. Preview for the Transition and Animation App
    Note: You must set your minimum SDK level to 21 or higher in order to implement shared element transitions.

Summary

  • A CardView is a good layout when presenting information that has mixed media (such as images and text)
  • CardView is a UI component found in the Android Support Libraries
  • CardView was not designed just for text child Views.
  • Loading images directly into ImageView is very memory intensive. All images are loaded at full-resolution.
  • Use an image loading library, such as Glide, to efficiently load images into your app
  • The Android SDK has a class called ItemTouchHelper which assists in obtaining tap, swipe or drag-and-drop information for your UI.
  • A Floating Action Button (FAB) focuses the user on a particular action and "floats" about your UI.
  • Material Design suggest 3 colors for your app: a primary color, a primary dark color and an accent color.
  • The Material Design guidelines are a set of guiding principles that aim to create consistent, intuitive and playful applications.
  • Material Design promotes the use of bold imagery and colors to enhance user experience.
  • It also promotes consistent elements across platforms (such as CardView and the FAB).
  • Material Design should be used for meaningful, intuitive motion like dismissable or rearrangeable cards.

The related concept documentation is in Android Developer Fundamentals: Concepts.

Learn more

results matching ""

    No results matching ""