Android – Liste personnalisée

Objectif du tutoriel

L’objectif de ce tutoriel est de créer une liste personnalisée pour une application Android. Pour l’exemple j’ai voulu refaire une liste déroulante présente dans l’application Gmail lors de la sélection des libellés d’un email. L’application sera très simple : un écran d’accueil sur lequel il y aura un texte et un bouton pour accéder à la liste déroulante dans laquelle on choisit différentes couleurs qui seront ensuite affichées sur la page d’accueil. Voici ce que vous devriez obtenir à la fin de ce tutoriel :

Accueil  Sélection des couleurs  Couleurs sélectionnées

Initialisation de l’application

Dans un premier temps nous allons simplement créer la base de l’application à savoir une activité pour la page d’accueil, une activité pour afficher la liste et les vues associées. On rajoutera ensuite au fur et à mesure les différentes fonctionnalités pour arriver au résultat que je vous ai présenté.

Création du projet

Tout d’abord on commence par créer le projet avec comme package principal fr.infinitestudio.customlist.

On édite ensuite le manifest :

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="fr.infinitestudio.customlist"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="18" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">>
        <activity
            android:name=".activity.MainActivity"
            android:label="@string/app_name"
            android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".activity.ListColorsActivity"
            android:label="@string/listColorsActivityTitle"
            android:screenOrientation="portrait">
        </activity>
    </application>

</manifest>

puis on rajoute le titre de l’activité ListColorsActivity dans le fichier strings.xml :

strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Tuto Custom List</string>
    <string name="listColorsActivityTitle">Choix des couleurs</string>
</resources>

Nous venons de déclarer nos deux activités : MainActivity et ListColorsActivity, nous allons maintenant les créer.

Création de l’activité MainActivity

Pour commencer nous allons créer le package fr.infinitestudio.customlist.activity, et créer le fichier MainActivity.java ainsi que le layout activity_main.xml.

MainActivity.java

package fr.infinitestudio.customlist.activity;

import fr.infinitestudio.customlist.R;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends Activity implements OnClickListener{

	private static int REQUEST_CODE = 1;

	private TextView infosColorsSelected;
	private Button button;
	private Context context;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		context = this;

		infosColorsSelected = (TextView)findViewById(R.id.textInfos);
		button = (Button)findViewById(R.id.btnChooseColors);
		button.setOnClickListener(this);
	}

	@Override
	public void onClick(View paramView) {
		Intent intent = new Intent(context, ListColorsActivity.class);
		startActivityForResult(intent, REQUEST_CODE);
	}

	@Override
	public void onActivityResult(int requestCode, int resultCode, Intent data) {
		// On teste le retour de l'activité ListColorsActivity
		if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) {

		}
	}
}

Notre activité implémente l’interface OnClickListener qui nous permet d’implémenter la fonction onClick() que nous associons à notre bouton afin de déclencher l’activité ListColorsActivity. Pour lancer cette activité on utilise startActivityForResult() afin d’avoir un retour sur ce qui sera fait dans cette activité. L’appel à cette fonction nécessite un Intent ainsi qu’un code d’identification que nous avons appelé REQUEST_CODE.

La fonction onActivityResult() nous permettra de récupérer les couleurs sélectionnées dans la liste déroulante et d’afficher un message en conséquence.

Voici le code du layout activity_main.xml associé à l’activité MainActivity :

activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FFFFFF">

    <TextView
        android:id="@+id/textInfos"
        android:textColor="#000000"
        android:textStyle="bold"
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="@string/txtDefaultText"
        android:layout_margin="10dp"/>

    <Button
        android:id="@+id/btnChooseColors"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:text="@string/btnChooseColors"></Button>

</RelativeLayout>

Ce layout affichera simplement un texte centré verticalement et horizontalement dans l’écran ainsi qu’un bouton placé au bas de l’écran.

Et nous rajoutons les nouveaux textes :

strings.xml

<string name="btnChooseColors">Choix des couleurs</string>
<string name="txtDefaultText">Veuillez choisir vos couleurs préférées en cliquant sur le bouton</string>

Création de l’objet Color

