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()