Diseño Android: Spinner

VERSIONES

  1. 25/04/2013 (Primera publicación)
  2. 19/09/2015:
    • Actualización a Marshmallow
    • Corrección en layout

android

El widget Spinner de Android muestra una lista desplegable para seleccionar un único elemento y es equivalente al típico select de html o los ComboBox de otros entornos de desarrollo. En este tutorial vamos a ver cómo utilizar y personalizar tanto el Spinner como la lista de opciones desplegables. Con los estilos, tenemos el mismo problema que ya hemos visto en algunos tips: se aplica el estilo del fabricante/versión de Android y este puede resultar antiestético con el diseño general de la aplicación.

Spinner básico

Al igual que un ListView, el contenido de la lista desplegable con los elementos del Spinner proviene de un Adapter (en el ejemplo de la documentación es un array definido en un fichero xml). A continuación se muestra un ejemplo básico de Spinner con su layout y la Activity asociada la cual mostrará un Toast si se selecciona un elemento. Esta selección no es obligatoria, ya que pulsando “back” la lista se cierra.

<?xml version="1.0" encoding="utf-8"?>

<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >

    <Spinner
        android:id="@+id/spinner"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />

</merge>

Nota: En Android 2 la lista desplegada se puede mostrar en forma de cuadro de diálogo si establecemos la propiedad “android:prompt” con su título obteniéndose el siguiente resultado:

cuadro dialogo

package com.danielme.tipsandroid.spinner;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.Toast;

import com.danieme.tipsandroid.spinner.R;

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

	@Override
	protected void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);	
		
		Spinner spinner = (Spinner) findViewById(R.id.spinner);
		String[] valores = {"uno","dos","tres","cuatro","cinco","seis", "siete", "ocho"};
		spinner.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, valores));
		spinner.setOnItemSelectedListener(new OnItemSelectedListener() {

			@Override
			public void onItemSelected(AdapterView<?> adapterView, View view, int position, long id)
			{
				Toast.makeText(adapterView.getContext(), (String) adapterView.getItemAtPosition(position), Toast.LENGTH_SHORT).show();
			}

			@Override
			public void onNothingSelected(AdapterView<?> parent)
			{
				// vacio
				
			}
		});

	}
}

La siguiente imagen muestra el resultado de este Spinner en Android 2.1 y Jelly Bean

spinner comparación

Tal y como se puede apreciar, los estilos aplicados por defecto, en este caso en el emulador, no tienen nada que ver en ambos casos. Afortunadamente podemos personalizar los Spinner para que su estilo no sólo se adapte al de nuestra app sino para que también sea siempre el mismo independientemente del fabricante y versión de Android.

Personalizar el botón del Spinner

La personalización de la imagen que muestra el elemento seleccionado y cuya pulsación despliega la lista la haremos a través de un drawabler selector, y para cada caso usaremos una imagen de tipo 9-patch.

<?xml version="1.0" encoding="utf-8"?>

<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >

    <Spinner
        android:id="@+id/spinner"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" 
        android:background="@drawable/spinner_selector"/>

</merge>

/drawable/spinner_selector

<?xml version="1.0" encoding="utf-8"?>

 <selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:drawable="@drawable/btn_dropdown_normal" android:state_enabled="true" android:state_window_focused="false"/>
    <item android:drawable="@drawable/btn_dropdown_normal" android:state_enabled="false" android:state_window_focused="false"/>
    <item android:drawable="@drawable/btn_dropdown_pressed" android:state_pressed="true"/>
    <item android:drawable="@drawable/btn_dropdown_selected" android:state_enabled="true" android:state_focused="true"/>
    <item android:drawable="@drawable/btn_dropdown_normal" android:state_enabled="true"/>
    <item android:drawable="@drawable/btn_dropdown_selected" android:state_focused="true"/>
    <item android:drawable="@drawable/btn_dropdown_normal"/>

</selector>

Las imágenes utilizadas en el ejemplo son las siguientes:

btn_dropdown_disabled.9.png btn_dropdown_disabled.9
btn_dropdown_normal.9.png btn_dropdown_normal.9
btn_dropdown_pressed.9.png btn_dropdown_pressed.9
btn_dropdown_selected.9.png btn_dropdown_selected.9