Avant de s’attaquer à la création de la liste, nous allons créer un nouveau package fr.infinitestudio.customlist.model dans lequel nous allons ajouter la classe Color.

Color.java

package fr.infinitestudio.customlist.model;

public class Color {

	private Integer id;
	private String color;
	private String label;
	private boolean checked;

	public Color(){

	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((id == null) ? 0 : id.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Color other = (Color) obj;
		if (id == null) {
			if (other.id != null)
				return false;
		} else if (!id.equals(other.id))
			return false;
		return true;
	}

	public Integer getId() {
		return id;
	}
	public void setId(Integer id) {
		this.id = id;
	}
	public String getColor() {
		return color;
	}
	public void setColor(String color) {
		this.color = color;
	}
	public String getLabel() {
		return label;
	}
	public void setLabel(String label) {
		this.label = label;
	}
	public boolean isChecked() {
		return checked;
	}
	public void setChecked(boolean checked) {
		this.checked = checked;
	}
}

Voici les propriétés de notre objet :

  • id : identifiant unique d’un objet
  • color : la couleur stockée au format hexadécimal (#FFFFFF)
  • label : le nom de la couleur
  • checked : nous permet de savoir si la couleur a été sélectionnée ou non dans la liste

On implémente également les méthodes equals() et hashCode() pour pouvoir comparer deux objets Color.

Création de l’activité ListColorsActivity

Nous allons maintenant créer notre liste et voir comment l’afficher.

ListColorsActivity.java

package fr.infinitestudio.customlist.activity;

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

import android.app.Activity;
import android.app.ListActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import fr.infinitestudio.customlist.R;
import fr.infinitestudio.customlist.adapter.ListColorsAdapter;
import fr.infinitestudio.customlist.model.Color;

public class ListColorsActivity extends ListActivity implements OnItemClickListener{

	private List<Color> colors;
	private ArrayList<Color> colorsSelected;

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

		colorsSelected = new ArrayList<Color>();
		colors = createColors();

		setListAdapter(new ListColorsAdapter(this, R.layout.item_list, colors));
		getListView().setOnItemClickListener(this);
	}

	/**
	 * Fonction appelée lors de la sélection d'un item de la liste
	 * @param arg0
	 * @param view
	 * @param pos
	 * @param id
	 */
	@Override
	public void onItemClick(AdapterView<?> arg0, View view, int pos, long id) {

	}

	@Override
	public void onBackPressed() {
		// On revient sur l'activité MainActivity
		setResult(Activity.RESULT_OK);
		finish();
	}

	/**
	 * Fonction permettant de construire la liste des différentes couleurs
	 * @return
	 */
	private List<Color> createColors(){
		List<Color> colors = new ArrayList<Color>();
		colors.add(createColor(1, "#1abc9c", "Turquoise"));
		colors.add(createColor(2, "#2ecc71", "Emerald"));
		colors.add(createColor(3, "#3498db", "Peter River"));
		colors.add(createColor(4, "#9b59b6", "Amethyst"));
		colors.add(createColor(5, "#34495e", "Wet Asphal"));
		colors.add(createColor(6, "#16a085", "Green Sea"));
		colors.add(createColor(7, "#27ae60", "Nephritis"));
		colors.add(createColor(8, "#2980b9", "Belize Hole"));
		colors.add(createColor(9, "#8e44ad", "Wisteria"));
		colors.add(createColor(10, "#2c3e50", "Midnight Blue"));
		colors.add(createColor(11, "#f1c40f", "Sun Flower"));
		colors.add(createColor(12, "#e67e22", "Carrot"));
		colors.add(createColor(13, "#e74c3c", "Alizarin"));
		colors.add(createColor(14, "#ecf0f1", "Clouds"));
		colors.add(createColor(15, "#95a5a6", "Concrete"));
		colors.add(createColor(16, "#f39c12", "Orange"));
		colors.add(createColor(17, "#d35400", "Pumpkin"));
		colors.add(createColor(18, "#c0392b", "Pomegranate"));
		colors.add(createColor(19, "#bdc3c7", "Silver"));
		colors.add(createColor(20, "#7f8c8d", "Asbestos"));
		return colors;
	}

	/**
	 * Fonction permettant de créer un objet Color
	 * @param id
	 * @param color
	 * @param label
	 * @return
	 */
	private Color createColor(Integer id, String color, String label){
		Color c = new Color();
		c.setChecked(false);
		c.setColor(color);
		c.setId(id);
		c.setLabel(label);
		return c;
	}
}

Notre classe hérite de ListActivity et implémente l’interface OnItemClickListener pour que l’on puisse implémenter la fonction onItemClick() qui nous permettra de savoir quel item a été sélectionné.

Dans la fonction onCreate() on initialise une liste colorsSelected que l’on remplis grâce aux fonctions createColors() et createColor() puis on définit l’adapter à utiliser, dans notre cas ce sera un adapter personnalisé que nous verrons juste après. Le layout item_list.xml utilisé par l’adapter pour styliser les différents items de la liste, sera également vu plus bas.

On a également implémenté la méthode onBackPressed() qui nous permettra de revenir à l’activité principale lorsque l’on appuiera sur le bouton retour de notre appareil.

Voici maintenant le layout associé :

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >
    </ListView>

</RelativeLayout>

Ici rien de compliqué, simplement une liste.

Comme promis voici le layout utilisé pour afficher les items de la liste :

item_list.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <CheckBox
        android:id="@+id/checkColor"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:layout_gravity="center_vertical"
        android:focusableInTouchMode="false"
        android:focusable="false" />

