We are the Dev Teams of
  • brands
  • ebay_main
  • ebay
  • mobile
<
>
BLOG

Creating a Cards UI on Android

by Yasin Yildirim
in Tutorials

Since Google introduced the Cards UI for the first time in the Google+ app, it started to become more and more popular in the Android community. Everybody liked it and started to use the same concept in their apps. It's not only used for the stylish look it has, but also for the functionality it provides. Each card has its own content and own action items which performs some actions on the content of it's own. In other words, each card has a content management of its own.

When we started to think about feature booking in the eBay Kleinanzeigen Android app, the biggest problem was where to place the entry point to that functionality. Finally we decided that the best way to do it in the user's ads list would be using the Cards UI. So that each Ad would have its own capability to be enhanced with features using the "Promote" action button.

In this article I will try to explain how to create a Cards UI with only using a normal ListView and Adapter logic.

 

Let’s start with the layout

The layout will contain a ListView inside a LinearLayout which has grey as background color. ListView will have a padding of 10 dip and transparent 10 dip dividers between list items. Additionally list items will have white background, so that they will be distinguishable and look like cards. Here’s the layout of the Fragment or Activity containing the ListView:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/light_grey" >

    <ListView
        android:id="@+id/cards_list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:clipToPadding="false"
        android:divider="@android:color/transparent"
        android:dividerHeight="10dp"
        android:padding="10dp"
        android:scrollbarStyle="outsideOverlay"
        tools:listitem="@layout/list_item_card" />

</LinearLayout>

One detail about ListView is actually so important, it should have the clipToPadding attribute set to false (which is set to true by default). Otherwise the ListView contents will be moving in the limits of padding excluded area, but we want to scroll all the area including padding. Following image explains the difference...

Another important point is the scrollbarStyle attribute, we set it to "outsideOverlay" so that it will not overlay the cards, it will appear at the edge of the ListView, ignoring the padding.

List item design is up to your content. In this simple example, I added a TextView and two action buttons in a LinearLayout with a white background.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/selectable_background"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/list_item_card_text"
        style="@style/ListItemText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <View
        android:id="@+id/list_item_seperator"
        android:layout_width="match_parent"
        android:layout_height="1dip"
        android:layout_marginLeft="5dip"
        android:layout_marginRight="5dip"
        android:background="@color/light_grey" />

    <LinearLayout
        style="?android:attr/buttonBarStyle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        <Button
            android:id="@+id/list_item_card_button_1"
            style="?android:attr/buttonBarButtonStyle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="5dip"
            android:layout_weight="1"
            android:focusable="false"
            android:focusableInTouchMode="false"
            android:text="@string/list_item_left_button"
            android:textSize="12sp"
            android:textStyle="normal" />

        <Button
            android:id="@+id/list_item_card_button_2"
            style="?android:attr/buttonBarButtonStyle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="5dip"
            android:layout_weight="1"
            android:focusable="false"
            android:focusableInTouchMode="false"
            android:text="@string/list_item_right_button"
            android:textSize="12sp"
            android:textStyle="normal" />
    </LinearLayout>

</LinearLayout>

Back to the code

The design of the code part will also be simple with a couple of important or tricky parts. I prefer to create the adapter first, in order to be ready to set it when the ListView is initialized. I just extend the BaseAdapter, inflate the list item layout in the getView method, initialize the TextView and action Buttons. Important part is how to set the onClickListeners for the Buttons. Because adapter works with a recycling logic, it’s very likely that if the onClickListener is set inside the adapter, next recycled item will also use the exact same onClickListener with a different data. In the adapter logic, each list item has the same elements with the same view id, therefore it’s not possible to know which list item’s button with that id is clicked.

In this point, it’s better to give the responsibility to the place where we set the adapter to the ListView. To do so, I accept a View.OnClickListener in my adapter’s constructor, save it as a field in the adapter class and set the onClickListener of the buttons as this very instance.

Here’s how the adapter’s constructor and getView methods look like:

public CardsAdapter(Context context, List<String> items, OnClickListener itemButtonClickListener) {
        this.context = context;
        this.items = items;
        this.itemButtonClickListener = itemButtonClickListener;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        ViewHolder holder;

        if (convertView == null) {
            convertView = LayoutInflater.from(context).inflate(R.layout.list_item_card, null);

            holder = new ViewHolder();
            holder.itemText = (TextView) convertView.findViewById(R.id.list_item_card_text);
            holder.itemButton1 = (Button) convertView.findViewById(R.id.list_item_card_button_1);
            holder.itemButton2 = (Button) convertView.findViewById(R.id.list_item_card_button_2);
            convertView.setTag(holder);

        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        holder.itemText.setText(items.get(position));

        if (itemButtonClickListener != null) {
            holder.itemButton1.setOnClickListener(itemButtonClickListener);
            holder.itemButton2.setOnClickListener(itemButtonClickListener);
        }

        return convertView;
    }

Now the tricky part

In the Fragment or Activity of your choice, we just inflate the layout including the ListView, initialize the ListView instance and set an adapter to it. Lastly we’re at the point that we need to give a View.OnclickListener to the adapter’s constructor, which has the responsibility of distinguishing between the buttons of the visible items of list. We don’t need to care about to non-visible list items because it’s impossible to click on them when they are out of our viewport.

In the overridden onClick method of the View.OnClickListener I simply iterate over the visible items using ListView’s getFirstVisiblePosition() and getLastVisiblePosition() methods and check whether the button which is clicked belongs to the current item being iterated at that moment.

private final class ListItemButtonClickListener implements OnClickListener {
        @Override
        public void onClick(View v) {
            for (int i = cardsList.getFirstVisiblePosition(); i <= cardsList.getLastVisiblePosition(); i++) {
                if (v == cardsList.getChildAt(i - cardsList.getFirstVisiblePosition()).findViewById(R.id.list_item_card_button_1)) {
                    // PERFORM AN ACTION WITH THE ITEM AT POSITION i
                    Toast.makeText(getActivity(), "Clicked on Left Button of List Item " + i, Toast.LENGTH_SHORT).show();
                } else if (v == cardsList.getChildAt(i - cardsList.getFirstVisiblePosition()).findViewById(R.id.list_item_card_button_2)) {
                    // PERFORM ANOTHER ACTION WITH THE ITEM AT POSITION i
                    Toast.makeText(getActivity(), "Clicked on Right Button of List Item " + i, Toast.LENGTH_SHORT).show();
                }
            }
        }
    }

The key point is while ListView.getFirstVisiblePosition() or ListView.getLastVisiblePosition() methods are returning the exact index inside the list, ListView.getChildAt method gives us the index inside the visible area. For example if the firstVisiblePosition is 3, getChildAt(3) would give us the 6th element of the list.

Additionally I added an OnItemClickListener to the ListView to do something else when clicked on the list item’s actual content (e.g. go to the detail page of that item).

The result

Finally we have a very basic and simple list view which looks and feels like the Cards UI. There may be more action items located in different areas of the list item, performing different actions on the data in that index, and ListView’s OnItemClick action can behave like a normal item click.

The complete code of this example can be found on GitHub

And also you can see the same implementation in-action, using the eBay Kleinanzeigen Android App: https://play.google.com/store/apps/details?id=com.ebay.kleinanzeigen

android, programming

?>