Tip Android #27: ListView con secciones o cabeceras

22/08/2013
android tip

Vuelve el ListView a los tip Android y en esta ocasión veremos cómo dividir sus filas en secciones para agrupar su contenido. Lo que haremos es en realidad muy sencillo, simplemente usaremos dos layout distintos para las filas de un mismo ListView y seleccionaremos para cada tipo de datos a mostrar el que corresponda. StickyListHeaders proporciona una solución más sofisticada que la que veremos aquí cuyo resultado final lucirá así:

listview with headers

Por lo tanto, nuestra app sólo contendrá una Activity con el siguiente layout:

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" >

    <ListView
        android:id="@android:id/list"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/background_light"
        android:divider="@android:color/background_dark"
        android:dividerHeight="1dp"
        android:cacheColorHint="@android:color/transparent">
    </ListView>

</merge>

Como se ha comentado, en el ListView tendremos dos tipos de filas que he llamado Header y Content. Las clases de ejemplo que modelan estos son datos son las siguientes.

package com.danielme.tipsandroid.listviewheaders.model;

/**
 * 
 * @author danielme.com
 *
 */
public class Header
{
	private String title;

	public String getTitle()
	{
		return title;
	}

	public void setTitle(String title)
	{
		this.title = title;
	}

}

package com.danielme.tipsandroid.listviewheaders.model;

/**
 * 
 * @author danielme.com
 * 
 */
public class Content
{

	private String text1;

	private String text2;

	public String getText1()
	{
		return text1;
	}

	public void setText1(String text1)
	{
		this.text1 = text1;
	}

	public String getText2()
	{
		return text2;
	}

	public void setText2(String text2)
	{
		this.text2 = text2;
	}

}

Los layout para cada una son listview_header.xml y listview_content.xml respectivamente.

<?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:background="@drawable/listselector_header"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/textViewHeader"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/test"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:textColor="@android:color/white" />

</LinearLayout>
<?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:orientation="vertical" 
    android:paddingLeft="5dp"
    android:background="@drawable/listselector_content">

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/test"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:textColor="@android:color/black" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/test"
        android:textAppearance="?android:attr/textAppearanceSmall"
        android:textColor="@android:color/black" />

</LinearLayout>

Los drawables se pueden consultar en el código de ejemplo, no son relevantes para este artículo.

La Activity se limitará a mostrar el ListView generando unos datos de prueba consistentes en una lista que contendrá de forma organizada los Header y Content a mostrar. También gestionará el evento de pulsación en una fila, actuándose de forma diferente si el elemento pulsado es una sección o un contenido de una de ellas. En el caso de una sección, la mostrará como primer elemento del ListView.

package com.danielme.tipsandroid.listviewheaders;

import java.util.ArrayList;
import java.util.List;
import android.annotation.SuppressLint;
import android.app.ListActivity;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.Toast;
import com.danielme.tipsandroid.listviewheaders.model.Content;
import com.danielme.tipsandroid.listviewheaders.model.Header;
import com.danieme.tipsandroid.listviewheaders.R;

/**
 * 
 * @author danielme.com
 * 
 */
public class MainActivity extends ListActivity
{

	private List<Object> dataset = getContent();

	@Override
	protected void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		setListAdapter(new CustomArrayAdapter(this, dataset));