    <TextView
        android:id="@+id/textColor"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="5dp"
        android:layout_toLeftOf="@id/checkColor"
        android:textSize="16sp" />

</RelativeLayout>

Pour l’instant on affiche simplement le nom de la couleur à gauche et la checkbox à droite.

Pour finir il nous faut créer l’adapter utilisé pour afficher la liste des couleurs donc on crée le package fr.infinitestudio.customlist.adapter ainsi que la classe ListColorsAdapter.java.

ListColorsAdapter.java

package fr.infinitestudio.customlist.adapter;

import java.util.List;

import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.TextView;
import fr.infinitestudio.customlist.R;
import fr.infinitestudio.customlist.model.Color;

public class ListColorsAdapter extends ArrayAdapter<Color> {

	private Context context;
	private int resource;

	public ListColorsAdapter(Context context, int textViewResourceId, List<Color> colors) {
		super(context, textViewResourceId, colors);
		resource = textViewResourceId;
		this.context = context;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {

		ColorsViewHolder colorsViewHolder;
		// Gestion des performances : on "inflate" et on recherche les composants si nécessaire
		if (convertView == null) {
			convertView = View.inflate(context, resource, null);

			colorsViewHolder = new ColorsViewHolder();
			colorsViewHolder.text = (TextView)convertView.findViewById(R.id.textColor);
			colorsViewHolder.check = (CheckBox)convertView.findViewById(R.id.checkColor);

			convertView.setTag(colorsViewHolder);
        } else {
        	colorsViewHolder = (ColorsViewHolder) convertView.getTag();
        }

		// On récupère notre objet Color
		Color color = getItem(position);
        if (color != null) {
        	colorsViewHolder.text.setText(color.getLabel());
        	colorsViewHolder.check.setChecked(color.isChecked());
        	colorsViewHolder.check.setTag(position);
        }
		return convertView;
	}

	/**
	 * Classe statique permettant d'améliorer les performances lors du parcours de la liste
	 *
	 */
	static class ColorsViewHolder {
		TextView text;
		CheckBox check;
    }
}

Il y a plusieurs choses importantes dans cet adapter. Tout d’abord il hérite de ArrayAdapter<Color> et on passe une liste de Color dans le constructeur. Ensuite plusieurs choses sont faites dans la fonction getView() et qu’il est important de respecter surtout lorsqu’on traite de gros volumes de données.

Lorsque l’on fait défiler la liste et qu’ un item disparaît de l’écran, sa vue est conservée dans un espace pour le recyclage et lorsque cet item réapparaît à l’écran et que la vue est présente dans l’espace de recyclage, alors on peut la réutiliser grâce au paramètre convertView sinon on la crée.

Une autre partie qui peut consommer beaucoup de mémoire, est l’appel à la méthode findViewById() et pour y remédier, nous avons implémenté le patron de conception “support” grâce à la classe statique ColorsViewHolder.

ListColorsAdapter.java

static class ColorsViewHolder {
		TextView text;
		CheckBox check;
    }

Cette classe représente les éléments de notre vue que l’on va associer une seule fois et que nous réutiliserons de la même manière que le convertView.

ListColorsAdapter.java

ColorsViewHolder colorsViewHolder;
// Gestion des performances : on "inflate" et on recherche les composants si nécessaire
if (convertView == null) {
	convertView = View.inflate(context, resource, null);

	colorsViewHolder = new ColorsViewHolder();
	colorsViewHolder.text = (TextView)convertView.findViewById(R.id.textColor);
	colorsViewHolder.check = (CheckBox)convertView.findViewById(R.id.checkColor);

	convertView.setTag(colorsViewHolder);
} else {
        colorsViewHolder = (ColorsViewHolder) convertView.getTag();
}

On teste si convertView est null, si c’est le cas on va simplement créer la vue et initialiser notre objet colorsViewHolder en chargeant les différents composants de notre vue à savoir le champ texte et la checkbox.

On appelle ensuite la méthode setTag() de notre vue convertView pour lui associer l’objet colorsViewHolder afin que l’on puisse le réutiliser plus tard.

Si convertView n’est pas null, on récupère simplement notre objet colorsViewHolder :

ListColorsAdapter.java

colorsViewHolder = (ColorsViewHolder) convertView.getTag();

Et enfin on récupère notre objet Color qui nous servira à initialiser le texte et la checkbox de notre liste.

// On récupère notre objet Color
Color color = getItem(position);
if (color != null) {
        colorsViewHolder.text.setText(color.getLabel());
        colorsViewHolder.check.setChecked(color.isChecked());
        colorsViewHolder.check.setTag(position);
}

On stocke également la position dans la propriété tag de la checkbox ce qui  nous permettra de récupérer sa position dans la liste.

Premier test de l’application

Vous pouvez déjà tester l’application et vous devriez obtenir ceci :

Accueil Sélection des couleurs

Nous venons de créer notre liste, de l’alimenter avec des données et de créer notre propre adapter. Nous allons maintenant voir comment récupérer les couleurs sélectionnées dans la liste pour les afficher sur l’écran d’accueil.

Récupération des couleurs sélectionnées

Tout d’abord nous allons associer une fonction à l’événement onClick des checkbox lorsque l’on cliquera dessus, ce qui nous permettra d’ajouter ou de retirer une couleur de la liste des couleurs sélectionnées.

item_list.xml

<CheckBox
        android:id="@+id/checkColor"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:onClick="checkboxHandler"
        android:layout_gravity="center_vertical"
        android:focusableInTouchMode="false"
        android:focusable="false" />

Il nous faut alors créer la fonction checkboxHandler() :

ListColorsActivity.java

/**
 * Fonction appelée au clic d'une des checkbox
 * @param v
 */
public void checkboxHandler(View v) {
	CheckBox cb = (CheckBox) v;
	// On récupère la position à l'aide du tag défini dans l'adapter ListColorsAdapter
	int position = Integer.parseInt(cb.getTag().toString());

	// On récupère l'élément Color sur lequel on se trouve
	Color color = (Color)getListView().getItemAtPosition(position);
	color.setChecked(cb.isChecked());

	addColorSelected(color);
}

/**
 * Fonction qui ajoute ou retire une couleur de la liste des couleurs sélectionnées
 * @param color
 */
private void addColorSelected(Color color){
	if(colorsSelected.contains(color) && !color.isChecked()){
		colorsSelected.remove(color);
	}else if(!colorsSelected.contains(color) && color.isChecked()){
		colorsSelected.add(color);
	}
}

On récupère la position de la checkbox que l’on a stockée dans l’attribut tag, ce qui va nous permettre de récupérer notre objet Color pour mettre à jour sa propriété checked et l’ajouter ou non à la liste des couleurs sélectionnées (ce traitement est fait grâce à la fonction addColorSelected()).

Maintenant que l’on récupère les couleurs sélectionnées, il faut qu’on les passe à l’activité principale lorsqu’on clique sur le bouton retour.

Par contre nous avons un problème qui est, qu’en l’état, nous ne pouvons pas envoyer notre liste d’objets Color dans l’Intent. Pour remédier à ça il nous faut implémenter l’interface Parcelable dans notre objet Color.

Voyons comment faire.

Mise à jour de la classe Color

Pour commencer on implémente l’interface Parcelable :

Color.java

public class Color implements Parcelable

Puis on implémente les fonctions associées :

Color.java

@Override
public int describeContents() {
	return 0;
}

@Override
public void writeToParcel(Parcel paramParcel, int paramInt) {
	paramParcel.writeInt(id);
	paramParcel.writeString(color);
	paramParcel.writeString(label);
	paramParcel.writeInt(checked ? 1:0 );
}

L’objet Parcel ne contient pas de fonction permettant d’écrire des booleéns, on transforme alors la propriété checked en entier.

Enfin nous allons créer un champ statique pour lequel la fonction createFromParcel() sera appelée lorsque nous voudrons récupérer notre liste d’objets Color dans notre activité principale, qui elle même appellera le constructeur Color(parcel pc) que nous allons définir :

Color.java

/**
 * Constructeur qui lit les champs de l'objet Parcel dans l'ordre dans lequel ils ont été écrits dans la fonction writeToParcel
 * @param pc
 */
public Color(Parcel pc){
	id = pc.readInt();
	color = pc.readString();
	label = pc.readString();
	checked = (pc.readInt() == 1);
}

/**
 * Champ statique utilisé pour régénérer l'objet individuellement ou sous forme de tableau
 */
public static final Parcelable.Creator<Color> CREATOR = new Parcelable.Creator<Color>() {
	public Color createFromParcel(Parcel pc) {
		return new Color(pc);
	}
	public Color[] newArray(int size) {
		return new Color[size];
	}
};

Affichage des couleurs sélectionnées

Maintenant nous allons pouvoir afficher les couleurs sélectionnées dans notre activité principale.

Tout d’abord il nous faut les envoyer lorsque l’on ferme la liste des couleurs :

ListColorsActivity.java

private static String EXTRA_COLORS_SELECTED = "EXTRA_COLORS_SELECTED";

@Override
public void onBackPressed() {
	// On revient sur l'activité MainActivity en lui envoyant la liste des couleurs sélectionnées
	Intent intent = new Intent();
	intent.putParcelableArrayListExtra(EXTRA_COLORS_SELECTED, colorsSelected);
	setResult(Activity.RESULT_OK, intent);
	finish();
}

On initialise ensuite notre liste des couleurs sélectionnées :

MainActivity.java

private ArrayList<Color> colorsSelected;

@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_main);
	context = this;

