Optimización de un ListView
ListView y adapters.
En android disponemos de los Listview como forma de mostrar una lista de valores. En el caso de que la lista de elementos abarque más tamaño que la pantalla, podremos desplazarnos entre ellos mediante un scroll. También podremos permitir que el ListView admita la selección de uno o varios valores.
Para poder utilizar un ListView debemos entender como funcionan los adaptadores (Adapters). Todos los controles de selección tienen una “interfaz” común respecto al manejo de datos. Estos son los adaptadores. Siendo más concretos, un adaptador nos va a permitir indicar qué tipo de datos vamos a pasar a la lista y cómo deseamos que se visualicen esos datos en pantalla.
Centrándonos en la forma de pasar los datos al ListView los adaptadores más usados son:
- ArrayAdapter: pasaremos arrays de cualquier tipo de objetos así como colecciones list.
- SimpleCursorAdapter: mapearemos las columnas de un cursor (por ejemplo, obtenido de la consulta a una base de datos o a un ContentProvider) con las vistas correspondientes.
Veamos un ejemplo sencillo de uso de un ArrayAdapter:
final String[] valores = new String[]{"Uno","Dos","Tres","Cuatro","Cinco"}; ArrayAdapter adaptador = new ArrayAdapter(this, android.R.layout.simple_list_item_1, valores);
Podemos ver que estamos pasando tres valores al constructor. El primero es el contexto, en este caso sería la propia actividad. En el segundo caso un identificador del layout que queremos que forme la vista de una fila de nuestra lista. En este caso le pasamos un layout compuesto simplemente por un textView, pero podemos pasar cualquier layout predefinido en android o crearnos uno personalizado, como veremos más adelante. En tercer lugar le indicamos los datos que queremos que se muestren en ese layout.
Una vez declarado nuestro adaptador, simplemente le tenemos que asignar al listView y ya tendríamos preparada la lista.
ListView listaValores = (ListView)findViewById(R.id.listaValores); listContactos.setAdapter(adaptador);
Si quisiesemos utilizar un SimpleCursorAdapter, la forma de hacerlo sería muy similar. En el caso del cursor, vamos a añadir también el mapeo de sus columnas a las views del listView (SimpleCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to)). En el caso del ArrayAdapter, como le estamos pasando simplemente una columna de valores, el mapeo es automático.
String[] projection = new String[]{ Data._ID, Data.DISPLAY_NAME, Phone.TYPE, Phone.NUMBER}; Cursor cursor = this.managedQuery( Data.CONTENT_URI, projection, Data.MIMETYPE + "= '" + Phone.CONTENT_ITEM_TYPE + "'" + " AND " + Phone.NUMBER + " IS NOT NULL", null, null);
Recuperamos en un cursor de un contentProvider datos relativos a nuestros contactos.
String[] columnas = new String[]{Data.DISPLAY_NAME, Phone.NUMBER}; int[] id_views = new int[]{ android.R.id.text1, android.R.id.text2} SimpleCursorAdapter adaptador = new SimpleCursorAdapter( this, android.R.layout.simple_list_item_2, cursor, columnas, id_views);
Al constructor de un SimpleCursorAdapter debemos pasarle los siguientes parámetros:
- el contexto.
- el id del layout que queremos que se dibuje en el listview. En este caso le pasamos un layout compuesto por dos textviews. Como podéis ver, no estamos utilizando nuestra clase R, sino la clase de android (android.R.layout.simple_list_item_2).
- el cursor
- Un array de string con las columnas del cursor que queremos que se visualicen. Aunque nuestro cursor contiene cuatro tipo de datos sólo vamos a mostrar el nombre del contacto y el número de teléfono.
- Un array de enteros en el que realizamos el mapeo a los views.
ListActivity.
Android posee una clase que extiende de Activity y que está especialmente preparada para trabajar con listas, ListActivity.
Si utilizamos un ListActivity no es necesario que nos declaremos un layout conteniendo un listView. Se mostrará una actividad ocupada totalmente por un listView. En el caso de que quisiesemos tener además de un listview más controles, como un textview o un button, tendríamos que crearnos dentro del layout un listView con el identificador android:list.
android:id="@android:id/list"
android:layout_width="match_parent"android:layout_height="wrap_content">
</ListView>
También podemos añadir al layout un textview con el identificador @android:id/empty. Si la lista no muestra ningún valor se mostrá el textView en el centro de la pantalla con los atributos que hayamos indicado.
Para poder asignar el adaptador a nuestra ListActivity tenemos que utilizar el método setListAdapter(Adapter adapter).
ListViews personalizados.
En muchos casos, los layouts que trae android para un listView serán suficiente. Sin embargo, es posible declararnos nuestros propios layouts. Imaginemos que queremos que cada elemento de la fila contenga datos relativos a un usuario, como el nombre, el apellido y la dirección. Para ello tenemos que crearnos nuestro propio layout.
Una vez creado el layout tendremos que sobreescribir la clase adapter para poder indicarle el mapeo a cada una de las views. El método que vamos a sobreescribir y nos va a permitir cargar las filas es getView(int position, View convertView, ViewGroup parent) . Este método se llama cada vez que se carga una fila (con position nos indica que fila se está cargando). Debemos devolver una view inflada con nuestro layout personalizado.
private class UsuarioAdapter extends ArrayAdapter{ Activity context; public UsuarioAdapter(Activity context) { super(context, R.layout.usuariolayout,usuarios); this.context = context; } @Override public View getView(int position, View convertView, ViewGroup parent) { Usuario usuario; usuario = usuarios.get(position); View view = null; LayoutInflater inflate = context.getLayoutInflater(); view = inflate.inflate(R.layout.usuariolayout, null); TextView tvNombre = (TextView)view.findViewById(R.id.tvNombre); TextView tvApellido = (TextView)view.findViewById(R.id.tvApellido); TextView tvDireccion = (TextView)view.findViewById(R.id.tvDireccion); tvNombre.setText(usuario.getNombre()); tvApellido.setText(usuario.getApellido()); tvDireccion.setText(usuario.getDireccion()); return view; } }
En primer lugar pasamos al constructor del ArrayAdapter el contexto, el identificador del layout personalizado que vamos a cargar y el array con los valores.
A continuación sobreescribimos el método getView. Para ello, recogemos los valores de nuestro arrayList para el elemento de la lista que se va a cargar. Después, instanciamos el fichero XML del layout en sus correspondientes objetos views.
LayoutInflater inflate = context.getLayoutInflater(); view = inflate.inflate(R.layout.usuariolayout, null);
Sólo nos queda crear los objetos que componen el layout, en este caso tres TextViews y pasarles los valores del objeto Usuario que hemos obtenido de la posición actual del array.
Optimizando ListView mediante ConvertView.
Cuando trabajamos con layouts personalizados, cada vez que se carga una fila estamos creando un objeto. Si trabajamos con listas de muchos elementos tendremos un problema de rendimiento pues vamos a generar una gran cantidad de objetos en memoria.
Con convertView podremos reutilizar los primeros objetos creados. Detalladamente, sólo crearemos tantos objetos como filas se visualizan en la pantalla sin hacer scroll. Cuando nos desplacemos con el scroll, android obtendrá la instancia del primer elemento que dejamos de visualizar y lo utilizará para mostrar los datos del primer elemento que vamos a ver con el scroll. Veamos una imagen que nos aclara rápidamente todo esto.
Os recomiendo que echéis un vistazo al artículo de donde he sacado la imagen.
public View getView(int position, View convertView, ViewGroup parent) { Usuario usuario; usuario = usuarios.get(position); View view = null; TextView tvNombre; TextView tvApellido; TextView tvDireccion; if (convertView==null){ LayoutInflater inflate = context.getLayoutInflater(); view = inflate.inflate(R.layout.usuariolayout, null); tvNombre = (TextView)view.findViewById(R.id.tvNombre); tvApellido = (TextView)view.findViewById(R.id.tvApellido); tvDireccion = (TextView)view.findViewById(R.id.tvDireccion); }else{ view = convertView; } tvNombre.setText(usuario.getNombre()); tvApellido.setText(usuario.getApellido()); tvDireccion.setText(usuario.getDireccion()); return view; }
Si el objeto convertView nos llega vacío es que estamos cargando las filas que se visualizan en pantalla. Para inflar el layout y creamos los objetos views hacemos igual que en el ejemplo anterior. Cuando el convertView está instanciado es que estamos utilizando el scroll. En este caso no hace falta crearnos nuestro objeto view, sino que simplemente recuperamos su instancia. Después sólo es necesario asignar los valores de nuestro array en la posición que vamos a mostrar y devolver el objeto view.
ConvertView es muy sencillo de usar y supone una mejora muy importante en el rendimiento de nuestras listas así que es de uso obligatorio.
Optimizando un poco más con el patrón ViewHolder.
Cada vez que utilizamos el método findViewById() estamos realizando una operación muy cara en términos de rendimiento. Tenemos que evitar utilizar este método en la medida de lo posible, y para ello, cuando trabajamos con nuestras listas disponemos del patrón ViewHolder.
Nos crearemos una clase estática con la cual manejaremos las referencias a los campos que forman nuestro layout y de esta forma no tendremos que utilizar findViewById. Para ello, adjuntaremos esta clase a cada fila mediante el método setTag(). Cuando la fila sea reutilizada podemos obtener el tag de esa fial con el método getTag().
Es mucho más eficiente utilizar convertView que el patrón viewHolder, pero ambos juntos optimizan mucho un listView.
Veamos un ejemplo, además de mostrar los datos del usuario, vamos a añadir un checkbox para poder seleccionar o deseleccionar usuarios.
static class ViewHolder{ protected TextView tvNombre; protected TextView tvApellido; protected TextView tvDireccion; protected CheckBox cbSeleccionado; }
Nuestra clase ViewHolder sólo va a contener los atributos para cada uno de nuestros componentes en el layout.
@Override public View getView(int position, View convertView, ViewGroup parent) { Usuario usuario; usuario = usuarios.get(position); View view = null; if (convertView==null){ LayoutInflater inflate = context.getLayoutInflater(); view = inflate.inflate(R.layout.usuariolayout, null); final ViewHolder viewHolder= new ViewHolder(); viewHolder.tvNombre = (TextView)view.findViewById(R.id.tvNombre); viewHolder.tvApellido = (TextView)view.findViewById(R.id.tvApellido); viewHolder.tvDireccion = (TextView)view.findViewById(R.id.tvDireccion); viewHolder.cbSeleccionado = (CheckBox)view.findViewById(R.id.cbSeleccionado); viewHolder.cbSeleccionado.setOnCheckedChangeListener( new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { Usuario usuario = (Usuario)viewHolder.cbSeleccionado.getTag(); usuario.setSeleccionado(isChecked); } }); view.setTag(viewHolder); viewHolder.cbSeleccionado.setTag(usuarios.get(position)); }else{ view = convertView; view.getTag()).cbSeleccionado.setTag(usuarios.get(position)); } ViewHolder viewHolder = (ViewHolder) view.getTag(); viewHolder.tvNombre.setText(usuario.getNombre()); viewHolder.tvApellido.setText(usuario.getApellido()); viewHolder.tvDireccion.setText(usuario.getDireccion()); viewHolder.cbSeleccionado.setChecked(usuario.isSeleccionado()); return view; }
Cuando tenemos que crear el objeto view para la fila, guardamos las instancias de las vistas que componen el layout en nuestro objeto viewHolder. Después guardamos el objeto en nuestra fila mediante setTag().
Como también queremos utlizar un checbox, implementamos su listener. Cuando hacemos click en él, recuperamos el objeto usuario de esa fila que almacenaremos en el tag del checkbox y marcamos el atributo seleccionado como seleccionado o no. Como vemos, el método tag nos permite almacenar cualquier objeto.
Cuando estamos reciclando una fila, recuperamos el objeto viewHolder mediante el método getTag() de nuestra view devuelva en el parámetro convertView. Ya sólo nos queda asignar el valor del objeto usuario a los atributos del viewHolder.
Crearnos secciones indexadas en la lista.
En una listView podemos hacer que al desplazarnos con el scroll podamos avanzar más rápido viendo además algún elemento común de las filas que las agrupe. Por ejemplo, si utilizamos la agenda de contactos, podemos movernos entre ellos, viendo su primera letra.
Para poder movernos rápidamente con el scroll tendremos que poder el atributo xml del listView fastScrollEnabled a true. También podemos hacerlo mediante código con setFastScrollEnabled(boolean enabled).
Para visualizar el elemento común deberemos implementar la interfaz SectionIndexer en nuestra clase adaptador.
Esta interfaz nos obliga a implementar tres métodos.
@Override public int getPositionForSection(int section) { return indexadores.get(secciones[section]); } @Override public int getSectionForPosition(int position) { return 1; } @Override public Object[] getSections() { return secciones; }
Para ello debemos de trabajar con una lista ordenada. Almacenaremos en un hashmap la primera letra y la primera posición donde se da. Como el hashmap no permite tener claves duplicadas, sólo vamos a tener un registro por letra.
La otra colección que tenemos que crear en un array, en este caso de strings, con todo el conjunto de las primeras letras ordenadas. Estas las obtenemos de las claves del hashmap. Las ordenamos y las pasamos a nuestro array.
class PaisesAdapter extends ArrayAdapter implements SectionIndexer{ Activity context = null; HashMap indexadores; String[] secciones; public PaisesAdapter(Activity context){ super(context, android.R.layout.simple_list_item_1, paisesOrdenados); this.context = context; indexadores = new HashMap(); int tamaño = paisesOrdenados.length; String pais=null; for (int i=0;i seccionParaLetras = indexadores.keySet(); ArrayList listSecciones = new ArrayList(seccionParaLetras); Collections.sort(listSecciones); secciones = new String[listSecciones.size()]; listSecciones.toArray(secciones); } } }
Reader Comments (8)
Hola,
Otras partes no sé, pero el ejemplo del ConvertView no compila. Salta a la vista, tvNombre, tvApellido, etc... estan definidos dentro de un if y lo tratas de acceder fuera de él.
Un saludo.
Tienes toda la razón del mundo. Ya está corregido.
Muchas gracias.
Un buen articulo. Gracias por tu aportacion.
Un buen articulo. Gracias por tu aportacion.
Muchas gracias, me alegro que pueda resultar útil.
Excelente articiculo! Gracias
Excelente articulo, he implementado en mis desarrollos ListViews personalizados y por consecuencia sobreescribiendo la clase adapter, puedo mencionar que la explicación que se da en esta aportación es excelente, una vez que podamos implementar de esta manera los listview podremos hacer lo que queramos, por ejemplo adaptarle Checkbox, hacer el pintado de filas según cierta operación a modo de indicadores, Incluso hacer Filtrados que es otra cosa interesante que se puede, para localizar y mostrar el elemento en caso de ser localizado en el arrayadapter esto gracias a la interface Filterable.
Excelente articulo, he implementado en mis desarrollos ListViews personalizados y por consecuencia sobreescribiendo la clase adapter, puedo mencionar que la explicación que se da en esta aportación es excelente, una vez que podamos implementar de esta manera los listview podremos hacer lo que queramos, por ejemplo adaptarle Checkbox, hacer el pintado de filas según cierta operación a modo de indicadores, Incluso hacer Filtrados que es otra cosa interesante que se puede, para localizar y mostrar el elemento en caso de ser localizado en el arrayadapter esto gracias a la interface Filterable.