		getListView().setOnItemClickListener(new OnItemClickListener() {

			@SuppressLint("NewApi")
			@Override
			public void onItemClick(AdapterView<?> parent, View view, int position, long id)
			{
				Object item = dataset.get(position);
				if (item instanceof Header)
				{
					Toast.makeText(MainActivity.this, ((Header) item).getTitle(), Toast.LENGTH_SHORT).show();
					// back to header, see
					// https://danielme.com/tip-android-17-listview-back-to-top-volver-al-inicio/
					if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB)
					{
						getListView().setSelection(position);
					}
					else
					{
						getListView().smoothScrollToPositionFromTop(position, 0, 300);
					}
				}
				else
				{
					Toast.makeText(MainActivity.this, ((Content) item).getText1(), Toast.LENGTH_SHORT).show();
				}

			}
		});
	}

	private List<Object> getContent()
	{
		List<Object> list = new ArrayList<Object>(60);
		Content content = null;
		Header header = null;
		int j = 1;

		for (int i = 0; i < 50; i++)
		{
			// set a new header or section every five rows
			if (i % 5 == 0)
			{
				header = new Header();
				header.setTitle("Header number " + j);
				j++;
				list.add(header);
			}

			content = new Content();
			content.setText1("Text 1-" + (i + 1));
			content.setText2("Text 2-" + (i + 1));
			list.add(content);
		}

		return list;

	}
}

La clave de todo está en el Adapter que implementa la idea comentada al principio y que si el elemento a mostrar es un Header lo hará en un listview_header, usándose en caso contrario un listview_content. Para simplificar un poco el código sólo he usado el patrón ViewHolder Pattern para el contenido que en un ejemplo real cabría esperar que fuera la mayor parte de los datos a mostrar.

package com.danielme.tipsandroid.listviewheaders;

import java.util.List;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

import com.danielme.tipsandroid.listviewheaders.model.Content;
import com.danielme.tipsandroid.listviewheaders.model.Header;
import com.danieme.tipsandroid.listviewheaders.R;

/**
 * Custom adapter with "View Holder Pattern".
 * 
 * @author danielme.com
 * 
 */
public class CustomArrayAdapter extends ArrayAdapter<Object>
{

	private LayoutInflater layoutInflater;
	

	public CustomArrayAdapter(Context context, List<Object> objects)
	{
		super(context, 0, objects);
		layoutInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent)
	{
		//header
		if(getItem(position) instanceof Header)
		{
                     if (convertView == null || convertView.findViewById(R.id.textViewHeader)==null)
			{
				convertView = layoutInflater.inflate(R.layout.listview_header, null);
			}
			TextView textView = (TextView) convertView.findViewById(R.id.textViewHeader);
			Header header = (Header) getItem(position);
			textView.setText(header.getTitle());
		}
		else //ViewHolder Pattern for content
		{		
			Holder holder = null;
		    //check if this view contained a header
			if (convertView == null || convertView.findViewById(R.id.textViewHeader) != null)
			{
				holder = new Holder();
	
				convertView = layoutInflater.inflate(R.layout.listview_content, null);
				holder.setTextView1((TextView) convertView.findViewById(R.id.textView1));
				holder.setTextView2((TextView) convertView.findViewById(R.id.textView2));
				convertView.setTag(holder);
			}
			else
			{
				holder = (Holder) convertView.getTag();
			}
			Content content = (Content) getItem(position);
			holder.getTextView1().setText(content.getText1());
			holder.getTextView2().setText(content.getText2());
		}
		return convertView;
	}

}

class Holder
{

	private TextView textView1;

	private TextView textView2;

	public TextView getTextView1()
	{
		return textView1;
	}

	public void setTextView1(TextView textView)
	{
		this.textView1 = textView;
	}

	public TextView getTextView2()
	{
		return textView2;
	}

	public void setTextView2(TextView textView)
	{
		this.textView2 = textView;
	}

}

El resultado final en Jelly Bean 4.1

El proyecto completo se encuentra en Github. Para más información sobre cómo utilizar GitHub, consultar este artículo.

<< TIPS ANDROID

4 comentarios sobre “Tip Android #27: ListView con secciones o cabeceras

  1. Gracias por el código, Daniel, era justo lo que necesitaba, llevaba un par de días navegando sin encontrar la solución, con tu solución me ha funcionado.

Deja un comentario

Este sitio utiliza Akismet para reducir el spam. Conoce cómo se procesan los datos de tus comentarios.