	colorsSelected = new ArrayList<Color>();

	infosColorsSelected = (TextView)findViewById(R.id.textInfos);
	button = (Button)findViewById(R.id.btnChooseColors);
	button.setOnClickListener(this);
}

puis on récupère la liste reçue de l’activité ListColorsActivity et on construit un message en fonction des couleurs sélectionnées :

MainActivity.java

private static String EXTRA_COLORS_SELECTED = "EXTRA_COLORS_SELECTED";

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
	// On teste le retour de l'activité ListColorsActivity
	if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) {
		// On construit le message à afficher suivant les couleurs sélectionnées
		colorsSelected = data.getParcelableArrayListExtra(EXTRA_COLORS_SELECTED);
		if(colorsSelected.isEmpty()){
			infosColorsSelected.setText(R.string.emptyColors);
		}else{
			StringBuffer buffer = new StringBuffer();
			for(int i = 0; i < colorsSelected.size(); i++){
				if(i != 0){
					buffer.append(", ");
				}
				buffer.append(colorsSelected.get(i).getLabel());
			}
			infosColorsSelected.setText(String.format(getResources().getString(R.string.colorsSelected), buffer));
		}
	}
}

On n’oublie pas de rajouter les nouveaux textes :

strings.xml

<string name="emptyColors">Aucune couleur n'a été sélectionnée</string>
<string name="colorsSelected">Couleurs sélectionnées&#160;:&#160;%1$s</string>

