View
- View : classe ancêtre de tous les composants graphiques
- Responsabilité de la gestion de l'affichage et des événements d'une zone sur l'écran
- ViewGroup : peut contenir des vues enfants (arbre de vues)
- Arbre de vues statique définissable en XML dans une ressource layout
- Package android.widget : vues et groupes de vues prédéfinis pour des usages courants
-
Moteur de rendu 2D utilisé pour l'interface graphique : Skia
- Utilisation d'un backend Skia utilisant OpenGL pour le rendu
- Utilisation probable de Vulkan pour les versions futures d'Android
Arbre de vues
- Organisation des vues sous la forme d'un arbre selon l'imbrication
- Chaque vue a un unique père ou est la vue racine installée sur l'activité avec setContentView(View v)
- Avec Android Studio 3, possibilité d'obtenir une capture à l'instant t de l'arbre des vues d'une activités avec le layout inspector (menu Android>Tools>Layout inspector)
Dessin d'une vue
Principe
- Lorsqu'une région devient invalide, elle doit être redessinée (peut être forcé avec View.invalidate())
- L'arbre de vue est parcouru en profondeur pour trouver les vues intersectant la région invalide
- Le rendu d'une vue est implanté dans View.onDraw(Canvas c) ; le canevas communiqué contient les bornes de la région à redessiner récupérable avec c.getClipBounds()
- Lorsque les dimensions de la vue doivent évoluer (changement de contenu), on force un réagencement avec un appel à requestLayout() ; la vue peut changer de dimensions et être redessinée
- Le processus de mesure de la vue est implanté par une redéfinition de la méthode onMeasure(int widthMeasureSpec, int heightMeasureSpec) qui fixe les dimensions avec setMesuredDimension(int width, int height)
Canvas
- Fournit une API pour le dessin 2D
- Primitives de dessin draw*() utilisables directement pour dessiner sur le Bitmap sous-jacent : drawRect(), drawText(), drawLine(), drawBitmap()...
- Dernier argument de type Paint pour les méthodes draw*() : paramètres pour le dessin (couleur, fonte, anti-aliasing...)
- Possibilité de fixer une matrice de transformation (pour faire des rotations et mises à l'échelle) avec setMatrix(Matrix m)
- Délégation du dessin à un Drawable : appel de Drawable.draw(Canvas)
Une sélection de méthodes de Canvas :
- drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
- drawBitmap(Bitmap bitmap, float left, float top, Paint paint)
- drawCircle(float cx, float cy, float radius, Paint paint)
- drawLine(float startX, float startY, float stopX, float stopY, Paint paint)
- drawOval(float left, float top, float right, float bottom, Paint paint)
- drawPoints(float[] pts, Paint paint)
- drawPosText(String text, float[] pos, Paint paint)
- drawRect(float left, float top, float right, float bottom, Paint paint)
- drawText(String text, float x, float y, Paint paint)
Exemple
- Implantons une vue affichant un carré dont le coloris passe de noir à rouge
- Le carré occupe toute la taille de la vue
- Les dimensions de la vue varient de 0 pixels à la taille entière accordée par le composant parent
-
Règles :
- Valeurs redness et size définies par l'utilisateur avec des setters : flottants compris entre 0.0 et 1.0
- Valeur de la composante V et B : 0
- Valeur de la composante R : 255 * redness
- Taille de la vue : width=parent_width * size, height=parent_height * size
-
Ne pas oublier :
- D'hériter de View
- De réimplanter tous les constructeurs de View (pour permettre une instanciation depuis un layout XML)
- D'impanter les setters avec appels judicieux à requestLayout() et invalidate()
- De redéfinir onMeasure(...)
- De redéfinir onDraw(...)
Code de SquareView :
// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License package fr.upem.coursand.squareview; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.util.AttributeSet; import android.view.View; /** A variable-sized view displaying a black-red square */ public class SquareView extends View { // Implementation of all the ancestor constructors public SquareView(Context context) { super(context); } public SquareView(Context context, AttributeSet attrs) { super(context, attrs); } public SquareView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public SquareView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } private float redness = 0.0f; private float size = 0.0f; public void setSize(float size) { if (size != this.size) { this.size = size; requestLayout(); // to resize } } public void setRedness(float redness) { if (redness != this.redness) { this.redness = redness; invalidate(); } } protected int computeDimension(int spec) { if (MeasureSpec.getMode(spec) == MeasureSpec.EXACTLY) return MeasureSpec.getSize(spec); // we have no choice else return (int)(MeasureSpec.getSize(spec) * size); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(computeDimension(widthMeasureSpec), computeDimension(heightMeasureSpec)); } private final Paint paint = new Paint(); @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); paint.setARGB(255, (int)(255 * redness), 0, 0); // set the color canvas.drawRect(0, 0, getWidth(), getHeight(), paint); } }
Réalisation d'une activité utilisant SquareView :
- La vue doit être déclarée sur le layout en WRAP_CONTENT pour lui permettre d'adopter la taille qu'elle souhaite (autrement le layout parent impose la taille selon les contraintes)
<?xml version="1.0" encoding="utf-8"?> <!-- Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License --> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".squareview.SquareActivity"> <TextView android:id="@+id/textView5" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Size" app:layout_constraintTop_toTopOf="parent" tools:layout_editor_absoluteX="16dp" /> <TextView android:id="@+id/textView6" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Redness" app:layout_constraintTop_toBottomOf="@+id/textView5" tools:layout_editor_absoluteX="16dp" /> <SeekBar android:id="@+id/sizeBar" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginLeft="8dp" app:layout_constraintTop_toTopOf="@id/textView5" app:layout_constraintStart_toEndOf="@+id/textView5" app:layout_constraintEnd_toEndOf="parent"/> <SeekBar android:id="@+id/rednessBar" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginLeft="8dp" app:layout_constraintStart_toEndOf="@+id/textView6" app:layout_constraintTop_toTopOf="@id/textView6" app:layout_constraintEnd_toEndOf="parent" tools:layout_editor_absoluteY="99dp" /> <fr.upem.coursand.squareview.SquareView android:id="@+id/squareView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginLeft="8dp" android:layout_marginTop="8dp" android:layout_marginEnd="8dp" android:layout_marginRight="8dp" android:layout_marginBottom="8dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/rednessBar" /> </androidx.constraintlayout.widget.ConstraintLayout>
// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License package fr.upem.coursand.squareview; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.widget.SeekBar; import fr.upem.coursand.R; public class SquareActivity extends AppCompatActivity { private SquareView squareView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_square); squareView = findViewById(R.id.squareView); for (int id: new int[]{R.id.sizeBar, R.id.rednessBar}) ((SeekBar)findViewById(id)).setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { float value = getValue(seekBar.getId()); switch (seekBar.getId()) { case R.id.sizeBar: squareView.setSize(value); break; case R.id.rednessBar: squareView.setRedness(value); break; } } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } }); } public float getValue(int viewId) { SeekBar bar = (SeekBar)findViewById(viewId); return (float)bar.getProgress() / bar.getMax(); } }
Exercices d'application
Optimisation : utilisation d'un buffer
- Dessin direct sur le canvas d'une vue coûteux en cas de nombreuses opérations incrémentales
- Exemple : surface de dessin avec ajout successif de formes
-
Approche optimisée : ne plus dessiner directement sur le Canvas de la vue mais dans un buffer (object Bitmap)
- Bitmap bufferBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
- Canvas bufferCanvas = new Canvas(bufferBitmap)
- On dessine sur le bufferCanvas
- On applique le bufferBitmap sur le canvas de la vue dans la méthode onDraw
Paint paint = new Paint(); @Override protected void onDraw(Canvas c) { canvas.drawBitmap(bufferBitmap, csanvas.getWidth(), canvas.getHeight(), paint); }
Implantations élémentaires de View
-
Éléments de formulaire
-
TextView : affiche une chaîne
- Méthode getText() pour obtenir le contenu de la zone
- Méthode setText(int id) pour afficher une ressource texte (R.id.myText)
-
Méthode setText(CharSequence cs) pour afficher une chaîne
- ⚠ Pour afficher un int, utiliser setText("" + a) et non setText(a) (qui considérerait l'int comme un id de ressource)
-
EditText : permet la saisie d'une chaîne (propriété inputType pour le type d'entrée attendu)
- EditText hérite de TextView avec mêmes méthodes getText() et setText()
- Button : bouton cliquable, variante de type interrupteur avec ToggleButton
-
CheckBox : case à cocher
- Méthode boolean isChecked() pour savoir si la case est cochée
- Méthode void setChecked(boolean checked) pour changer l'état de cochage
-
RadioButton : bouton radio regroupable dans un RadioGroup
- Méthodes isChecked() et setChecked() utilisables
-
ProgressBar : barre de progression (horizontale, circulaire), variante avec étoiles de notation avec RatingBar
- Méthodes int getMax() et void setMax(int max) pour accéder et modifier la valeur maximale de la barre
- Méthodes int getProgress() et int setProgress(int value) pour manipuler la valeur de progression (comprise entre 0 et max)
-
SeekBar : barre de réglage
- Hérite de ProgressBar
- SearchView : champ de recherche avec proposition de suggestions
-
TextView : affiche une chaîne
-
Éléments multimédias
-
ImageView : affichage d'une ressource image
- Différents setters pour charger une nouvelle image : setImageResource(int resId) depuis un identifiant de ressource, setImageBitmap(Bitmap bm) depuis un Bitmap (constructible dynamiquement)...
- ImageButton : bouton avec image
- VideoView : affichage contrôlable de vidéo
- MediaController : offre des boutons de contrôle pour une vidéo (avec VideoView par exemple)
- ZoomControls : bouton de zoom/dezoom
-
ImageView : affichage d'une ressource image
-
Widgets composés pour formulaires
- TimePicker, DatePicker : choix d'horaire et de date
- CalendarView: affiche un calendrier avec date sélectionnable
- NumberPicker : sélection d'un entier dans un intervalle avec incrémentation et décrémentation
- DialerFilter : permet de saisir des chiffres/lettres avec un clavier numérique de téléphone
-
Autres
- Space : vue vide n'affichant rien (pouvant servir de vue intercalaire dans certains layouts tels que LinearLayout)

Listeners d'événements
Objectif : associer une action à réaliser lors de la survenue d'un événement
Réaction à des événements avec un listener
- Enregistrement d'un listener avec setOn**Event**Listener(EventListener)
-
EventListener est généralement une interface avec une seule méthode onEvent à implanter
- Signature typique : boolean onEvent(View v, Event e)
- Le booléen retourné indique si l'on a consommé ou non l'événement
- L'objet de type Event (KeyEvent, TouchEvent...) contient des informations détaillées sur l'événément
-
Il faut consulter la Javadoc pour plus d'informations sur chaque type de listener :
- Par exemple OnClickListener nécessite l'implantation de la méthode boolean onClick(View v)
-
L'enregistrement est généralement réalisé dans la méthode onCreate() de l'activité
- On créé le composant ou on le récupère depuis un layout XML installé avec View findViewById(int)
- On appelle le setter pour l'installation du listener (généralement une classe anonyme)
- On peut attribuer un tag (objet Java) à un composant avec setTag(Object obb) ; on peut plus tard retrouver le composant avec un tag donné en appelant root.findViewWithTag(tag)``
- Le composant racine installé avec setContentView peut être récupéré plus tard avec l'appel findViewById(android.R.id.content)
-
Principaux événéments supportés (fonctionnement dépendant du type de composant et du dispositif d'entrée) :
- click : clic sur un composant (appui puis relachement rapide)
- longClick : clic long sur un composant
- createContextMenu : invoqué lors de la création d'un menu contextuel
- drag : événément de glissé (lors d'un glissé-déposé)
- focusChange : changement de focus (gain ou perte)
- hover : lorsque le pointeur rentre dans une zone (utile pour un pointage avec une souris ou un stylet survolant, événement non produit avec un système d'entrée tactile)
- key : appui sur une touche d'un clavier physique
- touch : événement de touché sur le composant (appui, déplacement, relachement d'un ou plusieurs doigts)
Attribut XML onClick
View dispose d'une propriété onClick dont la valeur est définissable dans le layout XML :
- on y spécifie le nom de la méthode appelée lors d'un clic
-
cette méthode doit être présente dans l'activité chargeant le layout XML avec la signature void nomMethode(View v)
- sinon exception !
Redéfinition de la méthode onEvent()
On peut créer un composant dérivé du composant souhaité et redéfinir certaines méthodes... dont les méthodes de type onEvent() qui sont appelées à la survenue d'un événement. À déconseiller en règle générale.
Interception globale d'événements au niveau de l'activité
boolean Activity.dispatchXEvent(XEvent) est une méthode qui dispatche un événément reçu par l'activité vers la vue concernée
Types d'événéments traités :
- dispatchGenericMotionEvent(MotionEvent e)
- dispatchKeyEvent(KeyEvent e)
- dispatchKeyShortcutEvent(KeyEvent e)
- dispatchPopulateAccessibilityEvent(AccessibilityEvent e)
- dispatchTouchEvent(MotionEvent e)
- dispatchTrackballEvent(MotionEvent e)
On peut redéfinir la méthode dans l'activité pour capturer l'événement avant sa transmission à la vue concernée. Si on souhaite tout de même assurer la transmission normale à la vue, il ne faut pas oublier d'appeler super.dispatchX(...).
Interception d'événément par une vue parent
Des méthodes permettent d'intercepter un événement par une vue parent (avant sa transmission à une vue enfant) :
- boolean onInterceptHoverEvent(MotionEvent e)
- boolean onIntercepTouchEvent(MotionEvent e)
- Par défaut, ces méthodes ne font que retourner false. On les redéfinit pour réaliser une interception : on peut à la fin retourner true pour déclarer que l'événement a été consommé et ne doit plus être transmis à la vue enfant.
Une vue enfant peut temporairement désactiver l'interception d'un parent en appelant sa méthode requestDisallowInterceptTouchEvent(boolean disallowIntercept) (valable pour la séquence de touché courante).
Evénéments courants
- void onClick(View) : clic tactile, par trackball ou validation
- boolean onLongClick(View) : clic long (1s)
- void onFocusChange(View v, boolean hasFocus) : gain ou perte de focus
- boolean onKey(View v, int keyCode, KeyEvent e) : appui sur une touche matérielle
- boolean onTouch(View v, MotionEvent e) : événement de touché
Valeur de retour boolean : permet d'indiquer si l'événement a été consommé, i.e. s'il ne doit plus être communiqué à d'autres listeners (de la même vue ou de vues enfant) ou si la fin d'un événement composé doit être ignorée.
Événements de touché
Un exemple de OnTouchListener
view.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent e) { switch (e.getActionMasked()) { case MotionEvent.ACTION_DOWN : // Starting a new touch action (first finger pressed) // Coordinates of the starting point can be fetched with e.getX() and e.getY() // The first finger has the index 0 case MotionEvent.ACTION_POINTER_DOWN : // A new finger is pressed // Its identifier can be obtained with e.getPointerId(e.getActionIndex()) case MotionEvent.ACTION_MOVE : // One or several fingers are moved // Their coordinates can be obtained with e.getX(int index) and e.getY(int index) // e.getPointerCount() specifies the number of implied fingers // e.getPointerId(int) converts a pointer index to a universal id // that can be tracked across events // e.findPointerIndex(int) does the reverse operation case MotionEvent.ACTION_POINTER_UP : // A finger has been raised // Its identifier is e.getPointerId(e.getActionIndex()) case MotionEvent.ACTION_UP : // The last finger has been raised } return true; // returning true means that the event is consumed } });
Le OnTouchListener est toujours appelé avant les listeners de gestion de clic et de clic long : si la méthode de gestion d'événement de touché retourne true l'événement est consommé et les listeners de clic ne seront pas appelés.
Exercice d'application : le voyageur de commerce
- Afficher plusieurs points à l'écran
- Proposer à l'utilisateur de tracer avec le doigt un chemin entre tous ces points
- Calculer la longueur du chemin tracé en pixels et le comparer par rapport à un chemin théoriquement calculé en utilisant une heuristique de voyageur de commerce
Reconnaissance de gestures
- Un détecteur de gestes reçoit les événements de touché (par `onTouchEvent(MotionEvent e)`` et appelle les méthodes du listener enregistré lors de la détection de gestes
-
Reconnaissance de gestures simples avec GestureDetector :
- onDown, onDoubleTap, onLongPress, onFling, onScroll, onShowPress, onSingleTapConfirmed, onSingleTapUp...
-
Reconnaissance de gestures zoom avec ScaleGestureDetector :
- onScaleBegin, onScale, onScaleEnd
-
Réception de gestures complexes avec GestureOverlayView (vue transparente)
- onGesturePerformed(GestureOverlayView overlay, Gesture gesture) permet de récupérer le geste et de le comparer à une GestureLibrary (méthode recognize()) qui propose des candidats avec score de confiance
Drawable
- Définit des données vectorielles (très basiques) ou bitmap pouvant être dessinées : {Bitmap, Layer, NinePatch, Picture, Shape}Drawable
-
Méthodes intéressantes :
- Taille préférée : getInstrinsic{Width, Height}()
- Avant dessin, fixation des bornes : setBounds(Rect r)
- Dessin avec Drawable.draw(Canvas)
- Récupérable d'une ressource avec Ressources.getDrawable(int)
- Support du SVG non-natif (bibliothèque externe nécessaire telle que svg-android)
-
VectorDrawable supporté depuis Android Lollipop
- Ressource vectorielle XML (utilisant un sous-ensemble de SVG) ; il faut convertir le SVG vers ce format
- Possibilité de créer des animations vectorielles avec AnimatedVectorDrawable
Gestion du focus
- Element focusable : isFocusable(), isFocusableInTouchMode() (changement de l'état avec setter) ; hasFocusable() pour test également sur descendants
- Trouver le prochaine vue focusable : View.focusSearch(View.FOCUS_{UP,DOWN,LEFT,RIGHT})
- Possibilité de changer l'ordre de focus par défaut avec attributs XML : nextFocus{Down, Left, Right, Up}
- Demande dynamique de focus : View.requestFocus(), View.requestFocusFromTouch()