Gérer des événements dans une listeview (màj).

Gérer des événements dans une listeview (màj).

Bonjour à tous!

J’ai appris une nouvelle façon, plus facile, plus propre surtout, et plus « avantageuse » de gérer plusieurs événements sur une ligne dans une liste.

Souvenez-vous, j’avais réalisé et utilisé une classe qui nous gérait nos actions, aujourd’hui, nous utiliserons que le système Androïd et ses propres outils !

public class TutoAdapterActivity extends Activity {

    private ListView listview;
    private ArrayList<BeanRow> grpBr;
    private OnClickListener onClick;
    private MySecondeAdapter adapter;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        grpBr = new ArrayList<BeanRow>();
        BeanRow br1 = new BeanRow("Laurent", "Fritte7", false, 0);
        BeanRow br2 = new BeanRow("Benjamin", "Throrin's", false, 1);
        grpBr.add(br1);
        grpBr.add(br2);

        listview = (ListView) findViewById(R.id.listView);
        //MyAdapter adapter = new MyAdapter(getApplicationContext(), R.layout.row_tourne, grpBr);
        onClick = onClick();
        adapter = new MySecondeAdapter(getApplicationContext(), R.layout.row_tourne, grpBr, onClick);
        listview.setAdapter(adapter);
    }
}

Je n’ai rien modifié dans mon objet (liste) et mes actions seront toujours les mêmes ! Ainsi que ma vue, ce n’est que l’adapter qui changera. Tout d’abord, le constructeur change un peu, nous lui donnons un listener, que nous verrons plus tard. Allons de suite dans le nouvel adapter.

public class MySecondeAdapter extends BaseAdapter {

    private ArrayList<BeanRow> liste;
    private OnClickListener click;
    private LayoutInflater inflater;
    private int idView;
    private Holder holder;
    private Context ct;

    public MySecondeAdapter(Context applicationContext, int rowTourne,
            ArrayList<BeanRow> grpBr, OnClickListener onClick) {
        ct = applicationContext;
        liste = grpBr;
        click = onClick;
        inflater = LayoutInflater.from(applicationContext);
        idView = rowTourne;
    }

    /**
     * Ici on recupere la vue ou on la cree
     */
    public View getView(int position, View view, ViewGroup parent) {
        if (view == null) {
            holder = new Holder();
            view = inflater.inflate(idView, null);
            holder.rowIcon = (ImageView) view.findViewById(R.id.iconRow);
            holder.rowPrenom = (TextView) view.findViewById(R.id.prenomTextView);
            holder.rowPseudo = (TextView) view.findViewById(R.id.pseudoTextView);
            holder.rowAction = (ImageButton) view.findViewById(R.id.imageAction);
            holder.rowCheck = (CheckBox) view.findViewById(R.id.checkBoxRow);

            /** la vue etant cree, nous rajoutons le listener */
            holder.rowAction.setOnClickListener(click);
            holder.rowCheck.setOnClickListener(click);
            holder.rowPrenom.setOnClickListener(click);
            view.setTag(holder);
        } else {
            holder = (Holder) view.getTag();
        }
        /** je donne la position a holder, plus facile a recuperer dans le listener */
        holder.position = position;
        holder.rowPrenom.setText(liste.get(position).getPrenom());
        holder.rowPseudo.setText(liste.get(position).getPseudo());
        holder.rowCheck.setChecked(liste.get(position).isCheck());
        holder.rowIcon.setImageDrawable(switchIcon(liste.get(position).getPos()));

        boolean check = holder.rowCheck.isChecked();
        /** on redessine l'animation si coché = grisé ! */
        if (check) {
            AlphaAnimation anim = new AlphaAnimation(1, 0.2f);
            anim.setDuration(0);
            anim.setFillAfter(true);
            view.startAnimation(anim);
        } else {
            AlphaAnimation anim = new AlphaAnimation(0.2f, 1);
            anim.setDuration(0);
            anim.setFillAfter(true);
            view.startAnimation(anim);
        }

        return view;
    }

    private Drawable switchIcon(int pos) {
        Drawable icon = null;
        switch (pos) {
        case 0:
            icon = ct.getResources().getDrawable(R.drawable.labo_48x48);
            break;
        case 1:
            icon = ct.getResources().getDrawable(R.drawable.user_48x48);
            break;
        }
        return icon;
    }

    public int getCount() {
        return liste.size();
    }

    public BeanRow getItem(int position) {
        return liste.get(position);
    }

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

} 

J’ai commenté le code, pour plus de compréhension, mais nous voyons, qu’il créé une vue sur base d’une classe qui est un « mapping de l’xml » tout simplement, et nous lui donnons comme tag, l’objet en lui-même, sans oublier la position quand on récupère la vue (cela facilite la récupération dans la liste).

Nous définissons les champs qui ont besoin du listener et nous définissons une règle simple sur le checkbox, s’il est coché, on le grise, sinon on fait rien. C’est tout pour l’adapter, les événements sont gérés dans l’activity (ou une classe encore a part si on veut avoir un modèle MVC).

Voici les actions, nous constatons que je fais un switch, sur les id (de l’xml) des vues.

/**
 * Un simle listener qui gère tous les evenements de votre vue.
 * @return
 */
private OnClickListener onClick() {
    OnClickListener event = new OnClickListener() {
        public void onClick(View v) {
            View parent;
            Holder holder;

            switch (v.getId()) {

                /** Ici nous devons remonter dans la hiérarchie de la vue (dans l'xml) */
                case R.id.imageAction:
                    /** v = imagebouton */
                    parent = (View) v.getParent();
                    /** parent = le linearlayout */
                    parent = (View) parent.getParent();
                    /** parent = la vue xml qui a le Holder en tag */
                    holder = (Holder) parent.getTag();
                    Toast.makeText(getApplicationContext(), "Le pseudo de la ligne: " + grpBr.get(holder.position).getPseudo(), Toast.LENGTH_SHORT).show();
                    break;

                case R.id.checkBoxRow:
                    /** v = le checkbox */
                    parent = (View) v.getParent();
                    /** parent = la vue xml qui a le Holder en tag */
                    holder = (Holder) parent.getTag();
                    CheckBox check = (CheckBox) v;
                    if (check.isChecked()) {
                        grpBr.get(holder.position).setCheck(true);
                    } else {
                        grpBr.get(holder.position).setCheck(false);
                    }
                    /** on notifie le changement de la liste pour que le check se met à jour */
                    adapter.notifyDataSetChanged();
                    break;

                case R.id.prenomTextView:
                    /** v = textview */
                    parent = (View) v.getParent();
                    /** parent = linearlayout */
                    parent = (View) parent.getParent();
                    /** parent(écrasé) = la vue xml qui a le tag Holder */
                    holder = (Holder) parent.getTag();
                    Toast.makeText(getApplicationContext(), "Le prenom de la ligne: " + grpBr.get(holder.position).getPrenom(), Toast.LENGTH_SHORT).show();
                    break;
            }
        }
    };
    return event;
}

Étant donné que lorsqu’on fait view.getId(), nous récupérons l’id de la vue ainsi, nous pouvons tester la valeur et définir nos événements. Ensuite, il faut suivre la hiérarchie de votre vue xml, c’est pourquoi je fais des view.getParent(), c’est pour monter dans la hiérarchie.

Je peux ainsi, remonter à la base, et récupérer mon tag, qui n’est autre que l’objet Holder de l’adapter. On récupère les données et la position sans problème, et nous lançons les événements.

Voilà, j’espère avoir été plus clair que la dernière fois, et que vous préfériez cette méthode-ci. Je vous ai ré-uploadé sur code.google le projet complet, avec les deux adapters.

Le thème Holo pour tous

Le thème Holo pour tous

Aujourd’hui, je ne vais pas vous faire un tutoriel pour profiter du thème Holo dans vos applications quand vous êtes sur de vielles versions mais d’une library faisant exactement ceci.

Je viens de l’essayer et ma fois, après quelques réécritures minimes (surcharges de thèmes perso et surcharge des Dialogs) je peux vous dire qu’elle marche très très bien.

Cette library s’appelle HoloEverywhere et elle est disponible sur ce GitHub.

Je ne vais pas entrer dans les détails d’installation, ils l’expliquent très bien. Je vais juste vous montrer que, ça marche très bien même si je note de petites différences avec le thème Holo original.

Seul gros bémol, la taille supplémentaire que prendra votre application à la suite de l’inclusion de cette library : comptez entre 400Ko et 1Mo de plus d’espace pris lorsque votre application est installée sur un smartphone. Cette taille s’explique car, pour pouvoir reproduire ce thème, ils ont dû intégrer les images de chaque élément graphique utilisés par Android.

Voici un exemple d’applis que j’ai fait. Il s’agit d’un questionnaire tout bête avec pleins de champs les uns à la suite des autres. La première image montre l’aperçu sur Android 4.0.3 et le second ce que ça donne, sans HoloEverywhere, sur Android 2.1. Le dernier screen quand à lui montre le résultat sur Android 2.1 avec la library. Bluffant n’est-ce pas ? 

Formulaire sous Android ICS

Aperçu sous Android 2.1 sans HoloEverywhere

Aperçu sur Android Eclair avec la library

AsyncTasks s’attendants

AsyncTasks s’attendants

Difficile de trouver un nom adéquat pour cet article.

En fait, je vais vous donner un conseil utile pour exécuter deux AsyncTasks en parallèle et qu’elles s’attendent pour continuer. Si vous ne le savez pas, les AsyncTasks sont des classes d’exécution de code en arrière-plan. Elles sont utilisées sur Android pour récupérer des données du WEB, faire des calculs, … et tout ça, sans gêner le Thread d’affichage.

De plus, vous pouvez effectuer des traitements sur le Thread d’affichage juste avant et juste après le traitement asynchrone très simplement. D’ailleurs la documentation Android privilégie l’utilisation des AsyncTasks en lieu et place des Threads.

Bon, revenons à nos moutons. Exécuter deux AsyncTasks en parallèle et attendre que les deux se terminent pour effectuer le traitement final. En fait, nous  allons lancer la seconde AsyncTasks dans le PreExecute() de la première et effectuer le traitement final dans le PostExecute de la première aussi. Nous attendrons que la seconde finisse dans le traitement asynchrone de la première afin d’éviter tout gel du Thread d’affichage.

Si vous m’avez suivi, voici le squelette java que nous devrions avoir :

private TaskA taskA;
private TaskB taskB;

class TaskA extends AsyncTask<Void, Void, Void>{

   @Override
   protected void onPreExecute(){
       // Ici, on lance notre seconde Task
       taskB = new TaskB();
       taskB.execute();
   }

   @Override
   protected Void doInBackground(Void... params){
       // Traitements asynchrones de la Task A
       // ...
       // Fin des traitements de la Task A

       // On attends le résultat de B
       try{
           taskB.get();
       } catch(ExecutionException e) {
           e.printStackTrace();
       } catch(InterruptedException e) {
           e.printStackTrace();
       }

       return null;
   }

   @Override
   protected void onPostExecute(Void result){
       // Le traitement final
       // Ici, vous êtes dans l'UI Thread
   }
}

class TaskB extends AsyncTask<Void, Void, Void>{

   @Override
   protected Void doInBackground(Void... params){
       // Traitements asynchrones de la Task B
       // ...
       // Fin des traitements de la Task B

       return null;
   }
}

La partie qui permet d’attendre que l’autre asynctask finisse est juste la fonction get() des AsyncTask. Elle évite de passer par les Threads et de faire le couple wait()/notify().

De plus elle lève certaines exceptions si jamais la tâche que vous attendez a été annulée ou a eu une erreur d’exécution entre autre. Et pour exécuter ces deux tâches vous avez juste à appeler cette ligne dans votre code :

taskA = new TaskA();
taskA.execute();

Bien entendu, vous pouvez étendre ce cas pour en effectuer X en parallèle. Je pense avoir fini ma partie conseil pour les AsyncTasks. Si vous avez des questions, suggestions, corrections, n’hésitez surtout pas à poster un commentaire pour que j’y réponde.

Gérer des évènements dans une listview.

Gérer des évènements dans une listview.

Bonjour à tous, ceci est mon premier tuto-conseil soyez indulgent :D.

Je vais vous présenter un adapter que j’utilise, assez souvent, dans Androïd. Tout d’abord voici une classe abstraite, qui n’est autre qu’un adapter qui étends BaseAdapter, et qui va nous aider a implémenter, facilement des événements dans notre liste. ClickableListAdapter.java

Je vois parfois des codes qui utilise, pour une liste, le onItemClickListener, donc l’action ne s’arrête qu’au simple click d’une ligne, alors que nous pourrions avoir plusieurs boutons, textview et autres interactivité sur un seul champ.

Personnellement voici ce que j’obtiens, sur une seule ligne :

[]1

La vue de chaque ligne avec leurs évènements propres.

J’ai donc deux actions, une checbkox et une image qui ouvre une quickaction, remplacé dans le tuto par un Toast. J’aurais pu rajouter plus, mais étant limité sur la largeur, au lieu de mettre plusieurs image-button, nous avons opté pour une quickaction qui est un semblant de pop-up.

Cela m’évite de retoucher ma vue et vu que nous rajoutions au fur et à mesure des actions possibles, c’était plus facile. Mais je m’égare, cela sera peut-être un prochain tuto, si vous me le demandez, revenons à nos adapter.

Explications:

Que fait la classe ClickableAdapter, c’est très simple, elle créé la vue de chaque ligne dans la liste, et nous la retourne. Tout ce que vous avez à faire, est d’étendre cette classe, et de définir votre classe Holder qui étendra ViewHolder. La classe Holder, n’est autre que les champs (textview, imageview, checkbox) que vous avez dans votre vue xml, qui défini chaque ligne. Nous créons notre Adapter personnel qui étends ClickableAdapter et nous nous retrouvons avec deux méthodes et un constructeur.

class MyAdapter extends ClickableListAdapter {

    public MyAdapter(Context context, int viewid, List objects) {
        super(context, viewid, objects);
        // TODO Auto−generated constructor stub
    }

    @Override
    protected ViewHolder createHolder(View v) {
        // TODO Auto−generated method stub
        return null;
    }

    @Override
    protected void bindHolder(ViewHolder h) {
        // TODO Auto−generated method stub

    }
}

Je récupère la taille de l’objet passé en paramètre, pour créer un tableau de boolean. Pourquoi me direz-vous, tout simplement parce que j’ai trouvé des petits problèmes de récupération de position dans une liste où nous devons scroller.

Il check des checkbox aléatoirement à la création/recréation de ligne et c’est très embêtant. J’ai trouvé comme solution, de créer un tableau de boolean, qui récupère la position de l’objet de la ligne (un int position dans l’objet bean lui même) et je check alors s’il est à vrai ou faux. À l’action check, il défini la position du tableau à vrai, et donc quand il recréera la ligne, si vous avez scrollé, il redéfinira correctement à vrai le checkbox.

int nbre = objects.size();
checkbox = new boolean[nbre];
for (int i = 0; i < nbre; i++) {
    checkbox[i] = false;
}

La méthode bindHolder, n’est que le remplissage de donnée, dans la vue elle-même. En conséquent, la méthode createHolder, nous créé la vue. Vu qu’elle est créé dynamiquement, toutes les actions seront les mêmes pour tous les lignes, il suffirait de rajouter des if ou un switch qui détecterait la position de l’objet dans la vue, avec la variable position, pour définir des actions différentes dans les évènements.

J’ai rajouté dans le onCheck la petite subtilité qui gère notre tableau de booleans, ainsi qu’une légère animation sur la ligne elle-même.

@Override
 public void onCheck(CompoundButton buttonView, boolean isChecked,
 ViewHolder mViewHolder) {
     RowHolder rh = (RowHolder) mViewHolder;
     br = (BeanRow) rh.data;

     checkbox[br.pos] = buttonView.isChecked();

     if (isChecked) {

         rh.rowAction.setEnabled(!checkbox[br.pos]);
         AlphaAnimation anim = new AlphaAnimation(1, 0.2f);
         anim.setDuration(0);
         anim.setFillAfter(true);
         v.startAnimation(anim);
     } else {

         rh.rowAction.setEnabled(!checkbox[br.pos]);
         AlphaAnimation anim = new AlphaAnimation(0.2f, 1);
         anim.setDuration(0);
         anim.setFillAfter(true);
         v.startAnimation(anim);
     }
}

N’oubliez pas que les onClick/onCheck et onLongClick sont ceux de la classe ClickableListAdapter, et non pas ceux proposé par Androïd, sinon vous ne récupéreriez pas la vue de votre ligne et donc, vous aurez une erreur. Voilà, j’espère avoir été clair. Je vous ai uploadé sur code.google le petit projet tuto, il sera surement plus facile a comprendre :-).

Bonne journée.

Arrivée d’un nouveau Contributeur

Arrivée d’un nouveau Contributeur

Bonjour,

Vous êtes chaque jours de plus en plus à lire mon blog tourné autour du développement Informatique et je vous en remercie.

Je sais que sur certains articles  je ne m’explique pas comme il faut et que je suis encore hésitant sur certains choix et explications. Cela fait maintenant 3 ans que j’utilises la technologie Zend au quotidien et 1an que je développe sous Android. Mais, malgré cette expérience acquise, je ne m’estime pas être un expert en la matière. Je préfère faire des articles parlant des parties où j’ai moi même eu des obstacles pour arriver à mes fins. Et là encore, je ne peux pas tout partager avec vous.

Afin d’ammener une touche d’objectivité à ce blog et que vous puissiez voir d’autres façons de faire et de penser, un nouvel auteur va faire son apparition sur mon blog. Ce contributeur travaille tous les jours sur Android et va vous donner certains conseilles pour bien développer dessus.

Il viendra de temps en temps (environ 1 fois par mois) vous donner de ses précieux conseils.

Sur ce, je vous souhaite un bon Pont du Premier Mai malgré la pluie.