Si vous lancez l’application, que vous cliquez sur plusieurs checkbox et que vous revenez sur l’activité principale vous devriez voir un message du genre : “Couleurs sélectionnées : Turquoise, Emerald, Peter River” et le message “Aucune couleur n’a été sélectionnée” si aucune sélection n’a été faite.

Après avoir sélectionné des couleurs et être revenu sur la page d’accueil, si vous cliquez à nouveau sur le bouton “Choix des couleurs” la sélection est perdue et plus aucune couleur n’est sélectionnée.

Nous allons y remédier tout de suite.

Garder la sélection des couleurs

Pour garder la sélection des couleurs, il nous suffit de faire la même chose que nous venons de faire c’est à dire renvoyer la liste des couleurs sélectionnées à l’activité ListColorsActivity pour qu’elle puisse savoir quelles couleurs ont déjà été sélectionnées.

Tout d’abord il nous faut envoyer la liste lorsque la fonction onClick() est déclenchée :

MainActivity.java

@Override
public void onClick(View paramView) {
	// On appelle l'activité ListColorsActivity en lui envoyant la liste des couleurs sélectionnées précédemment
	Intent intent = new Intent(context, ListColorsActivity.class);
	intent.putParcelableArrayListExtra(EXTRA_COLORS_SELECTED, colorsSelected);
	startActivityForResult(intent, REQUEST_CODE);
}

dans la fonction onCreate() de l’activité ListColorsActivity on récupère la liste envoyée depuis MainActivity :

ListColorsActivity.java

colorsSelected = getIntent().getParcelableArrayListExtra(EXTRA_COLORS_SELECTED);

et on modifie la fonction createColors() pour qu’elle prenne en paramètre la liste des couleurs sélectionnées :

ListColorsActivity.java

/**
 * Fonction permettant de construire la liste des différentes couleurs
 * @param colorsSelected
 * @return
 */
private List<Color> createColors(List<Color> colorsSelected){
	List<Color> colors = new ArrayList<Color>();
	colors.add(createColor(1, "#1abc9c", "Turquoise"));
	colors.add(createColor(2, "#2ecc71", "Emerald"));
	colors.add(createColor(3, "#3498db", "Peter River"));
	colors.add(createColor(4, "#9b59b6", "Amethyst"));
	colors.add(createColor(5, "#34495e", "Wet Asphal"));
	colors.add(createColor(6, "#16a085", "Green Sea"));
	colors.add(createColor(7, "#27ae60", "Nephritis"));
	colors.add(createColor(8, "#2980b9", "Belize Hole"));
	colors.add(createColor(9, "#8e44ad", "Wisteria"));
	colors.add(createColor(10, "#2c3e50", "Midnight Blue"));
	colors.add(createColor(11, "#f1c40f", "Sun Flower"));
	colors.add(createColor(12, "#e67e22", "Carrot"));
	colors.add(createColor(13, "#e74c3c", "Alizarin"));
	colors.add(createColor(14, "#ecf0f1", "Clouds"));
	colors.add(createColor(15, "#95a5a6", "Concrete"));
	colors.add(createColor(16, "#f39c12", "Orange"));
	colors.add(createColor(17, "#d35400", "Pumpkin"));
	colors.add(createColor(18, "#c0392b", "Pomegranate"));
	colors.add(createColor(19, "#bdc3c7", "Silver"));
	colors.add(createColor(20, "#7f8c8d", "Asbestos"));

	for(Color colorSelected : colorsSelected){
		for(Color color : colors){
			if(colorSelected.equals(color)){
				color.setChecked(true);
				break;
			}
		}
	}
	return colors;
}

et on modifie l’appel de cette fonction dans la fonction onCreate() :

ListColorsActivity.java

colors = createColors(colorsSelected);

Relancez l’application et vous devriez voir que la sélection que vous avez faite est bien sauvegardée.

Petits plus

Nous allons rajouter quelques petites choses pour rendre l’application plus agréable.

Sélection de la checkbox lors du clic sur l’item

Si vous appuyez sur l’item et non pas sur la checkbox la liste se ferme. Ce serait quand même mieux que lorsqu’on appui sur l’item on agisse directement sur la checkbox. Il nous faut modifier la fonction onItemClick() :

ListColorsActivity.java

/**
 * Fonction appelée lors de la sélection d'un item de la liste
 * @param arg0
 * @param view
 * @param pos
 * @param id
 */
@Override
public void onItemClick(AdapterView<?> arg0, View view, int pos, long id) {
	// On coche la checkbox associée à l'élément cliqué
	CheckBox cb = (CheckBox)getListView().getChildAt(pos - getListView().getFirstVisiblePosition()).findViewById(R.id.checkColor);
	cb.toggle();

	// On récupère l'élément Color sur lequel on se trouve
	Color color = (Color)getListView().getItemAtPosition(pos);
	color.setChecked(cb.isChecked());

	addColorSelected(color);
}

Affichage de la liste sous la forme d’une boite de dialogue

Pour l’instant notre liste s’affiche en plein écran. Ce serait plus sympa si elle s’affichait comme une boite de dialogue ou comme une popup.

values/styles.xml

<resources>
    <!--
        Base application theme, dependent on API level. This theme is replaced
        by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
    -->
    <style name="AppBaseTheme" parent="android:Theme.Light">
        <!--
            Theme customizations available in newer API levels can go in
            res/values-vXX/styles.xml, while customizations related to
            backward-compatibility can go here.
        -->
    </style>

    <!-- Application theme. -->
    <style name="AppTheme" parent="AppBaseTheme">
        <!-- All customizations that are NOT specific to a particular API-level can go here. -->
    </style>

    <style name="ThemeDialogSelector" parent="@android:style/Theme.Dialog">
    </style>
</resources>

values-v11/styles.xml

<resources>
    <!--
        Base application theme for API 11+. This theme completely replaces
        AppBaseTheme from res/values/styles.xml on API 11+ devices.
    -->
    <style name="AppBaseTheme" parent="android:Theme.Holo.Light">
        <!-- API 11 theme customizations can go here. -->
    </style>

    <style name="ThemeDialogSelector" parent="@android:style/Theme.Holo.Light.Dialog">
    </style>
</resources>

On vient de créer le thème ThemeDialogSelector que nous allons simplement appliquer à notre activité ListColorActivity :

AndroidManifest.xml

<activity
	android:name=".activity.ListColorsActivity"
	android:label="@string/listColorsActivityTitle"
	android:screenOrientation="portrait"
	android:theme="@style/ThemeDialogSelector">
</activity>

Voici la différence entre avant et après l’application du thème :

Sélection des couleurs style dialogue Sélection des couleurs sans style

Ajout d’un rectangle de couleur

Pour terminer nous allons ajouter un petit rectangle de couleur en haut à gauche de chaque item de la liste pour identifier plus facilement chaque couleur.

Pour faire ça on va créer notre propre composant. On commence par créer un nouveau package fr.infinitestudio.customlist.view dans lequel on ajoute la classe RectangleView.

RectangleView.java

package fr.infinitestudio.customlist.view;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;

public class RectangleView extends View{

	private Paint paint = new Paint();
	private String hexaColor;

    public RectangleView(Context context) {
        super(context);
    }

    public RectangleView (Context context, AttributeSet attrs) {
    	super(context, attrs);
    }

    @SuppressLint("DrawAllocation")
	@Override
    public void onDraw(Canvas canvas) {
    	// On dessine un rectangle avec un fond de couleur uni défini par hexaColor
        paint.setColor(Color.parseColor(hexaColor));

        Rect rect = new Rect(0, 0, getWidth(), getHeight());
        canvas.drawRect(rect, paint);
    }

	public void setHexaColor(String hexaColor) {
		this.hexaColor = hexaColor;
	}
}

Notre composant hérite de la classe View et dans la méthode onDraw() on remplit simplement toute la surface avec la couleur hexadécimale contenue dans la propriété hexaColor.

On intègre ce composant dans le layout item_list.xml de manière à le placer en haut à gauche de l’item :

item_list.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <fr.infinitestudio.customlist.view.RectangleView
        android:id="@+id/rectangleColor"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_width="50dp"
        android:layout_height="6dp" />

    <RelativeLayout
	    android:layout_width="match_parent"
	    android:layout_height="wrap_content"
	    android:padding="10dp">
	    <CheckBox
	        android:id="@+id/checkColor"
	        android:layout_width="wrap_content"
	        android:layout_height="wrap_content"
	        android:layout_alignParentRight="true"
	        android:layout_centerVertical="true"
	        android:layout_gravity="center_vertical"
	        android:onClick="checkboxHandler"
	        android:focusableInTouchMode="false"
	        android:focusable="false" />

	    <TextView
	        android:id="@+id/textColor"
	        android:layout_width="match_parent"
	        android:layout_height="wrap_content"
	        android:layout_centerVertical="true"
	        android:layout_gravity="center_vertical"
	        android:layout_marginLeft="5dp"
	        android:layout_toLeftOf="@id/checkColor"
	        android:textSize="16sp" />
	   </RelativeLayout>

</RelativeLayout>

Enfin on met à jour notre adapter pour prendre en compte cette nouvelle donnée :

ListColorsAdapter.java

@Override
public View getView(int position, View convertView, ViewGroup parent) {

	ColorsViewHolder colorsViewHolder;
	// Gestion des performances : on "inflate" et on recherche les composants si nécessaire
	if (convertView == null) {
		convertView = View.inflate(context, resource, null);

		colorsViewHolder = new ColorsViewHolder();
		colorsViewHolder.text = (TextView)convertView.findViewById(R.id.textColor);
		colorsViewHolder.check = (CheckBox)convertView.findViewById(R.id.checkColor);
		colorsViewHolder.rectangle = (RectangleView)convertView.findViewById(R.id.rectangleColor);

		convertView.setTag(colorsViewHolder);
	} else {
		colorsViewHolder = (ColorsViewHolder) convertView.getTag();
	}

	// On récupère notre objet Color
	Color color = getItem(position);
	if (color != null) {
		colorsViewHolder.text.setText(color.getLabel());
		colorsViewHolder.check.setChecked(color.isChecked());
		colorsViewHolder.check.setTag(position);
		colorsViewHolder.rectangle.setHexaColor(color.getColor());
	}
	return convertView;
}

/**
 * Classe statique permettant d'améliorer les performances lors du parcours de la liste
 *
 */
static class ColorsViewHolder {
	TextView text;
	CheckBox check;
	RectangleView rectangle;
}

Vous pouvez maintenant lancer l’application et vous devriez obtenir la même chose que je vous ai présenté au début de l’article :

Sélection des couleurs

Conclusion

Nous avons vu comment créer une liste personnalisée pour une application Android mais également quelques petites choses en plus :

  • la création et l’optimisation d’un adapter personnalisé
  • le passage d’une liste d’objets dans un Intent en implémentant l’interface Parcelable
  • la création d’un composant

Vous trouverez les sources de l’application au bas de cet article et également sur GitHub : https://github.com/Infinite-Studio/tuto-android-custom-list.

Si vous avez des remarques ou des questions n’hésitez pas.


[attachments style=”medium”]

Partager cet article

Commentaires (5)

  • DocBrown Répondre

    Excellent !!
    Tuto super clair et qui va bien m’aider à progresser ! Merci beaucoup !

    28 janvier 2015 - 19 h 35 min
  • Infinite Studio Répondre

    @DocBrown Merci, n’hésite à poser des questions si tu en a.

    29 janvier 2015 - 9 h 36 min
  • kamen Répondre

    Exercice très intéressant.
    Cependant je remarque un comportement inattendu. Quand j’ouvre la liste, que je clique pile poil sur la checkbox d’une ou plusieurs couleurs et que je sors de la liste, les couleurs sélectionnées ne s’affichent pas. Avez vous la même chose?

    22 février 2015 - 19 h 14 min
  • kamen Répondre

    J’ai trouvé la solution, dans item_list.xml, mettre la ligne suivante dans la checkbox:
    android:clickable=”false”

    24 février 2015 - 14 h 31 min
  • Infinite Studio Répondre

    @kamen Merci pour l’info 😉

    24 février 2015 - 14 h 41 min

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *