Android: How to make a dashboard using a GridView

When your Android application provides many functionalities via different Activities, it is a good UI Design to provide a dashboard for an easy access and navigation. This pattern can be found in major Android applications such as Google+, Twitter, Evernote.

In our use case scenario, you are developing an application which provides maps for the main Paris transportation services (i.e. Metro, RER, Bus, Noctilien).

The main entry to your application will be a dashboard containing icons to access the transportation maps.

The dashboard Activity will be using a GridView of 2 columns in portrait mode and 4 columns in landscape mode.

Here is the code for the dashboard layout:

Dashboard layout: res/layout/dashboard.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="fill_parent"
   android:layout_height="fill_parent"
   android:background="@android:color/white">
 
    <GridView
       android:id="@+id/dashboard_grid"
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:listSelector="@android:color/transparent"
       android:stretchMode="columnWidth"
       android:verticalSpacing="20.0dip"
       android:layout_centerInParent="true"
       style="@style/dashboard" />
 
</RelativeLayout>

The above layout uses a reference to an external style which is going to provide a different UI depending on the orientation (portrait or landscape).

Dashboard portrait mode style: res/values/styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="dashboard">
        <item name="android:numColumns">2</item>
    </style>
</resources>

Dashboard landscape mode style: res/values-land/styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="dashboard">
        <item name="android:numColumns">4</item>
    </style>
</resources>

The GridView will be filled programatically using an inflater for the dashboard entries (icon + text)

Dashboard entry layout: res/layout/dashboard_icon.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="fill_parent"
   android:layout_height="wrap_content"
   android:layout_marginBottom="20.0dip"
   android:gravity="center"
   android:orientation="vertical">
 
    <ImageView
       android:id="@+id/dashboard_icon_img"
       android:layout_width="fill_parent"
       android:layout_height="96.0dip"
       android:scaleType="fitCenter" />
 
    <TextView
       android:id="@+id/dashboard_icon_text"
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:layout_marginBottom="20.0dip"
       android:layout_marginTop="2.0dip"
       android:gravity="center"
       android:textAppearance="?android:textAppearanceMedium"
       android:textColor="@android:color/black" />
 
</LinearLayout>

The dashboard Activity where we fill the GridView :

Dashboard Activity: src/org/agafix/metro/DashboardActivity.java

package org.agafix.metro;
 
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;
 
public class DashboardActivity extends Activity implements OnItemClickListener {
 
    static final String EXTRA_MAP = "map";
 
    static final LauncherIcon[] ICONS = {
            new LauncherIcon(R.drawable.ic_metro, "Metro", "metro.png"),
            new LauncherIcon(R.drawable.ic_rer, "RER", "rer.png"),
            new LauncherIcon(R.drawable.ic_bus, "Bus", "bus.png"),
            new LauncherIcon(R.drawable.ic_noctilien, "Noctilien", "noctilien.png"),
    };
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.dashboard);
 
        GridView gridview = (GridView) findViewById(R.id.dashboard_grid);
        gridview.setAdapter(new ImageAdapter(this));
        gridview.setOnItemClickListener(this);
 
        // Hack to disable GridView scrolling
        gridview.setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return event.getAction() == MotionEvent.ACTION_MOVE;
            }
        });
    }
 
    @Override
    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
        Intent intent = new Intent(this, MapActivity.class);
        intent.putExtra(EXTRA_MAP, ICONS[position].map);
        startActivity(intent);
    }
 
    static class LauncherIcon {
        final String text;
        final int imgId;
        final String map;
 
        public LauncherIcon(int imgId, String text, String map) {
            super();
            this.imgId = imgId;
            this.text = text;
            this.map = map;
        }
 
    }
 
    static class ImageAdapter extends BaseAdapter {
        private Context mContext;
 
        public ImageAdapter(Context c) {
            mContext = c;
        }
 
        @Override
        public int getCount() {
            return ICONS.length;
        }
 
        @Override
        public LauncherIcon getItem(int position) {
            return null;
        }
 
        @Override
        public long getItemId(int position) {
            return 0;
        }
 
        static class ViewHolder {
            public ImageView icon;
            public TextView text;
        }
 
        // Create a new ImageView for each item referenced by the Adapter
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            View v = convertView;
            ViewHolder holder;
            if (v == null) {
                LayoutInflater vi = (LayoutInflater) mContext.getSystemService(
                    Context.LAYOUT_INFLATER_SERVICE);
 
                v = vi.inflate(R.layout.dashboard_icon, null);
                holder = new ViewHolder();
                holder.text = (TextView) v.findViewById(R.id.dashboard_icon_text);
                holder.icon = (ImageView) v.findViewById(R.id.dashboard_icon_img);
                v.setTag(holder);
            } else {
                holder = (ViewHolder) v.getTag();
            }
 
            holder.icon.setImageResource(ICONS[position].imgId);
            holder.text.setText(ICONS[position].text);
 
            return v;
        }
    }
}

For the sake of completeness, the map will be displayed using an activity using a WebView.

Map layout: res/layout/map.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="fill_parent"
   android:layout_height="fill_parent"
   android:orientation="vertical">
 
    <WebView
       android:id="@+id/webview"
       android:layout_width="fill_parent"
       android:layout_height="fill_parent" />
 
</LinearLayout>

Map Activity: src/org/agafix/metro/MapActivity.java

package org.agafix.metro;
 
import android.app.Activity;
import android.content.res.Configuration;
import android.os.Bundle;
import android.webkit.WebSettings;
import android.webkit.WebView;
 
public class MapActivity extends Activity {
 
    @Override
    public void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.map);
        WebView webview = (WebView) findViewById(R.id.webview);
 
        WebSettings settings = webview.getSettings();
        webview.setScrollBarStyle(WebView.SCROLLBARS_OUTSIDE_OVERLAY);
        webview.setScrollbarFadingEnabled(true);
 
        settings.setJavaScriptEnabled(true);
        settings.setLoadWithOverviewMode(true);
        settings.setUseWideViewPort(true);
        settings.setBuiltInZoomControls(true);
 
        final String dim;
        if (getResources().getConfiguration().orientation
                == Configuration.ORIENTATION_PORTRAIT) {
            dim = "height";
        } else {
            dim = "width";
        }
 
        final String map = getIntent().getStringExtra(DashboardActivity.EXTRA_MAP);
        final String imgSrc = "<img src=\"" + map + "\" " + dim + "=\"100%\">";
        webview.loadDataWithBaseURL("file:///android_asset/", imgSrc,
                "text/html", "utf-8", null);
    }
 
}

Here are the results:

Portrait mode

Landscape mode