En muchas ocasiones no es suficiente con las clases y objetos que la librería de android nos ofrece por lo que hay que construir soluciones personalizadas. Este es el caso de las listas de datos que tienen un aspecto diferente al «standard» que los layout de android tiene. Para ello habrá que crear un adaptador propio sobreescribiendo los métodos necesarios.

Listas personalizadas

Lo primero que hay que tener en cuenta es que ya no se utilizará un objeto de tipo ArrayAdapter básico sino que habrá que crear uno propio. Para ello se crea una clase que implemente la interfaz BaseAdapter. En el momento que se extiende de esta clase se obliga a implementar los métodos:

  • getCount(): devuelve en número de elementos que tendrá la lista. Este será el número de veces que se ejecuten el resto de métodos
  • getItem(): devuelve el item asociado a una posición concreta
  • getItemId(): devuelve el id del ítem asociado a una posición concreta
  • getView(): devuelve la vista que tienen que rellenar en una posición concreta

Para empezar se crea un constructor que se utilizará para asociar el objeto desde la clase donde está el componente en cuestión. Este constructor tendrá como parámetros la lista de datos iniciales que se quieren mostrar y el contexto sobre el que se mostrará la lista

Context context;
List listaDatos;

public ListaSimpleAdaptador(Context context, List listaDatos) {
    this.context = context;
    this.listaDatos = listaDatos;
}

Cuando sea llamado el constructor los datos se rellenarán por lo que el resto de métodos serán ejecutados:

  • getCount() accederá al número de elementos de la lista mediante el método size()
@Override
public int getCount() {
    return listaDatos.size();
}
  • getItem() accederá al elemento de la lista en la posición mediante el método get(position)
@Override
public Object getItem(int position) {
    return listaDatos.get(position);
}
  • getItemId() devolverá el id del elemento asociado a la posición. Puede ser llamado mediante un método getId() del objeto o simplemente devolviendo la propia posición
@Override
public long getItemId(int position) {
    return position;
}
  • getView() devuelve el objeto view con la vista inflada y rellena. Dicha vista será la apariencia que tendrá cada una de las filas. Para ello necesitamos el xml de dicha vista. En este caso se pondrá una foto, un texto y un subindice
<?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="wrap_content"
    android:orientation="horizontal"
    android:padding="10dp">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:layout_marginLeft="10dp">
        
        <TextView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="0.5"
            android:text="Nombre"
            android:id="@+id/text_item_nombre"
            android:textAppearance="@style/TextAppearance.AppCompat.Large"/>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="0.5"            
            android:text="Apellido"
            android:id="@+id/text_item_apellido"
            android:textAppearance="@style/Base.TextAppearance.AppCompat.Small"/>

    </LinearLayout>

</LinearLayout>

Los parámetros de este método son:

  • position: número que actualmente se está evaluando de la lista. Este numero se utilizará para poder acceder a la posición concreta de la lista de datos
  • convertView: vista que se inflará con el layout que tenga cada elemento de la lista
  • viewGroup: elemento donde se va a situar el layout inflado

Con este xml creado ya podríamos rellenar la vista. Lo primero que hay que hacer es inflar el parámetro convertView con dicho xml mediante un objeto de tipo LayoutInflater y el método from() e inflate() donde se le indicará el xml que queremos utilizar.

convertView = LayoutInflater.from(context).inflate(R.layout.item_lista_simple,parent,false);

Los parámetros de este método inflate son:

  • id: layout que representa cada una de los elementos de la fila
  • viewGroup: vista donde se asociará el diseño del layout del parámetro anterior
  • attach: si se quiere conservar la jerarquía del las views del diseño inflado

Una vez inflado el parámetro convertView ya se puede acceder a cada uno de los elementos del mismo mediante el método findViewById(). Lo que hay que tener en cuenta es que no directamente desde la clase sino desde la vista inflada

convertView = LayoutInflater.from(context).inflate(R.layout.item_lista_simple,parent,false);
TextView nombre, apellido;
nombre = (TextView) convertView.findViewById(R.id.text_item_nombre);
apellido = (TextView) convertView.findViewById(R.id.text_item_apellido);

