Cyril Mottier

“It’s the little details that are vital. Little things make big things happen.” – John Wooden

La Gestion Des TouchEvents

Introduction

Voilà maintenant un petit moment que je souhaitais parler de la gestion de ce que j'appelle les TouchEvent sous Android. Les TouchEvent sont les actions utilisateur générées lorsque ce dernier interagit avec l'écran. Le framework Android propose plusieurs façon de gérer ces évenements :

  • Hériter de la classe View : il est ainsi possible de redéfinir la méthode onTouchEvent(MotionEvent event) appelée par le système lors d'un TouchEvent

  • Utiliser la notion de Listener : if suffit d'implémenter l'interface View.OnTouchListener et de s'inscrire auprès de la vue sur laquelle on souhaite suivre les évènements par un simple setOnTouchListener(OnTouchListener listener)

  • Hériter de la classe Activity : cette classe comporte une méthode onTouchEvent(MotionEvent event) qui est appelée si aucune des vues présente dans l'activité n'a consommé l'évènement. C'est en quelque sorte le dernier moyen de récupérer un évènement avant qu'il ne soit tout simplement perdu

Le but de ce post n'est pas d'expliquer comment gérer les TouchEvents dans votre application mais plutôt de comprendre comment Android transporte l'événement dans votre arborescence de Views. Si vous souhaitez simplement étudier les TouchEvent et leur utilisation dans vos vues, je vous conseille de lire la Javadoc associée à MotionEvent. Pour résumer, il suffit de regarder la valeur de event.getAction() pour savoir ce que l'utilisateur vient de faire. Les valeurs possibles sont :

  • MotionEvent.ACTION_DOWN : L'utilisateur vient d'appuyer sur l'écran. C'est la première valeur récupérée suite à une action sur l'écran

  • MotionEvent.ACTION_MOVE : Fait suite à l'événement précédent et indique que l'utilisateur n'a pas relaché la pression sur l'écran et est en train de bouger

  • MotionEvent.ACTION_UP : Envoyé lorsque l'utilisateur cesse d'appuyer sur l'écran

  • MotionEvent.ACTION_CANCEL : Action un peu spéciale dont je parlerai dans la suite de cet article

Préparation du cadre

Elaborons tout d'abord le cadre de notre étude : nous disposons d'une activité affichant à l'écran l'interface partiellement donnée ci-dessous. Cette dernière est composée d'un FrameLayout affichant 3 carrés empilés les uns sur les autres :

main.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/frameLayout"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content">

    <TextView
        android:text="Z = 3"
        android:gravity="bottom|right"
        android:padding="10px"
        android:textColor="#000"
        android:background="#0000ff"
        android:layout_width="240px"
        android:layout_height="200px" />

    <TextView
        android:id="@+id/z2"
        android:text="Z = 2"
        android:gravity="bottom|right"
        android:padding="10px"
        android:textColor="#000"
        android:background="#00ff00"
        android:layout_width="160px"
        android:layout_height="150px" />

    <TextView
        android:id="@+id/z1"
        android:text="Z = 1"
        android:gravity="bottom|right"
        android:padding="10px"
        android:textColor="#000"
        android:background="#ff0000"
        android:layout_width="80px"
        android:layout_height="100px" />

</FrameLayout>

Le code source de l'activité principale est assez succinct et utilise la technique des listeners pour récupérer les TouchEvents :

TouchActivity.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package com.cyrilmottier.android.touchevent;

import android.app.Activity;
import android.os.Bundle;
import android.widget.FrameLayout;
import android.widget.TextView;
import android.widget.ToggleButton;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

public class TouchActivity extends Activity implements View.OnTouchListener {

    private static final String TAG_LOG = "TouchActivity";
    private static final boolean LOG = true;

    private FrameLayout mFrameLayout;
    private TextView mZ1;
    private TextView mZ2;

    private ToggleButton mButtonZ1;
    private ToggleButton mButtonZ2;
    private ToggleButton mButtonFrameLayout;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        mFrameLayout = (FrameLayout)findViewById(R.id.frameLayout);
        mZ1 = (TextView)findViewById(R.id.z1);
        mZ2 = (TextView)findViewById(R.id.z2);
        mButtonFrameLayout = (ToggleButton)findViewById(R.id.buttonFrameLayout);
        mButtonZ1 = (ToggleButton)findViewById(R.id.buttonZ1);
        mButtonZ2 = (ToggleButton)findViewById(R.id.buttonZ2);

        mZ1.setOnTouchListener(this);
        mZ2.setOnTouchListener(this);
        mFrameLayout.setOnTouchListener(this);

    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (v == mZ1) {
            log("mZ1: " + stringValue(event));
            return mButtonZ1.isChecked();
        } else if (v == mZ2) {
            log("mZ2: " + stringValue(event));
            return mButtonZ2.isChecked();
        } else if (v == mFrameLayout) {
            log("mFrameLayout: " + stringValue(event));
            return mButtonFrameLayout.isChecked();
        }
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        log("Activity: " + stringValue(event));
        return true;
    }

    private String stringValue(MotionEvent event) {

        final int action = event.getAction();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                return "ACTION_DOWN";
            case MotionEvent.ACTION_MOVE:
                return "ACTION_MOVE";
            case MotionEvent.ACTION_UP:
                return "ACTION_UP";
            case MotionEvent.ACTION_CANCEL:
                return "ACTION_CANCEL";
        }

        return "";
    }

    private static void log(String message){
        if (LOG) {
            Log.d(TAG_LOG, message);
        }
    }
}

Le code complet de ce programme est disponible dans le zip accessible à cette adresse. Je vous conseille vivement de lancer vous même ce programme car il vous permettra de tester de façon plus approfondie et interactive les points que nous traiterons ci-dessous. Une fois lancé, vous devriez obtenir une interface comme suit :

Comportement d'Android face aux TouchEvents

Nous voilà près à analyser la façon dont Android gère et fait naviguer l'information dans l'arborescence des vues. Nous parlions en introduction de “consommer les évènements”. Pour informer Android qu'un évènement à été consommé ou non par une vue, il faut gérer le retour de la méthode s'occupant des TouchEvent. Dans le cas de l'exemple, on utilise la méthode onTouch(MotionEvent event) et on gère le code de retour (true ou false) en fonction de l'état des ToggleButton.

En laissant inactif l'ensemble des vues (ToggleButtons éteints), puis en effectuant un TouchEvent, la sortie de débogage affiche une séquence concordant avec le modèle suivant :

05-05 16:52:23.225: DEBUG/TouchActivity(723): mZ1: ACTION_DOWN
05-05 16:52:23.225: DEBUG/TouchActivity(723): mZ2: ACTION_DOWN
05-05 16:52:23.245: DEBUG/TouchActivity(723): mFrameLayout: ACTION_DOWN
05-05 16:52:23.255: DEBUG/TouchActivity(723): Activity: ACTION_DOWN
05-05 16:52:24.264: DEBUG/TouchActivity(723): Activity: ACTION_MOVE
05-05 16:52:24.305: DEBUG/TouchActivity(723): Activity: ACTION_MOVE
05-05 16:52:25.150: DEBUG/TouchActivity(723): Activity: ACTION_UP

Ce premier test nous permet d'en savoir plus sur la gestion de l'information de touch. Android envoie d'abord l'information à la vue (sur laquelle on appui bien sûr) de plus haut niveau, c'est à dire celle étant en sommet de pile. Si cette dernière ne consomme pas l'évenement, il est retransmis à la vue directement inférieure. Cette action se répète jusqu'à ce qu'une vue consomme l'évènement. Lorsqu'aucune vue ne consomme l'évènement, c'est l'activité qui sert de dernier “recourt”.

On réitère maintenant l'opération mais en activant la seconde vue (c'est à dire Z = 2). Une pression suivie d'un mouvement et d'un relâchement sur la vue Z = 1 (vue rouge) donne la sortie suivante :

05-05 16:52:53.104: DEBUG/TouchActivity(723): mZ1: ACTION_DOWN
05-05 16:52:53.114: DEBUG/TouchActivity(723): mZ2: ACTION_DOWN
05-05 16:52:55.585: DEBUG/TouchActivity(723): mZ2: ACTION_MOVE
05-05 16:52:56.826: DEBUG/TouchActivity(723): mZ2: ACTION_MOVE
05-05 16:52:57.214: DEBUG/TouchActivity(723): mZ2: ACTION_UP

En gardant la même configuration et répétant la même procédure mais sur la vue Z = 3, on revient au premier cas cité : l'activité est donc la dernière à gérer l'évènement.

Pour résumer, Android passe simplement l'information de la vue de plus haut niveau à la vue de plus bas niveau. Ce passage d'information est effectué jusqu'à ce qu'une vue consomme (renvoie true lors de la gestion du TouchEvent). Dès lors que l'évènement ACTION_DOWN a été consommé, l'ensemble des évènements suivants (ACTION_DOWN et ACTION_UP) sont directement envoyés à la vue dite cible.

Interception de TouchEvents

La gestion des TouchEvents est, après étude succincte, tout à fait logique. Il existe également une autre méthode permettant de contourner certains problèmes relatifs aux ViewGroups. En effet, prenons un ViewGroup contenant un ensemble d'ImageView. L'utilisateur peut faire “glisser” les images de l'une à l'autre un peu à l'instar de la galerie photo de l'iPhone. Il peut être intéressant de faire en sorte que la ViewGroup gère directement sans se soucier du retour des onTouchEvent(MotionEvent event) des ImageViews. Pour ce faire, on utilise la méthode onInterceptTouchEvent(MotionEvent event).

La documentation incluse dans le SDK explique de façon exhaustive cette méthode et peut se résumer de la façon suivante :

  1. L'évènement ACTION_DOWN est reçu ici (comprendre la ViewGroup)

  2. L'évènement circule de vue en vue comme expliqué précédemment. Si la méthode onTouchEvent(MotionEvent event) reçoit l'évènement ACTION_DOWN, il suffit de faire en sorte que cette dernière renvoit true. Ainsi les évènements suivants ne sont plus reçus par la méthode onInterceptTouchEvent(MotionEvent event) mais passe directement à onTouchEvent(MotionEvent event)

  3. Tant que cette méthode retourne false, les évènements seront envoyés à la fois dans cette méthode et à la vue cible (celle ayant consommé l'ACTION_DOWN)

  4. Si true est retourné, aucun autre évènement n'est envoyé à cette méthode et la vue cible reçoit l'action ACTION_DOWN. Les évènements suivant sont envoyés à la méthode onTouchEvent(MotionEvent event) de la ViewGroup

Les méthodes disponibles et les façons de récupérer les TouchEvents sous Android sont nombreuses. Notez, tout de même, que la philosophie d'Android est de gérer, le plus possible, les évènements au niveau le plus bas dans l'arbre (sur les feuilles) et donc au niveau View. Evitez au maximum de gérer par exemple, les TouchEvents au niveau de l'Activity.