De esta forma conseguimos unificar la imagen del Spinner en todos los dispositivos. En lo que respecta a la lista desplegable, se le aplicará el estilo por defecto que se haya definido para los ListView. Asimismo, el contenido no tiene por qué ser un texto (en nuestro caso android.R.layout.simple_spinner_item) sino que puede ser cualquier layout (recordad que el Spinner toma los datos de un Adapter como si fuera un ListView o GridView). Lo veremos en la siguiente sección.

Personalizar la lista desplegable

Ya hemos visto el uso básico del Spinner así como la configuración de la imagen del selector. Ahora vamos a ver lo más complejo: cómo personalizar su contenido, esto es, la lista desplegable de la que podemos seleccionar un elemento. Si simplemente queremos modificar el estilo texto, basta con crear un layout con simplemente un TextView con las características que queramos y utilizarlo en el ArrayAdapter del Spinner. Por ejemplo, consideremos este fichero (/res/layout/textview_spinner.xml):

<?xml version="1.0" encoding="utf-8"?>

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/text1"
    android:textColor="#de0000"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"/>

Usamos este layout en el ArrayAdapter anterior

spinner.setAdapter(new ArrayAdapter<String>(this, R.layout.textview_spinner, valores));

Obtendremos lo siguiente:

spinner red

Una lista compleja con su propio Adapter

Veamos ahora un ejemplo más complejo y realista en el que usaremos una lista de elementos que consistirán en una red social y su icono. Para ello, ampliaremos el proyecto de ejemplo. Los iconos provienen del Social Media Icons Pack de Blog Perfume
y el pack completo se puede descargar aquí. Los iconos dentro del proyecto quedan tal que así:

iconos

Lo que tenemos que tener muy presente es que hay que trabajar con dos elementos gráficos: por un lado el valor seleccionado que se muestra encima del botón del Spinner y por otro los elementos de la lista desplegable. En función de lo que queramos, necesitaremos un layout común a ambos elementos o, el caso más habitual, un layout para cada uno. Vayamos en primer lugar con el layout que usaremos para el elemento seleccionado; es fundamental que tenga un fondo transparente para que no oculte parte del botón del Spinner (suponiendo que sea esto lo que queremos).

/res/layout/spinner_selected_item.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="wrap_content"
    android:background="@android:color/transparent"
    android:padding="1dp">

    <ImageView
        android:id="@+id/icono"
        android:layout_width="36dp"
        android:layout_height="36dp"
        android:contentDescription="@string/icono"/>

    <TextView
        android:id="@+id/texto"
        android:gravity="center_vertical"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_toRightOf="@+id/icono"
        android:layout_alignBottom="@+id/icono"
        android:layout_marginLeft="10dp"
        android:singleLine="true"
        android:textColor="@android:color/black" 
        android:textSize="17sp"/>

</RelativeLayout>

El objetivo es modelar esto:

spinner-icon

Para los elementos de la lista usaremos un layout muy parecido pero con la imagen y el texto de menor tamaño. Asimismo, en lugar de ser transparente este layout tendrá un drawable de tipo selector que establecerá el fondo de color blanco para los disitnos estados que puede tener el elemento. Veamos primero el layout (/res/layout/spinner_list_item.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:background="@drawable/spinner_item_selector"
    android:orientation="horizontal"
    android:padding="3dp" >

    <ImageView
        android:id="@+id/icono"
        android:layout_width="32dp"
        android:layout_height="32dp"
        android:contentDescription="@string/icon" />

    <TextView
        android:id="@+id/texto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginLeft="6dp"
        android:gravity="center_vertical"
        android:singleLine="true"
        android:textColor="@android:color/black"
        android:textSize="15sp" />

</LinearLayout>

El drawable /res/drawable/spinner_item_selector.xml es muy simple.

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true" >
        <shape>
            <solid
                android:color="#5bc0e5" />
           
        </shape>
    </item>
    <item>
        <shape>
           <solid
                android:color="@android:color/white" />           
        </shape>
    </item>
</selector>

Ya tenemos los diseños que usaremos, y ahora toca volver a la Activity para aplicarlos. Tal y como ya se ha comentado, tendremos que implementar un Adapter. Veamos primero la Activity, los únicos cambios que ha sufrido han sido la creación de la lista y del Adapter personalizado.

package com.danielme.android.spinnerdemo;

import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.Spinner;
import android.widget.Toast;

import com.danieme.android.spinnerdemo.R;

/**
 * 
 * @author danielme.com
 *
 */
public class MainActivity extends Activity
{
	
	private Spinner spinner;

	@Override
	protected void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);	
		
		//datos a mostrar
		List<SocialNetwork> items = new ArrayList<SocialNetwork>(15);
		items.add(new SocialNetwork(getString(R.string.ninguna), R.drawable.ninguno));
		items.add(new SocialNetwork(getString(R.string.blogger), R.drawable.blogger));
		items.add(new SocialNetwork(getString(R.string.delicious), R.drawable.delicious));
		items.add(new SocialNetwork(getString(R.string.digg), R.drawable.digg));
		items.add(new SocialNetwork(getString(R.string.ebay), R.drawable.ebay));
		items.add(new SocialNetwork(getString(R.string.facebook), R.drawable.facebook));
		items.add(new SocialNetwork(getString(R.string.flickr), R.drawable.flickr));
		items.add(new SocialNetwork(getString(R.string.google), R.drawable.google));
		items.add(new SocialNetwork(getString(R.string.lastfm), R.drawable.lastfm));
		items.add(new SocialNetwork(getString(R.string.reddit), R.drawable.reddit));
		items.add(new SocialNetwork(getString(R.string.stumbleupon), R.drawable.stumbleupon));
		items.add(new SocialNetwork(getString(R.string.technorati), R.drawable.technorati));
		items.add(new SocialNetwork(getString(R.string.twitter), R.drawable.twitter));
		items.add(new SocialNetwork(getString(R.string.wordpress), R.drawable.wordpress));
		items.add(new SocialNetwork(getString(R.string.youtube), R.drawable.youtube));

		spinner = (Spinner) findViewById(R.id.spinner);
		spinner.setAdapter(new SocialNetworkSpinnerAdapter(this,items));
		spinner.setOnItemSelectedListener(new OnItemSelectedListener() 
		{
			@Override
			public void onItemSelected(AdapterView<?> adapterView, View view, int position, long id)
			{
  			  Toast.makeText(adapterView.getContext(), ((SocialNetwork) adapterView.getItemAtPosition(position)).getNombre(), Toast.LENGTH_SHORT).show();
			}

			@Override
			public void onNothingSelected(AdapterView<?> adapterView)
			{
				//nothing				
			}
		});

	}
}

La clase SocialNetwork es un simple bean:

package com.danielme.android.spinnerdemo;

/**
 * Elemento de la lista del spinner.
 * @author danielme.com
 *
 */
public class SocialNetwork 
{
	private String name;
	
	private int icon;	

	public SocialNetwork(String nombre, int icono) 
	{
		super();
		this.name = nombre;
		this.icon = icono;
	}

	public String getNombre() 
	{
		return name;
	}

	public void setNombre(String nombre) 
	{
		this.name = nombre;
	}

	public int getIcono() 
	{
		return icon;
	}

	public void setIcono(int icono) 
	{
		this.icon = icono;
	}	

}

Para el Adapter, actuaremos de modo similar a cuando implementamos un ListView y heredamos de ArrayAdapter. Sin embargo, en esta ocasión tendremos que sobreescribir dos métodos con un propósito distinto:

  • getView: se usará sólo para establecer el elemento seleccionado que se muestra sobre el botón del Spinner. En los ListView, es aquí donde “rellenamos” la lista.
  • getDropDownView: es aquí donde se rellenan las filas de la lista deplegable, lo que en el caso de los ListView hacemos en el método getView. Por eficiencia, recurriremos al ViewHolderPattern. El Adapter comentado es el siguiente:
  • package com.danielme.android.spinnerdemo;
    
    import java.util.List;
    
    import com.danieme.android.spinnerdemo.R;
    
    import android.content.Context;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.ArrayAdapter;
    import android.widget.ImageView;
    import android.widget.TextView;
    
    
    public class SocialNetworkSpinnerAdapter extends ArrayAdapter<SocialNetwork> 
    {
    	private Context context;
    
    	List<SocialNetwork> datos = null;
    
    	public SocialNetworkSpinnerAdapter(Context context, List<SocialNetwork> datos) 
    	{
    		//se debe indicar el layout para el item que seleccionado (el que se muestra sobre el botón del botón)
    		super(context, R.layout.spinner_selected_item, datos);
    		this.context = context;
    		this.datos = datos;
    	}
    
    	//este método establece el elemento seleccionado sobre el botón del spinner
    	@Override
    	 public View getView(int position, View convertView, ViewGroup parent) 
    	 {
    	    if (convertView == null) 
    	    {
    	         convertView = ((LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(R.layout.spinner_selected_item,null);
    	    }	        
    	    ((TextView) convertView.findViewById(R.id.texto)).setText(datos.get(position).getNombre());
    	    ((ImageView) convertView.findViewById(R.id.icono)).setBackgroundResource(datos.get(position).getIcono());	   
    	    
    	    return convertView;
    	 }
    
    	//gestiona la lista usando el View Holder Pattern. Equivale a la típica implementación del getView
    	//de un Adapter de un ListView ordinario
    	@Override
    	public View getDropDownView(int position, View convertView, ViewGroup parent) 
    	{
    		View row = convertView;
    		if (row == null) 
    		{
    			LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    			row = layoutInflater.inflate(R.layout.spinner_list_item, parent, false);
    		}
    
    		if (row.getTag() == null) 
    		{
    			SocialNetworkHolder redSocialHolder = new SocialNetworkHolder();
    			redSocialHolder.setIcono((ImageView) row.findViewById(R.id.icono));
    			redSocialHolder.setTextView((TextView) row.findViewById(R.id.texto));
    			row.setTag(redSocialHolder);
    		}
    
    		//rellenamos el layout con los datos de la fila que se está procesando
    		SocialNetwork redSocial = datos.get(position);		
    		((SocialNetworkHolder) row.getTag()).getIcono().setImageResource(redSocial.getIcono());		
    		((SocialNetworkHolder) row.getTag()).getTextView().setText(redSocial.getNombre());
    
    		return row;
    	}
    	
    	/**
    	 * Holder para el Adapter del Spinner
    	 * @author danielme.com
    	 *
    	 */
    	private static class SocialNetworkHolder
    	{
    
    		private ImageView icono;
    
    		private TextView textView;
    
    		public ImageView getIcono()
    		{
    			return icono;
    		}
    
    		public void setIcono(ImageView icono) 
    		{
    			this.icono = icono;
    		}
    
    		public TextView getTextView() 
    		{
    			return textView;
    		}
    
    		public void setTextView(TextView textView) 
    		{
    			this.textView = textView;
    		}
    
    	}
    }
    

    Hemos tenido que escribir algo de código pero ya tenemos nuestro Spinner completamente personalizado y con un estilo propio de la app e independiente de la plataforma y, lo más importante, cada vez que tengamos que trabajar con este widget ya tenemos un ejemplo usable como plantilla. El resultado final en Android 2.3.3:


    Novedades en Android 5.0

    Si ejecutamos el proyecto de ejemplo en Android 5.0 Lollipop (API 21) o superior veremos que el listado de opciones se muestra sobre el Spinner y no debajo.

    spinner overlapanchor

    Se puede forzar que la lista se muestre debajo del Spinner utilizando el atributo overlapAnchor. Su valor por omisión es true por lo que lo establecemos a false.

        <Spinner
            android:id="@+id/spinner"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" 
            android:background="@drawable/spinner_selector"
            android:overlapAnchor="false"/>
    

    Proyecto de ejemplo

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

7 Responses to Diseño Android: Spinner

  1. Alberto dice:

    Bonito y util el spinner pero… ¿como posiciono el spinner en un elemento directamente?.

    Lo recupero de una tabla y quiero mostrar sus datos con spinner posicionado en ese dato.

    • danielme.com dice:

      Puedes seleccionar programáticmente cuando quieras un elemento del spinner con el método setSelection proporcionando la posición que ocupa en la lista que le pasas al Adapter.

      En el ejemplo, con la siguiente sentencia se selecionaria Youtube:
      spinner.setSelection(14);

  2. Duban Pineda dice:

    Como cambio el tamaño del Spinner es decir del botton que no se vea tan grande

    Gracias

    • danielme.com dice:

      Tendrás que ajustar el tamaño tanto de lo que se muestra sobre el botón en spinner_selecter_item.xml como del
      propio spinner directamente en el activity_main.xml y/o modifcar los 9-patch.

  3. Joaquin dice:

    Hola

    Con respecto a la propiedad “prompt” que mencionas al inicio de la explicación donde se especifica el titulo, una pregunta … Cómo puedo cambiar el color tanto del background y de la letra donde se despliga este titulo?

    Saludos & Gracias

  4. Javier dice:

    cual es el spinner_item_selector.png como debe ser?

  5. De Leon dice:

    Eres el mejor enserio amigo gracias realmente no entendia lo que decias pero poco a poco fui entendiendo lo que nos estabas enseñando🙂

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: