Programando una aplicación Android para mostrar noticias, es habitual mostrar los resultados en un listView. Si el número de datos a mostrar es grande y tenemos que descargarlos de internet no es conveniente descargar todos de golpe sino ir descargando a medida que el usuario va moviéndose por la lista para evitar que tenga que esperar a que los resultados se muestren y que se descarguen muchos datos de internet.

La descarga debería realizarse siempre desde un hilo de ejecución diferente al principal, para no bloquear la interfaz gráfica y que esta siga respondiendo (que el teléfono no se quede “congelado”). Para hacer esto debemos hacer uso de un Loader (AsyncTaskLoader) pero esto lo dejaremos para un post posterior.

En este quiero mostraros cómo implemento yo las endlist, o listas sin fin. No será ni el mejor ni el más eficiente (no soy programador profesional) pero funciona aunque estaría encantado de leer mejoras.

Parto de que todo el mundo sabe montar un listView, si no, ya hay excelentes tutoriales en español sobre cómo hacer (por ejemplo este). Yo siempre empleo un listView y un elemento independiente para cada uno de los elementos de la lista, por ejemplo:

Mi listView:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/noticias"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center" >
    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleLarge"
        android:layout_width="68dp"
        android:layout_height="68dp"
        android:layout_gravity="center" />
    <ListView
        android:id="@+id/noticias_list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</FrameLayout>

Aquí he empleado un FrameLayout para hacer algo interesante. El FrameLayout sólo muestra una View así, si pongo dos Views una se mostrará por encima de la otra. Esto está bien en mi caso porque primero voy a mostrar una barra de progreso (ProgressBar) mientras se cargan los datos y luego el ListView. Así guardaré la misma configuración de márgenes para ambos (en un LinearLayout por ejemplo, uno no podría ocupar el espacio que está ocupando el otro con lo que quedaría la lista desplazada, pruébalo en tu emulador para entenderlo mejor).

Cada elemento de mi lista será así:

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

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="@dimen/list_element_height"
    android:layout_gravity="center_horizontal"
    android:orientation="horizontal" >

    <ImageView
        android:id="@+id/list_imagen"
        android:layout_width="80dp"
        android:layout_height="88dp"
        android:contentDescription="@string/desc" />

    <LinearLayout
        android:id="@+id/list_content_container"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:orientation="vertical" >

        <TextView
            android:id="@+id/list_titulo"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textStyle="bold" />

        <TextView
            android:id="@+id/list_authorAndDate"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom"
	    android:textSize="10sp"
	    android:textStyle="italic"/>

        <TextView
            android:id="@+id/list_extracto"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="12sp"/>

    </LinearLayout>
</LinearLayout>

Cada elemento mostrará una imagen, un título, un autor y fecha y un pequeño extracto del contenido. Todos estos datos los obtengo a través de una consulta JSON (si estáis interesados también la pondremos en otro post).

Cómo aplicar entonces el endless list?? Lo primero es que nuestra actividad (o fragmento) tiene que implementar onScrollListener, por ejemplo:


public class NoticiasFragment extends Fragment implements OnScrollListener{
....

Aquí he empleado un Fragment (porque mi aplicación usa ActionBarCompat y Tabs por lo que los fragments es la mejor solución) que tendrá las ListView e implementa OnScrollListener.

Dentro del código tendremos que recordar ponerle a la ListView el ScrollListener:


//Entre nuestras variables habrá un ListView

private ListView lstView;
...

//mas adelante en el código pondremos el ScrollListener, siempre después de haber llenado de datos la lista (después de un notifyDataSetChanged())
//el this será una referencia a al fragment o a la actividad dónde está la lista

lstView.setOnScrollListener(this);

Lo que nos queda será implementar los métodos de OnScrollListener:


//Creamos una variable que indique segun cuantos elementos fuera
//de pantalla queremos que empiece la carga. Si queremos que
//cargue mas datos cuando quedan 2 elementos fuera de pantalla,
//hariamos esta variable igual a 2

private int visibleThereshold = 0;

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
        // Realizamos la actualizacion cuando se detenga el Scroll,
        // o sea, este en estado idle
	if(scrollState == SCROLL_STATE_IDLE){
                // Si el ultimo elemento + 1 es mayor o igual que el
                // numero de elementos en la lista menos los que quedan
                // fuera de la pantalla inicia la carga
		if( (lstView.getLastVisiblePosition()+1) >= lstView.getCount() - visibleThereshold ){
                        // Añado a la lista una footer view para indicarle al
                        // usuario que se estan cargando datos
                        lstView.addFooterView(myFooterView);
                        tuFuncionDeCargaDeDatos();
		}
	}
}

Cuando el scroll de la lista llegue abajo y cumpla nuestra condición, lanzará la función que hemos programado de carga de datos. Y ya tenemos implementado nuestro endless list.

Si quisiéramos durante la carga de datos anular el scroll o las pulsaciones en pantalla tendríamos que hacer nuestra propia View que herede de ListView e interceptar el evento dispatchTouchEvent. A modo de ejemplo, nuestro lstView será ahora:

private myCustomListView lstView;

y tendremos que programar nuestro myCustomListView.java que herederá de ListView:

public class myCustomListView extends ListView {
	
	View myFooterView;

	public myCustomListView(Context context) {
		super(context);
	}
	public myCustomListView(Context context, AttributeSet attrs) {
		super(context, attrs);
	}
	public myCustomListView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}
	@Override
	public boolean dispatchTouchEvent(MotionEvent ev){
		switch (ev.getAction()){
		case MotionEvent.ACTION_MOVE:
                        // Si se muestra el FooterView de Loading...
			if(myFooterView.isShown()){
                                // No le hacemos caso al movimiento de pantalla
				if(ev.getAction() == MotionEvent.ACTION_MOVE)
					return false;
			}
		case MotionEvent.ACTION_DOWN:
		case MotionEvent.ACTION_UP:
			if(myFooterView.isShown()){
                                // Lo mismo hacemos cuando el usuario pulsa en pantalla
                                // Simplemente ignoramos los eventos
				if(ev.getAction() == MotionEvent.ACTION_DOWN || ev.getAction() == MotionEvent.ACTION_UP)
					return false;
			}
		default:
                        // Para el resto, enviamos el evento a la clase padre
			return super.dispatchTouchEvent(ev);
		}
	}
	
	public void saveFooterView (View footerView){
		myFooterView = footerView;
	}
}

Y por hoy hasta aquí. Ya ha quedado un post bastante largo. Espero que el siguiente pueda dedicarlo a los Loaders para explicar la carga de datos de modo asíncrono. Hasta entonces, sigamos programando.

 

 

Anuncios