Android ListView: Estrategias de paginación (y II)

Última actualización: 25/11/2012

Completamos la demo con las dos estrategias que faltaban.

«Load More» Button

Esta solución al problema de paginación es análoga a EndlessListView, la única diferencia es que la carga de los siguientes datos no se realiza de forma automática al hacer scroll sino que se requiere la petición expresa del usuario.  El objetivo es optimizar el rendimiento de la aplicación y no obtener y mostrar datos que el usuario no desea ver por el simple hecho de haber avanzado hasta el final del ListView.

Para este ejemplo nos sirve el mismo layout endless.xml, pero habrá que definir un nuevo layout footer_loadmore.xml. Se puede utilizar este footer para mostrar el botón o el progress bar programáticamente según nuestras necesidades.

<?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:gravity="center_horizontal"
    android:orientation="horizontal"
    android:padding="3dp" >

    <ProgressBar
        android:id="@+id/progressBar1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminateOnly="true" 
        android:visibility="gone"/>
    
     <Button
        android:id="@+id/buttonLoadMore"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="loadMore"
        android:text="@string/loadmore" />

</LinearLayout>

La nueva activity LoadMoreListViewActivity es muy similar a EndlessListViewActivity. Por simplicidad se va a modificar esa clase, aunque para la demo publicada en GitHub se ha abstraído el código común en una superclase. El proceso que hay que implementar es el siguiente :

  1. Detectar si el usuario ha hecho scroll hasta el final y quedan elementos por mostrar. Esto ya estaba implementado, pero ahora en lugar de ejecutar la tarea asíncrona que obtiene los nuevos datos simplemente mostramos el footer con el botón.
  2. Si el usuario pulsa el botón, se ejecutará la misma tarea asíncrona que en el ejemplo anterior. En esta ocasión además hay que cambiar el botón del footer por el progress bar.
  3. Una vez obtenidos los nuevos datos, se incorporan al Adapter y se actualiza la vista, eliminando el footer y actualizando el número de elementos mostrados. Esto no cambia 🙂

A continuación se muestra el código más elevante de toda la clase. Se han resaltado aquellas líneas que marcan la diferencia con respecto al ejemplo EndlessListView.


public class LoadMoreListViewActivity extends ListActivity
{

	private Datasource datasource;

	private static final int PAGESIZE = 10;

	private TextView textViewDisplaying;
	
	private View footerView;	
	
	private boolean loading = false;

	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.endless);
		datasource = Datasource.getInstance();		

		footerView = ((LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(R.layout.footer_loadmore, null, false);
		getListView().addFooterView(footerView, null, false);		
		setListAdapter(new CustomArrayAdapter(this, R.layout.listview, datasource.getData(0, PAGESIZE)));
		getListView().removeFooterView(footerView);
		
		getListView().setOnScrollListener(new OnScrollListener()
		{			
			@Override
			public void onScrollStateChanged(AbsListView arg0, int arg1)
			{
				// nothing here				
			}
			
			@Override
			public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
			{
				boolean lastItem = (firstVisibleItem + visibleItemCount == totalItemCount);
				boolean moreRows = getListAdapter().getCount() < datasource.getSize();
				
				if (!loading &&  lastItem && moreRows)
				{
					loading = true;
					footerView.findViewById(R.id.buttonLoadMore).setVisibility(View.VISIBLE);
					footerView.findViewById(R.id.progressBar1).setVisibility(View.GONE);
					getListView().addFooterView(footerView, null, false);					
				}				
			}
		});
		
		updateDisplayingTextView();

	}
	
	public void loadMore(View view)
	{
		footerView.findViewById(R.id.buttonLoadMore).setVisibility(View.GONE);
		footerView.findViewById(R.id.progressBar1).setVisibility(View.VISIBLE);
		(new LoadNextPage()).execute("");
	}
	
	
	private class LoadNextPage extends AsyncTask<String, Void, String>
	{	

En MenuActivity ya podemos invocar la nueva Activity

	public void loadmore(View button)
	{
		startActivity(new Intent(this, LoadMoreListViewActivity.class));
	}

Por último, antes de probar añadimos esta nueva Activity al AndroidManifest.xml.

      <activity android:name=".activities.LoadMoreListViewActivity" />

load more

Paging Buttons

Por último, vamos a implementar la típica barra de botones de paginación que podemos encontrar en numerosas aplicaciones web. Esta solución es la menos «elegante» para un dispositivo Android táctil, pero será de utilidad cuando queramos que el usuario pueda desplazarse rápidamente entre los datos y, por ejemplo, acceder con un sólo click a la última página sin ni siquiera tener que obtener y mostrar en el ListView los datos intermedios.

Primero, creamos el siguiente layout buttons.xml.

buttons.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:orientation="vertical" >

    <ListView
        android:id="@android:id/list"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_below="@+id/buttonfirst"
        android:drawSelectorOnTop="false" >
    </ListView>

    <Button
        android:id="@+id/buttonfirst"
        style="?android:attr/buttonStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:enabled="false"
        android:onClick="first"
        android:text="@string/first" />

    <Button
        android:id="@+id/buttonprev"
        style="?android:attr/buttonStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_toRightOf="@+id/buttonfirst"
        android:enabled="false"
        android:onClick="previous"
        android:text="@string/previous" />

    <Button
        android:id="@+id/buttonlast"
        style="?android:attr/buttonStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:enabled="false"
        android:onClick="last"
        android:text="@string/last" />

    <Button
        android:id="@+id/buttonnext"
        style="?android:attr/buttonStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_toLeftOf="@+id/buttonlast"
        android:enabled="false"
        android:onClick="next"
        android:text="@string/next" />

    <TextView
        android:id="@+id/displaying"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_above="@android:id/list"
        android:layout_alignParentTop="true"
        android:layout_toLeftOf="@+id/buttonnext"
        android:layout_toRightOf="@+id/buttonprev"
        android:gravity="center_horizontal|center_vertical"
        android:textSize="13sp" />

    <ProgressBar
        android:id="@+id/progressBar1"
        style="?android:attr/progressBarStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/displaying"
        android:layout_centerHorizontal="true"
        android:indeterminateOnly="true" />

</RelativeLayout>

El texto del «Displaying» es ahora ligeramente diferente, ya que hay que indicar todo el rango de elementos mostrados en pantalla.

    <string name="displayshort">%1$s - %2$s of %3$s</string>
    <string name="first">first</string>
    <string name="previous">prev</string>
    <string name="next">next</string>
    <string name="last">last</string>

Creamos la clase PagingButtonsListViewActivity donde tendremos en cuenta las siguientes consideraciones a la hora de implementar la paginación:

  • Los botones sólo estarán habilitados en el caso de que su funcionalidad tenga sentido (por ejemplo, si estamos mostramos la última página el botón last no estará disponible). En buttons.xml están deshabilitados por defecto.
  • Hay que conocer en todo momento la página de datos mostrada en pantalla. Esto se puede hacer de varias maneras, yo voy a optar por guardar en un campo la posición del primer elemento mostrado. A partir de ese valor y con un poquito de matemáticas se obtendrán el resto de datos.
  • La carga de la nueva página se realizará con la misma tarea asíncrona de los ejemplos anteriores por lo que la interfaz gráfica no quedará bloqueada. En esta ocasión se ha implementado el método onPreExecute() para poder modificar la vista y habilitar el ProgressBar. Asimismo, los nuevos elementos obtenidos no se agregan a los existentes sino que los reemplaza.
  • En esta ocasión no habrá footer en el ListView, así que he colocado una animación de carga al principio del texto que informa de los datos mostramos, por lo que habrá que mostrarla y ocultarla cuando sea necesario.

El código de la clase completa quedará tal que así:

package com.danielme.blog.android.paginatedlistview.activities;

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

import com.danielme.blog.android.paginatedlistview.CustomArrayAdapter;
import com.danielme.blog.android.paginatedlistview.Datasource;
import com.danielme.blog.android.paginatedlistview.R;

import android.app.ListActivity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

/**
 * 
 * @author http://danielme.com
 * 
 */
public class PagingButtonsListViewActivity extends ListActivity
{

	private int offset = 0;

	private Datasource datasource;

	private static final int PAGESIZE = 10;

	private TextView textViewDisplaying;

	private ProgressBar progressBar;

	private boolean loading = false;

	private Button first;

	private Button last;

	private Button prev;

	private Button next;

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

		progressBar = (ProgressBar) findViewById(R.id.progressBar1);
		textViewDisplaying = (TextView) findViewById(R.id.displaying);
		first = (Button) findViewById(R.id.buttonfirst);
		prev = (Button) findViewById(R.id.buttonprev);
		next = (Button) findViewById(R.id.buttonnext);
		last = (Button) findViewById(R.id.buttonlast);
		datasource = Datasource.getInstance();

		setListAdapter(new CustomArrayAdapter(this, R.layout.listview, new ArrayList<String>()));

		(new LoadNextPage()).execute();
	}

	@Override
	protected void onListItemClick(ListView l, View v, int position, long id)
	{
		Toast.makeText(this, getListAdapter().getItem(position) + " " + getString(R.string.selected), Toast.LENGTH_SHORT).show();
	}

	private class LoadNextPage extends AsyncTask<String, Void, String>
	{
		private List<String> newData = null;

		@Override
		protected void onPreExecute()
		{
			progressBar.setVisibility(View.VISIBLE);
			loading = true;
			super.onPreExecute();
		}

		@Override
		protected String doInBackground(String... arg0)
		{
			// para que de tiempo a ver la animación
			try
			{
				Thread.sleep(1000);
			}
			catch (InterruptedException e)
			{
				Log.e("PagingButtons", e.getMessage());
			}
			newData = datasource.getData(offset, PAGESIZE);
			return null;
		}

		@Override
		protected void onPostExecute(String result)
		{
			CustomArrayAdapter customArrayAdapter = ((CustomArrayAdapter) getListAdapter());
			customArrayAdapter.clear();
			for (String value : newData)
			{
				customArrayAdapter.add(value);
			}
			customArrayAdapter.notifyDataSetChanged();

			updateDisplayingTextView();

			loading = false;
		}

	}

	private void updateDisplayingTextView()
	{
		String text = getString(R.string.displayshort);
		text = String.format(text, Math.min(datasource.getSize(), offset + 1), Math.min(offset + PAGESIZE, datasource.getSize()), datasource.getSize());
		textViewDisplaying.setText(text);
		updateButtons();
		progressBar.setVisibility(View.INVISIBLE);
	}

	private void updateButtons()
	{
		if (getCurrentPage() > 1)
		{
			first.setEnabled(true);
			prev.setEnabled(true);
		}
		else
		{
			first.setEnabled(false);
			prev.setEnabled(false);
		}
		if (getCurrentPage() < getLastPage())
		{
			next.setEnabled(true);
			last.setEnabled(true);
		}
		else
		{
			next.setEnabled(false);
			last.setEnabled(false);
		}

	}

	private int getLastPage()
	{
		return (int) (Math.ceil((float) datasource.getSize() / PAGESIZE));
	}

	private int getCurrentPage()
	{
		return (int) (Math.ceil((float) (offset + 1) / PAGESIZE));
	}

	/******************** BUTTONS *************************/

	public void first(View v)
	{
		if (!loading)
		{
			offset = 0;
			(new LoadNextPage()).execute();
		}
	}

	public void next(View v)
	{
		if (!loading)
		{
			offset = getCurrentPage() * PAGESIZE;
			(new LoadNextPage()).execute();
		}
	}

	public void previous(View v)
	{
		if (!loading)
		{
			offset = (getCurrentPage() - 2) * PAGESIZE;
			(new LoadNextPage()).execute();
		}
	}

	public void last(View v)
	{
		if (!loading)
		{
			offset = (getLastPage() - 1) * PAGESIZE;
			(new LoadNextPage()).execute();
		}
	}

}

Y listo. Sólo resta añadir esta nueva Activity al manifest e invocarla desde el botón correspondiente de la pantalla inicial para comprobar el resultado.

paging buttons

Proyecto en GitHub

Proyecto para Eclipse ADT publicado en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.

Nota: El proyecto puede presentar ligeras diferencias con el código mostrado. Asimismo, parte del código común se ha abstraído en una superclase.

El .apk está en el siguiente enlace:

https://github.com/danielme-com/Android-Paginated-ListView-Demo/blob/master/Paginated%20ListView%20Demo.apk?raw=true

4 comentarios sobre “Android ListView: Estrategias de paginación (y II)

  1. Hola, muy buen tutorial, tanto el I como el II, queria preguntarte si esto se puede hacer en ves con un ArrayAdapter con un BaseAdapte? si es asi en el AsyncTask, que tipos de datos usaria? Un saludo

    1. En principio no hay ninguna diferencia, ListView requiere un objeto de la interfaz ListAdapter así que simplemente puedes usar cualquier implementación de las que trae Android o hacer una propia implementando directamente la interfaz o bien heredando de una de las existentes que es lo que he hecho yo con mi CustomArrayAdapter. BaseAdapter es sólo una clase abstracta que agrupa el código común de todas las implementaciones de ListAdapter como ArrayAdapter y CursorAdapter.

      En cuanto al AsyncTask, los cambios dependerán de cómo carges los datos en tu Adapter, pero el patrón de código a seguir es el mismo (obtener los datos y realizar la lógica que requiera tiempo en el doInBackground y cargarlos en el Adapter y actualizar la pantalla en el onPostExecute).

      1. Hola otra vez, cambie la clase BaseAdapter por ArrayAdapter como en tutorial y me funciona fenomenal. Luego de ver como funcionaba modifique el adapter para que mostrara una imagen que se descarga en tiempo de ejecución y la muestra en el ListView, pero al ser ese el caso cuando se hace el Scroll muy rápido se ralentiza mucho el ListView , no tendrás alguna sugerencia de que puedo hacer para evitar eso?

      2. Tienes que hacer la obtención de imágenes de forma asíncrona en un hilo aparte para no bloquear la pantalla usando por ejemplo un AsyncTask (más claro que usar Runnable). Es lo que hago en Muspy for Android y puedes verlo aquí en la clase LoadCover: https://github.com/danielme-com/Muspy-for-Android/blob/master/src/com/danielme/muspyforandroid/activities/adapters/ReleasesAdapter.java

        El problema es que al hacer scroll las filas del ListView se reciclan, por lo que es posible que cuando obtengas una imagen ya no sea necesaria mostrarla en pantalla y si la muestras puede que no sea la correcta, puedes ver como lo controlo en el método onPostExecute.

        En breve abriré una sección en el blog dedicada a snippets para Android y explicaré cosillas como estas.

Deja un comentario

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