De esta forma como el método getView se ejecuta tantas veces como elementos en la lista se tengan para poder poner elementos en la lista solo habría que acceder a las posiciones.

nombre.setText(listaDatos.get(position).toString());
apellido.setText(listaDatos.get(position).toString());

Antes de del mismo modo, para que todo se mucho más eficiente y ya que el método se ejecuta tantas veces como elementos de la lista, el convertview solo se inflará la primera vez comprobando su existencia

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    if (convertView == null){
        convertView = LayoutInflater.from(context).inflate(R.layout.item_lista_simple,parent,false);
    }

    TextView nombre = (TextView) convertView.findViewById(R.id.text_item_nombre);
    TextView apellido = (TextView) convertView.findViewById(R.id.text_item_apellido)
    nombre.setText(listaDatos.get(position).toString());
    apellido.setText(listaDatos.get(position).toString());

    return convertView;
}

Por último esta nueva clase deberá de contar con un constructor que pueda servir como punto de enlace con la clase donde será llamado. Dependiendo de las necesidades del proyecto, los constructores tendrán unos parámetros u otros, dependiendo de las necesidades

public class ListaSimpleAdaptador extends BaseAdapter {

    Context context;
    List listaDatos;

    public ListaSimpleAdaptador(Context context, List listaDatos) {
        this.context = context;
        this.listaDatos = listaDatos;
    }

    @Override
    public int getCount() {
        return listaDatos.size();
    }

    @Override
    public Object getItem(int position) {
        return listaDatos.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null){
            convertView = LayoutInflater.from(context).inflate(R.layout.item_lista_simple,parent,false);
        }
        TextView nombre = (TextView) convertView.findViewById(R.id.text_item_nombre);
        TextView apellido = (TextView) convertView.findViewById(R.id.text_item_apellido);
        nombre.setText(((Persona)listaDatos.get(position)).getNombre());
        apellido.setText(((Persona)listaDatos.get(position)).getApellido());
        return convertView;
    }
}

Para poder ser llamado, desde la clase donde se encuentra instancias la lista tan solo tendrá que ser creado y ajustado con el método setAdapter. Para ello antes tiene que existir una clase Persona con sus getter y setter

public class ListaPersoActivity extends AppCompatActivity {
    
    ListView listaPerso;
    ArrayList personas = new ArrayList();
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_listaperso);

        for (int i = 0; i<10;i++){
            personas.add(new Persona("Nombre "+String.valueOf(i),"Apellido "+String.valueOf(i)));
        }
        
        listaPerso = (ListView) findViewById(R.id.list_view_perso);
        listaPerso.setAdapter(new ListaSimpleAdaptador(this,personas));
    }
}

Manejo de eventos

Para manejar los eventos producidos en un elemento de una lista personalizada son dos las posibilidades:

  • Manejar la pulsación directemente del ítem de la lista, al igual que se explico en la entrada de listView con el escuchador OnItemClickListener desde la actividad donde esté la lista
listaPerso.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Toast.makeText(ListaPersoActivity.this,((Persona)(parent.getAdapter().getItem(position))).getNombre().toString()
                        ,Toast.LENGTH_SHORT).show();
            }
        });
  • Manejar la pulsación de uno de los elementos de la posición pulsada. Para ello hay que programar el escuchador directamente en la clase que ha configurado el escuchador
public View getView(final int position, View convertView, ViewGroup parent) {
        if (convertView == null){
            convertView = LayoutInflater.from(context).inflate(R.layout.item_lista_simple,parent,false);
        }
        TextView nombre = (TextView) convertView.findViewById(R.id.text_item_nombre);
        TextView apellido = (TextView) convertView.findViewById(R.id.text_item_apellido);
        nombre.setText(((Persona)listaDatos.get(position)).getNombre());
        apellido.setText(((Persona)listaDatos.get(position)).getApellido());
        nombre.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(context,"Nombre en la posición " +String.valueOf(position),Toast.LENGTH_SHORT).show();
            }
        });
        
        return convertView;
    }