Saturday, February 1, 2014

Creating a "Card" UI

The newest version of most Android apps published by Google have started featuring "cards."  Many other popular apps are also following suit... After reading this tutorial you will be able to create cards as well.  It really is very simple.

What is a Card?

A card is nothing more than a layout or a view with a background drawable. And that drawable can be defined in XML as a layer drawable.

res/drawable/layer_card_background.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
   <item>
      <shape android:shape="rectangle">
         <solid android:color="#CABBBBBB"/>
         <corners android:radius="2dp" />
      </shape>
   </item>

   <item
      android:left="0dp"
      android:right="0dp"
      android:top="0dp"
      android:bottom="2dp">
      <shape android:shape="rectangle">
         <solid android:color="@android:color/white"/>
         <corners android:radius="2dp" />
      </shape>
   </item>
</layer-list>

The first item in the layer-list defines what will be the card's shadow.  The second item in the layer-list is the main content for the card.  You can turn any view or layout into a card by setting the background to the layer_card_background drawable.

res/layout/hello_card.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:gravity="center"
   android:background="#E0EEEE">
   <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_gravity="center"
      android:gravity="center"
      android:layout_margin="15dp"
      android:padding="15dp"
      android:background="@drawable/layer_card_background"
      android:text="Hello Card!\nThis is an example of a card..."/>
</LinearLayout>


Putting Cards in a List

At this point you probably realize that putting cards in a list view isn't all that difficult.  However, there are a few details that you won't want to overlook...

ListView Setup

In your listview's XML definition, you need the following attributes set:
  • android:divider="@null"
  • android:dividerHeight="10dp"
  • android:listSelector="@android:color/transparent" 
  • android:cacheColorHint="@android:color/transparent"
  • android:headerDividersEnabled="true"
  • android:footerDividersEnabled="true"

The first attribute tells the listview that you don't want a view used for the listview's divider.  The second attribute tells the listview the height of the divider.  Since we have specified @null for the divider this will be the space between each card in the list. Setting the list selector color to transparent allows us to define our own pressed state behavior for the card.  Setting the cache color hint to transparent is a good thing to do if you run into weird behaviors while scrolling.   The last two attributes will allow for margin values at the top and bottom of the list, but it will require a few changes to the listview in code as well.  After inflating the list view, and before calling setAdapter(), add empty header and footer views, like this:

m_list.addHeaderView(new View(this));
m_list.addFooterView(new View(this));

Card Item Selector Setup

If you don't want to have a pressed state for the cards in the list then you can skip this step.  Otherwise, create a new drawable file called layer_card_background_pressed.xml.  It should be a duplicate of layer_card_background.xml but the main color defined in the second item should be changed to a different color for the pressed state.

res/drawable/layer_card_background_selected.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
   <item>
      <shape android:shape="rectangle">
         <solid android:color="#CABBBBBB"/>
         <corners android:radius="2dp" />
      </shape>
   </item>

   <item
      android:left="0dp"
      android:right="0dp"
      android:top="0dp"
      android:bottom="2dp">
      <shape android:shape="rectangle">
         <solid android:color="#CCCCCC"/>
         <corners android:radius="2dp" />
      </shape>
   </item>
</layer-list>

Next you need to create a selector resource that will be used as the background for the card items.

res/drawable/selector_card_background.xml
<?xml version="1.0" encoding="utf-8"?>

<selector xmlns:android="http://schemas.android.com/apk/res/android">
   <item
      android:state_pressed="true"
      android:drawable="@drawable/layer_card_background_selected" />

   <item android:drawable="@drawable/layer_card_background" />
</selector>

Card Item Layout Setup

ListView items will take up the entire width of the ListView.  In general this isn't a problem but when creating a list of cards this just won't work. To get around this, you need to wrap the item with the card background in another layout that has padding values set.  Here is an example card item layout that does this:

res/layout/list_item_card.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:paddingLeft="15dp"
   android:paddingRight="15dp"
   android:descendantFocusability="beforeDescendants">

   <LinearLayout
      android:orientation="vertical"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:paddingLeft="15dp"
      android:paddingTop="15dp"
      android:paddingBottom="15dp"
      android:paddingRight="15dp"
      android:background="@drawable/selector_card_background"
      android:descendantFocusability="afterDescendants">

      <TextView
         android:id="@+id/text1"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"/>

      <TextView
         android:id="@+id/text2"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"/>

      <TextView
         android:id="@+id/text3"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"/>
   </LinearLayout>
</FrameLayout>

Inflating the Items

Now all you have to do is get your adapter to inflate the items with the above layout. You can modify anything inside the LinearLayout... You can even change the LinearLayout to a different kind of layout if you need to.  Just don't change any of the attributes on the FrameLayout other than the padding or item clicks may not work right.

For information on a really cool way to prevent writing lots of adapters check our my last post.


If you want the entire source used for this post you can download this IntelliJ Project.