Créer de nouvelles vues

 En tant que développeur novateur, vous n’avez qu’une question de temps avant de vous retrouver dans une situation où aucune des commandes intégrées ne répond à vos besoins.

La possibilité d’étendre des vues existantes, d’assembler des contrôles composites et de créer de nouvelles vues uniques permet d’implémenter de superbes interfaces utilisateur optimisées pour le flux de travail spécifique de votre application. Android vous permet de sous-classer la boîte à outils View existante ou d'implémenter vos propres contrôles View, vous offrant une totale liberté pour personnaliser votre interface utilisateur afin d'optimiser l'expérience utilisateur.

 Remarque Lors de la conception d’une interface utilisateur, il est important d’équilibrer l’esthétique brute et la convivialité. Le pouvoir de créer vos propres contrôles personnalisés est la tentation de reconstruire tous vos contrôles à partir de zéro. Résistez à cette envie. Les vues standard seront familières aux utilisateurs d’autres applications Android et seront mises à jour conformément aux nouvelles versions de la plate-forme. Sur de petits écrans, où les utilisateurs paient souvent une attention limitée, la familiarité peut souvent offrir une meilleure facilité d'utilisation qu'un contrôle légèrement plus brillant.

 La meilleure approche à utiliser lors de la création d’une nouvelle vue dépend de ce que vous voulez réaliser:

 Modifier ou étendre l'apparence et / ou le comportement d'une vue existante lorsqu'elle fournit les fonctionnalités de base souhaitées En redéfinissant les gestionnaires d’événements et / ou onDraw, tout en rappelant les méthodes de la superclasse, vous pouvez personnaliser une vue sans avoir à ré-implémenter ses fonctionnalités. Par exemple, vous pouvez personnaliser un objet TextView pour qu'il affiche des nombres en utilisant un nombre défini de points décimaux.

Combinez des vues pour créer des contrôles atomiques réutilisables qui exploitent les fonctionnalités de plusieurs vues interconnectées. Par exemple, vous pouvez créer un chronomètre en combinant une

TextView et un bouton qui réinitialise le compteur lorsque vous cliquez dessus.

Créez un contrôle entièrement nouveau lorsque vous avez besoin d’une interface complètement différente que vous ne pouvez pas utiliser.

obtenir en modifiant ou en combinant les contrôles existants.

 Modification des vues existantes

 La boîte à outils des widgets Android inclut des vues qui fournissent de nombreuses exigences d'interface utilisateur communes, mais les contrôles sont nécessairement génériques. En personnalisant ces vues de base, vous évitez de réimplémenter le comportement existant tout en adaptant l'interface utilisateur et les fonctionnalités aux besoins de votre application.

Pour créer une nouvelle vue basée sur un contrôle existant, créez une classe qui l'étend, comme illustré avec la classe dérivée TextView présentée dans le Listing 5-8. Dans cet exemple, vous étendez la vue texte pour personnaliser son apparence et son comportement.

 LISTING 5-8 : Extension de la vue texte

import android.content.Context; import android.graphics.Canvas; import android.util.AttributeSet; import android.view.KeyEvent; import android.widget.TextView; public class MyTextView extends TextView {

  // Constructor used when creating the View in code

  public MyTextView (Context context) {

    this(context, null);   }

  // Constructor used when inflating the View from XML   public MyTextView (Context context, AttributeSet attrs) {

    this(context, attrs, 0);   }

  // Constructor used when inflating the View from XML when it has a

  // style attribute

  public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {     super(context, attrs, defStyleAttr);

    // Do any custom initialization here

  }

}

Si vous construisez une vue réutilisable, il est vivement recommandé de remplacer ces trois constructeurs pour vous assurer que votre vue peut être créée dans le code et gonflée dans des fichiers XML, comme toutes les vues incluses dans le SDK Android.

Pour remplacer l'apparence ou le comportement de votre nouvelle vue, remplacez et étendez les gestionnaires d'événements associés au comportement que vous souhaitez modifier.

Dans l’extension suivante du code Listing 5-8, la méthode onDraw est substituée pour modifier l’apparence de la vue, et le gestionnaire onKeyDown est remplacé pour autoriser la gestion des appuis clavier personnalisés:

 public class MyTextView extends TextView {

  public MyTextView(Context context) {

    this(context, null);   }

  public MyTextView(Context context, AttributeSet attrs) {

    this(context, attrs, 0);   }

  public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {

    super(context, attrs, defStyleAttr);

  }

  @Override

  public void onDraw(Canvas canvas) {     [ ... Draw things on the canvas under the text ... ]

    // Render the text as usual using the TextView base class.     super.onDraw(canvas);

    [ ... Draw things on the canvas over the text ... ]

  }

  @Override

  public boolean onKeyDown(int keyCode, KeyEvent keyEvent) {

    [ ... Perform some special processing ... ]

    [ ... based on a particular key press ... ]

    // Use the existing functionality implemented by

    // the base class to respond to a key press event.     return super.onKeyDown(keyCode, keyEvent);

  }

}

Les gestionnaires d'événements disponibles dans Vues sont traités plus en détail plus loin dans ce chapitre.

 Définir des attributs personnalisés

 Comme indiqué dans la section précédente, vous disposez de trois constructeurs principaux pour les vues, utilisés pour prendre en charge la création d’une vue en code ainsi qu’une partie d’un fichier XML. Cette même dualité s’applique aux fonctionnalités que vous pourriez ajouter à votre vue - vous voudrez bien prendre en charge la modification de fonctionnalités ajoutées via le code et via XML.

L'ajout de fonctionnalités dans le code n'est pas différent pour une vue ou toute autre classe et implique généralement l'ajout d'une méthode set et get:

public class PriceTextView extends TextView {   private static NumberFormat CURRENCY_FORMAT =     NumberFormat.getCurrencyInstance();   private float mPrice;

  // These three constructors are required for all Views

  public PriceTextView(Context context) {

    this(context, null);   }

  public PriceTextView(Context context, AttributeSet attrs) {

    this(context, attrs, 0);   }

  // Constructor used when inflating the View from XML when it has a

  // style attribute

  public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {

    super(context, attrs, defStyleAttr);

  }

  public void setPrice(float price) {

    mPrice = price;

    setText(CURRENCY_FORMAT.format(price));   }

  public float getPrice() {     return mPrice;

  }

}

Cependant, cela ne permet que de changer le prix dans le code. Pour définir le prix affiché dans vos fichiers XML, vous pouvez créer un attribut personnalisé, généralement dans un fichier res / values ​​/ attrs.xml contenant un ou plusieurs éléments <declare-styleable>:

 < resources >

  <declare-styleable name="PriceTextView">

    <attr name="price" format="reference|float" />

  </declare-styleable>

< /resources >

Il est d'usage que le nom <declare-styleable> corresponde au nom de la classe utilisant l'attribut, bien que cela ne soit pas strictement requis.

Il est important de noter que les noms utilisés sont globaux. Votre application ne sera pas compilée si le même attribut est déclaré plus d'une fois (comme dans votre application et dans une bibliothèque que vous utilisez). Pensez à ajouter un préfixe à vos attributs s’ils sont susceptibles d’être des noms communs.

Les formats de base disponibles pour les attributs incluent couleur, booléen, dimension, float, entier, chaîne, fraction, enum et indicateur. Le format de référence est particulièrement important et vous permet de référencer une autre ressource lorsque vous utilisez votre attribut personnalisé (par exemple, en utilisant @ string / nom_app). Si vous souhaitez autoriser plusieurs formats, combinez les formats avec le | personnage.

Votre View XML peut ensuite référencer l'attribut personnalisé en ajoutant une déclaration d'espace de nom associée à tous les attributs déclarés par votre application, généralement à l'aide de xmlns: app (bien que app puisse être n'importe quel identifiant choisi):

 < PriceTextView

    xmlns:android:"http://schemas.android.com/apk/res/android"     xmlns:app="http://schemas.android.com/apk/res-auto"

    android:layout_width="wrap_content"     android:layout_height="wrap_content"     app:price="1999.99" />

Vous pouvez ensuite lire les attributs personnalisés dans votre classe à l'aide de la méthode obtenirStyledAttributes:

 // Constructor used when inflating the View from XML when it has a

// style attribute

public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {

  super(context, attrs, defStyleAttr);

 

  final TypedArray a = context.obtainStyledAttributes(attrs,     R.styleable.PriceTextView, // The <declare-styleable> name

    defStyleAttr,

    0); // An optional R.style to use for default values

  if (a.hasValue(R.styleable.PriceTextView_price)) {     setPrice(a.getFloat(R.styleable.PriceTextView_price,

    0)); // default value

  }

  a.recycle(); }

Remarque Vous devez toujours appeler recycler lorsque vous avez terminé de lire les valeurs à partir de la table TypedArray.

 Création de contrôles composés

 Les contrôles composés sont des groupes de vues autonomes et autonomes qui contiennent plusieurs vues enfant disposées et connectées entre elles.

Lorsque vous créez un contrôle composé, vous définissez la disposition, l'apparence et l'interaction des vues qu'il contient. Vous créez des contrôles composés en développant un ViewGroup (généralement une présentation). Pour créer un nouveau contrôle composé, choisissez la classe d’agencement la plus appropriée pour positionner les contrôles enfants et étendez-la:

 public class MyCompoundView extends LinearLayout {   public MyCompoundView(Context context) {

    this(context, null);   }

  public MyCompoundView(Context context, AttributeSet attrs) {

    this(context, attrs, 0);   }

  public MyCompoundView(Context context, AttributeSet attrs,

                        int defStyleAttr) {     super(context, attrs, defStyleAttr);

  }

}

Comme pour les activités, la méthode préférée pour concevoir des dispositions d'interface utilisateur de vues composées consiste à utiliser une ressource externe.

Le Listing 5-9 montre la définition de la présentation XML pour un contrôle composé simple consistant en un texte d'édition pour la saisie de texte, avec un bouton «Effacer» en dessous.

 < ?xml version="1.0" encoding="utf-8"? >

< LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

  android:orientation="vertical"   android:layout_width="match_parent"   android:layout_height="wrap_content">

  <EditText

    android:id="@+id/editText"     android:layout_width="match_parent"     android:layout_height="wrap_content"

  />

  <Button

    android:id="@+id/clearButton"     android:layout_width="match_parent"     android:layout_height="wrap_content"

    android:text="Clear"

  />

< /LinearLayout >

Pour utiliser cette présentation dans votre nouvelle vue composée, substituez son constructeur pour gonfler la ressource de présentation à l'aide de la méthode inflate du service système LayoutInflate. La méthode inflate la ressource de présentation et renvoie la vue gonflée.

Dans des circonstances telles que celle dans laquelle la vue renvoyée doit être la classe que vous créez, vous pouvez transmettre la vue parent et y attacher le résultat automatiquement.

Le Listing 5-10 illustre cela en utilisant la classe ClearableEditText. Dans le constructeur, il gonfle la ressource de présentation du Listing 5-9, puis trouve une référence aux vues d'édition de texte et de boutons qu'il contient. Il fait également appel à hookupButton qui sera utilisé par la suite pour raccorder la tuyauterie qui implémentera la fonctionnalité de texte en clair.

 LISTING 5-10 : Construire une vue composée

public class ClearableEditText extends LinearLayout {

  EditText editText;

  Button clearButton;

  public ClearableEditText(Context context) {

    this(context, null);   }

  public ClearableEditText(Context context, AttributeSet attrs) {

    this(context, attrs, 0);   }

  public ClearableEditText(Context context, AttributeSet attrs,

                           int defStyleAttr) {     super(context, attrs, defStyleAttr);

    // Inflate the view from the layout resource.

    String infService = Context.LAYOUT_INFLATER_SERVICE;

    LayoutInflater li;

    li = (LayoutInflater)getContext().getSystemService(infService);

    li.inflate(R.layout.clearable_edit_text, this, true);

    // Get references to the child controls.     editText = (EditText)findViewById(R.id.editText);     clearButton = (Button)findViewById(R.id.clearButton);

    // Hook up the functionality

    hookupButton();

  }

}

Si vous préférez construire votre mise en page dans le code, vous pouvez le faire comme pour une activité:

 public ClearableEditText(Context context, AttributeSet attrs,

                         int defStyleAttr) {   super(context, attrs, defStyleAttr);

  // Set orientation of layout to vertical   setOrientation(LinearLayout.VERTICAL);

  // Create the child controls.   editText = new EditText(getContext());   clearButton = new Button(getContext());   clearButton.setText("Clear");

  // Lay them out in the compound control.   int lHeight = LinearLayout.LayoutParams.WRAP_CONTENT;   int lWidth = LinearLayout.LayoutParams.MATCH_PARENT;

  addView(editText, new LinearLayout.LayoutParams(lWidth, lHeight));   addView(clearButton, new LinearLayout.LayoutParams(lWidth, lHeight));

  // Hook up the functionality

  hookupButton(); }

Après avoir construit la présentation de la vue, vous pouvez connecter les gestionnaires d’événements pour chaque contrôle enfant afin de fournir les fonctionnalités dont vous avez besoin. Dans le Listing 5-11, la méthode hookupButton est renseignée pour effacer le texte à modifier lorsque le bouton est enfoncé.

 LISTING 5-11 : Mettre en œuvre le bouton "Clear"

private void hookupButton() {

  clearButton.setOnClickListener(new Button.OnClickListener() {

    public void onClick(View v) {       editText.setText("");

    }

  });

}

 Création de contrôles composés simples en tant que mise en page

 Il est souvent suffisant, et plus flexible, de définir la disposition et l’apparence d’un ensemble de vues sans câbler leurs interactions.

Vous pouvez créer une disposition réutilisable en créant une ressource XML qui encapsule le modèle d'interface utilisateur que vous souhaitez réutiliser. Vous pouvez ensuite importer ces modèles de présentation lors de la création de l'interface utilisateur d'activités ou de fragments à l'aide de la balise include dans leurs définitions de ressources de présentation:

 <include layout = "@ layout / clearable_edit_text" />

 La balise include vous permet également de remplacer les paramètres id et layout du nœud racine de la disposition incluse:

 <include

  layout="@layout/clearable_edit_text"   android:id="@+id/add_new_entry_input"   android:layout_width="match_parent"   android:layout_height="wrap_content"   android:layout_gravity="top" />

 Création de vues personnalisées

 La création de nouvelles vues vous donne le pouvoir de modifier fondamentalement l'apparence de vos applications. En créant vos propres contrôles, vous pouvez créer des interfaces utilisateur parfaitement adaptées à vos besoins.

Pour créer de nouveaux contrôles à partir d'un canevas vierge, vous étendez la classe View ou la classe SurfaceView. La classe View fournit à un objet Canvas une série de méthodes de dessin et de classes Paint. Utilisez-les pour créer une interface visuelle avec des bitmaps et des graphiques raster. Vous pouvez ensuite remplacer les événements utilisateur, y compris les effleurements à l'écran ou les touches pour une interactivité optimale.

Dans les situations dans lesquelles il n'est pas nécessaire de repeindre extrêmement rapidement et de graphiques 3D, la classe de base View offre une solution puissante et légère.

La classe SurfaceView fournit un objet Surface prenant en charge le dessin à partir d'un thread en arrière-plan et éventuellement l'utilisation d'OpenGL pour implémenter vos graphiques. C'est une excellente option pour les contrôles graphiques lourds qui sont fréquemment mis à jour (comme la vidéo en direct) ou qui affichent des informations graphiques complexes (notamment les jeux et les visualisations 3D).

Référence Cette section porte sur la création de contrôles basés sur la classe View.

Pour en savoir plus sur la classe SurfaceView et sur certaines des fonctionnalités de peinture Canvas les plus avancées disponibles dans Android, reportez-vous au Chapitre 14, «Personnalisation avancée de votre interface utilisateur».

 Création d'une nouvelle interface visuelle

 La classe View de base présente un carré distinctement vide de 100 pixels par 100 pixels. Pour modifier la taille du contrôle et afficher une interface visuelle plus convaincante, vous devez remplacer les méthodes onMeasure et onDraw.

Dans onMeasure, votre vue déterminera la hauteur et la largeur qu’elle occupera en fonction d’un ensemble de conditions aux limites. La méthode onDraw consiste à dessiner sur le canevas.

Le Listing 5-12 montre le code squelette pour une nouvelle classe View, qui sera examinée et développée dans les sections suivantes.

 

public class MyView extends View {

  public MyView(Context context) {     this(context, null);   }

  public MyView (Context context, AttributeSet attrs) {

    this(context, attrs, 0);   }

  public MyView(Context context, AttributeSet attrs, int defStyleAttr) {

    super(context, attrs, defStyleAttr);   }

  @Override

  protected void onMeasure(int wMeasureSpec, int hMeasureSpec) {

    int measuredHeight = measureHeight(hMeasureSpec);     int measuredWidth = measureWidth(wMeasureSpec);

    // MUST make this call to setMeasuredDimension

    // or you will cause a runtime exception when

    // the control is laid out.

    setMeasuredDimension(measuredHeight, measuredWidth);   }

  private int measureHeight(int measureSpec) {     int specMode = MeasureSpec.getMode(measureSpec);     int specSize = MeasureSpec.getSize(measureSpec);      [ ... Calculate the view height ... ]

     return specSize;   }

  private int measureWidth(int measureSpec) {     int specMode = MeasureSpec.getMode(measureSpec);     int specSize = MeasureSpec.getSize(measureSpec);      [ ... Calculate the view width ... ]

     return specSize;   }

  @Override

  protected void onDraw(Canvas canvas) {     [ ... Draw your visual interface ... ]

  }

}

 Remarque  La méthode onMeasure appelle setMeasuredDimension. Vous devez toujours appeler cette méthode dans la méthode onMeasure remplacée. sinon, votre vue lève une exception lorsque le conteneur parent tente de la structurer.

 Tirer votre contrôle

La méthode onDraw est l'endroit où la magie se produit. Si vous créez un nouveau widget à partir de rien, c’est parce que vous souhaitez créer une toute nouvelle interface visuelle. Le paramètre Canvas de la méthode onDraw est la surface que vous utiliserez pour donner vie à votre imagination.

Android Canvas utilise l’algorithme du peintre, c’est-à-dire que chaque fois que vous dessinez sur le canevas, il recouvre tout ce qui a déjà été dessiné sur la même zone.

Les API de dessin fournissent divers outils permettant de dessiner votre conception sur le canevas à l'aide de divers objets Paint. La classe Canvas comprend des méthodes d’aide pour dessiner des objets 2D primitifs, notamment des cercles, des lignes, des rectangles, du texte et des éléments dessinables (images). Il prend également en charge les transformations qui vous permettent de faire pivoter, traduire (déplacer) et redimensionner (redimensionner) le canevas pendant que vous dessinez dessus.

Lorsque ces outils sont combinés à Drawables et à la classe Paint (qui offrent une variété de remplissages et de plumes personnalisables), la complexité et les détails que votre contrôle peut restituer ne sont limités que par la taille de l'écran et la puissance du rendu du processeur. il.

 Avertissement L'une des techniques les plus importantes pour écrire du code efficace dans Android consiste à éviter la création et la destruction répétitives d'objets. Tout objet créé dans votre méthode onDraw sera créé et détruit à chaque actualisation de l'écran. Améliorez l'efficacité en définissant autant d'objets de cette classe (en particulier des exemples de Paint et Drawable) et en déplaçant leur création dans le constructeur.

 Le Listing 5-13 montre comment remplacer la méthode onDraw pour afficher une simple chaîne de texte au centre de la vue.

 

 @Override

protected void onDraw(Canvas canvas) {

  // Get the size of the control based on the last call to onMeasure.

  int height = getMeasuredHeight();   int width = getMeasuredWidth();

  // Find the center   int px = width/2;   int py = height/2;   // Create the new paint brushes.

  // NOTE: For efficiency this should be done in

  // the views's constructor

  Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);   mTextPaint.setColor(Color.WHITE);

  // Define the string.

  String displayText = "Hello View!";

  // Measure the width of the text string.   float textWidth = mTextPaint.measureText(displayText);

  // Draw the text string in the center of the control.

  canvas.drawText(displayText, px-textWidth/2, py, mTextPaint); }

 Remarque Pour modifier un élément de votre toile, vous devez repeindre l'intégralité de la toile. La modification de la couleur d’un pinceau ne modifiera pas l’affichage de votre View tant que le contrôle n’est pas invalidé et redessiné. Alternativement, vous pouvez utiliser OpenGL pour rendre les graphiques. Pour plus de détails, voir la discussion sur SurfaceView au chapitre 17, «Audio, vidéo et utilisation de la caméra».

 Dimensionner votre contrôle

Sauf si vous avez besoin d'un contrôle qui occupe toujours un espace de 100 pixels, vous devrez également remplacer onMeasure.

La méthode onMeasure est appelée lorsque le parent du contrôle présente ses contrôles enfants. Il pose la question «Combien d'espace utiliserez-vous?» Et transmet deux paramètres: widthMeasureSpec et heightMeasureSpec. Ces paramètres spécifient l'espace disponible pour le contrôle et certaines métadonnées pour décrire cet espace.

Plutôt que de renvoyer un résultat, vous transmettez la hauteur et la largeur de la vue à la méthode setMeasuredDimension.

L'extrait suivant montre comment remplacer onMeasure. Les appels à la méthode locale stubs measureHeight et measureWidth sont utilisés pour décoder les valeurs widthHeightSpec et heightMeasureSpec et calculer les valeurs de hauteur et de largeur préférées, respectivement:

@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

  int measuredHeight = measureHeight(heightMeasureSpec);   int measuredWidth = measureWidth(widthMeasureSpec);

  setMeasuredDimension(measuredHeight, measuredWidth);

}

private int measureHeight(int measureSpec) {

  // Return measured widget height.

}

private int measureWidth(int measureSpec) {

  // Return measured widget width.

}

Les paramètres de limite, widthMeasureSpec et heightMeasureSpec, sont transmis sous forme d'entiers pour des raisons d'efficacité. Avant de pouvoir être utilisés, ils doivent d'abord être décodés à l'aide des méthodes statiques getMode et getSize de la classe MeasureSpec:

int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec);

 En fonction de la valeur du mode, la taille représente soit l'espace maximum disponible pour le contrôle (dans le cas de AT_MOST), soit la taille exacte occupée par votre contrôle (pour EXACTEMENT). Dans le cas de UNSPECIFIED, votre contrôle n'a aucune référence pour ce que représente la taille.

En marquant une taille de mesure comme EXACT, le parent insiste pour que la vue soit placée dans une zone de la taille exacte spécifiée. Le mode AT_MOST indique que le parent demande quelle taille la vue souhaite occuper, en fonction d'une limite supérieure. Dans de nombreux cas, la valeur que vous renvoyez sera la même ou la taille requise pour emballer correctement l'interface utilisateur que vous souhaitez afficher.

Dans les deux cas, vous devez considérer ces limites comme absolues. Dans certaines circonstances, il peut toujours être approprié de renvoyer une mesure en dehors de ces limites. Dans ce cas, vous pouvez laisser le parent choisir comment traiter la vue surdimensionnée à l'aide de techniques telles que le découpage et le défilement.

 Le Listing 5-14 montre une implémentation typique pour la gestion des mesures View.

 @Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

  int measuredHeight = measureHeight(heightMeasureSpec);   int measuredWidth = measureWidth(widthMeasureSpec);

  setMeasuredDimension(measuredHeight, measuredWidth); }

private int measureHeight(int measureSpec) {   int specMode = MeasureSpec.getMode(measureSpec);   int specSize = MeasureSpec.getSize(measureSpec);

  //  Default size in pixels if no limits are specified.   int result = 500;

  if (specMode == MeasureSpec.AT_MOST) {     // Calculate the ideal size of your     // control within this maximum size.

    // If your control fills the available

    // space return the outer bound.

    result = specSize;

  } else if (specMode == MeasureSpec.EXACTLY) {

    // If your control can fit within these bounds return that value.     result = specSize;

  }

  return result; }

private int measureWidth(int measureSpec) {   int specMode = MeasureSpec.getMode(measureSpec);   int specSize = MeasureSpec.getSize(measureSpec);

  //  Default size in pixels if no limits are specified.   int result = 500;

  if (specMode == MeasureSpec.AT_MOST) {     // Calculate the ideal size of your control     // within this maximum size.

    // If your control fills the available space

    // return the outer bound.

    result = specSize;   } else if (specMode == MeasureSpec.EXACTLY) {

    // If your control can fit within these bounds return that value.

    result = specSize;

  }

  return result; }

Gestion des événements d'interaction utilisateur

 Pour que votre nouvelle vue soit interactive, elle doit réagir aux événements déclenchés par l'utilisateur, tels que les pressions sur les touches, les pressions sur l'écran et les clics sur les boutons. Android expose plusieurs gestionnaires d'événements virtuels que vous pouvez utiliser pour réagir aux entrées de l'utilisateur:

  • OnKeyDown: appelé lorsque vous appuyez sur une touche du périphérique. comprend les boutons D-Pad, clavier, raccrocher, appel, retour et caméra
  • onKeyUp: appelé lorsqu'un utilisateur relâche une touche enfoncée
  • onTouchEvent: appelé lorsque l'écran tactile est appuyé ou relâché, ou lorsqu'il détecte un mouvement

La liste 5-15 montre une classe squelette qui remplace chacun des gestionnaires d'interaction utilisateur dans une vue.

 LISTING 5-15: Gestion des événements d'entrée pour les vues

@Override

public boolean onKeyDown(int keyCode, KeyEvent keyEvent) {

  // Return true if the event was handled.

  return true;

}

@Override

public boolean onKeyUp(int keyCode, KeyEvent keyEvent) {

  // Return true if the event was handled.   return true; }

@Override

public boolean onTouchEvent(MotionEvent event) {   // Get the type of action this event represents   int actionPerformed = event.getAction();   // Return true if the event was handled.

  return true; } 

Prise en charge de l'accessibilité dans des vues personnalisées

 Créer une vue personnalisée avec une belle interface n’est que la moitié de l’histoire. Il est tout aussi important de créer des contrôles accessibles pouvant être utilisés par les utilisateurs handicapés qui les obligent à interagir avec leurs appareils de différentes manières.

Les API d'accessibilité proposent des méthodes d'interaction alternatives pour les utilisateurs souffrant de handicaps visuels, physiques ou liés à l'âge, qui rendent difficile toute interaction avec un écran tactile.

La première étape consiste à vérifier que votre vue personnalisée est accessible et navigable à l'aide d'événements D-pad, comme décrit dans la section précédente. Il est également important d’utiliser l’attribut description du contenu dans votre définition de présentation pour décrire les widgets d’entrée. (Ceci est décrit plus en détail au chapitre 14.)

Pour être accessibles, les vues personnalisées doivent implémenter l'interface AccessibilityEventSource et diffuser AccessibilityEvents à l'aide de la méthode sendAccessibilityEvent.

La classe View implémente déjà l'interface de source d'événements d'accessibilité, il vous suffit donc de personnaliser le comportement pour l'adapter aux fonctionnalités introduites par votre vue personnalisée. Pour ce faire, transmettez à la méthode sendAccessibilityEvent le type d'événement survenu (généralement un clic, un clic long, une modification de sélection, une modification de la focalisation ou une modification du texte ou du contenu). Pour les vues personnalisées qui implémentent une toute nouvelle interface utilisateur, cela inclut généralement une diffusion à chaque fois que le contenu affiché change, comme illustré dans le Listing 5-16.

 LISTING 5-16: Événements d'accessibilité à la diffusion

 public void setSeason(Season season) {

  mSeason = season;

  sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); }

 Les modifications apportées aux clics, aux clics longs et au focus et à la sélection sont généralement diffusées par l'implémentation View sous-jacente, bien que vous deviez veiller à diffuser tout événement supplémentaire non capturé par la classe View de base.

L'événement d'accessibilité de diffusion comprend un certain nombre de propriétés utilisées par le service d'accessibilité pour améliorer l'expérience utilisateur. Plusieurs de ces propriétés, y compris le nom de classe de View et l’horodatage de l’événement, n’auront pas besoin d’être modifiées; Toutefois, en redéfinissant le gestionnaire dispatchPopulateAccessibilityEvent, vous pouvez personnaliser des détails tels que la représentation textuelle du contenu de la vue, l’état coché et l’état de sélection de votre vue, comme indiqué dans le Listing 5-17.

 LISTING 5-17: Personnalisation de l'accessibilité, propriétés de l'événement

@Override

public boolean dispatchPopulateAccessibilityEvent(                  final AccessibilityEvent event) {

  super.dispatchPopulateAccessibilityEvent(event);

  if (isShown()) {

    String seasonStr = Season.valueOf(season);

    if (seasonStr.length() > AccessibilityEvent.MAX_TEXT_LENGTH)

      seasonStr =         seasonStr.substring(0, AccessibilityEvent.MAX_TEXT_LENGTH-1);

    event.getText().add(seasonStr);

    return true;

  }   else

    return false; }

 Création d'un exemple d'affichage Compass

Dans l'exemple suivant, vous allez créer une nouvelle vue Compass en développant la classe View. Cette vue affichera une rose des vents traditionnelle pour indiquer un cap / une orientation. Une fois terminé, il devrait ressembler à celui de la figure 5-5.

 

 

Une boussole est un exemple de contrôle d'interface utilisateur qui nécessite un affichage radicalement différent des vues et des boutons de texte disponibles dans la boîte à outils du SDK, ce qui en fait un excellent candidat pour créer à partir de rien.

Commencez par créer un nouveau projet Compass qui contiendra votre nouveau CompassView, puis créez un CompassActivity initialement vide dans lequel l'afficher:

 1. Créez une nouvelle classe CompassView qui étend View et ajoutez des constructeurs permettant l'instanciation de la View, sous forme de code ou par gonflage à partir d'une structure de ressources. Ajoutez setFocusable (true) au constructeur final pour permettre à un utilisateur utilisant un D-pad de sélectionner et de focaliser le compas (cela leur permettra de recevoir des événements d'accessibilité depuis la vue):

package com.professionalandroid.apps.compass;

import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint;

import android.support.v4.content.ContextCompat;

import android.util.AttributeSet; import android.view.View; import android.view.accessibility.AccessibilityEvent;

public class CompassView extends View {   public CompassView(Context context) {

    this(context, null);   }

  public CompassView(Context context, AttributeSet attrs) {

    this(context, attrs, 0);   }

  public CompassView(Context context, AttributeSet attrs, int defStyleAttr) {     super(context, attrs, defStyleAttr);

    setFocusable(true);

  }

}

 2. La vue Compass doit toujours être un cercle parfait qui occupe autant de la toile que cette restriction le permet. Remplacez la méthode onMeasure pour calculer la longueur du côté le plus court et utilisez setMeasuredDimension pour définir la hauteur et la largeur à l'aide de cette valeur:

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

  // The compass is a circle that fills as much space as possible.

  // Set the measured dimensions by figuring out the shortest boundary,

  // height or width.   int measuredWidth = measure(widthMeasureSpec);   int measuredHeight = measure(heightMeasureSpec);   int d = Math.min(measuredWidth, measuredHeight);

  setMeasuredDimension(d, d); }

private int measure(int measureSpec) {   int result = 0;

  // Decode the measurement specifications.

  int specMode = MeasureSpec.getMode(measureSpec);   int specSize = MeasureSpec.getSize(measureSpec);

  if (specMode == MeasureSpec.UNSPECIFIED) {

    // Return a default size of 200 if no bounds are specified.

    result = 200;

  } else {

    // As you want to fill the available space

    // always return the full available bounds.

    result = specSize;

  }

  return result; }

 3. Modifiez la ressource de présentation activity_compass.xml et remplacez-la par une présentation de cadre contenant votre nouveau CompassView:

< ?xml version="1.0" encoding="utf-8"? >

< FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"

  android:orientation="vertical"   android:layout_width="match_parent"   android:layout_height="match_parent">

  <com.professionalandroid.apps.compass.CompassView

    android:id="@+id/compassView"     android:layout_width="match_parent"     android:layout_height="match_parent"

  />

< /FrameLayout >

  1. Utilisez des fichiers de ressources pour stocker les couleurs et les chaînes de texte que vous utiliserez pour dessiner la boussole. 

4.1. Créez les ressources de chaîne de texte en remplaçant le fichier res / values ​​/ strings.xml par ce qui suit:

< ?xml version="1.0" encoding="utf-8"? >

< resources >

  <string name="app_name">Compass</string>

  <string name="cardinal_north">N</string>

  <string name="cardinal_east">E</string>

  <string name="cardinal_south">S</string>

  <string name="cardinal_west">W</string>

< /resources >

4.2. Ajoutez les ressources de couleur suivantes à res / values ​​/ colors.xml:

< ?xml version="1.0" encoding="utf-8"? >

< resources >

  <color name="colorPrimary">#3F51B5</color>

  <color name="colorPrimaryDark">#303F9F</color>

  <color name="colorAccent">#FF4081</color>

  <color name="background_color">#F555</color>

  <color name="marker_color">#AFFF</color>

  <color name="text_color">#AFFF</color>

< /resources >

  1. Revenez à la classe CompassView. Ajoutez une nouvelle propriété pour stocker le relèvement affiché et créez des méthodes get et set pour celui-ci. Appel invalide dans la méthode set pour s'assurer que la vue est repeinte lorsque le relèvement change:

private float mBearing;

public void setBearing(float bearing) {

  mBearing = bearing;   invalidate(); }

public float getBearing() {   return mBearing; }

  1. Créez un attribut personnalisé pour définir le relèvement en XML.

6.1. Créez l'attribut personnalisé dans le fichier res / values ​​/ attrs.xml:

< ?xml version="1.0" encoding="utf-8"? >

< resources >

  <declare-styleable name="CompassView">

    <attr name="bearing" format="reference|float" />

  </declare-styleable>

< /resources >

6.2. Mettez à jour le constructeur pour lire le relèvement à partir de l'attribut XML:

public CompassView(Context context, AttributeSet attrs,

                   int defStyleAttr) {   super(context, attrs, defStyleAttr);   setFocusable(true);   final TypedArray a = context.obtainStyledAttributes(attrs,

    R.styleable.CompassView, defStyleAttr, 0);   if (a.hasValue(R.styleable.CompassView_bearing)) {

    setBearing(a.getFloat(R.styleable.CompassView_bearing, 0));

  }

  a.recycle();

}

 7.  Dans le constructeur, obtenez des références à chaque ressource créée à l'étape 4. Stockez les valeurs de chaîne en tant que variables d'instance et utilisez les valeurs de couleur pour créer de nouveaux objets Paint à l'échelle de la classe. Vous utiliserez ces objets dans l’étape suivante pour dessiner la boussole.

private Paint markerPaint; private Paint textPaint; private Paint circlePaint; private String northString; private String eastString; private String southString; private String westString; private int textHeight;

public CompassView(Context context, AttributeSet attrs, int defStyleAttr) {   super(context, attrs, defStyleAttr);

  setFocusable(true);

  final TypedArray a = context.obtainStyledAttributes(attrs,

    R.styleable.CompassView, defStyleAttr, 0);   if (a.hasValue(R.styleable.CompassView_bearing)) {

    setBearing(a.getFloat(R.styleable.CompassView_bearing, 0));

  }

  a.recycle();

  Context c = this.getContext();

  Resources r = this.getResources();

  circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);

  circlePaint.setColor(ContextCompat.getColor(c, R.color.background_color));

  circlePaint.setStrokeWidth(1);   circlePaint.setStyle(Paint.Style.FILL_AND_STROKE);

  northString = r.getString(R.string.cardinal_north);   eastString = r.getString(R.string.cardinal_east);   southString = r.getString(R.string.cardinal_south);   westString = r.getString(R.string.cardinal_west);

  textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);   textPaint.setColor(ContextCompat.getColor(c, R.color.text_color));   textHeight = (int)textPaint.measureText("yY");

  markerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

  markerPaint.setColor(ContextCompat.getColor(c, R.color.marker_color)); }

  1. L'étape suivante consiste à dessiner la face compas à l'aide des objets String et Paint créés à l'étape 7. L'extrait de code suivant est présenté avec un commentaire limité. Vous trouverez plus de détails sur les dessins sur le canevas et sur l'utilisation des effets de peinture avancés au chapitre 14.

8.1. Commencez par remplacer la méthode onDraw dans la classe CompassView:

@Override

protected void onDraw(Canvas canvas) { 

8.2. Recherchez le centre du contrôle et enregistrez la longueur du plus petit côté sous le rayon du compas:

  int mMeasuredWidth = getMeasuredWidth();   int mMeasuredHeight = getMeasuredHeight();

  int px = mMeasuredWidth / 2;   int py = mMeasuredHeight / 2 ;   int radius = Math.min(px, py);

8.3. Tracez la limite extérieure et coloriez l'arrière-plan de la face Compass à l'aide de la méthode drawCircle. Utilisez l'objet circlePaint que vous avez créé à l'étape 7:

   // Draw the background

  canvas.drawCircle(px, py, radius, circlePaint);

8.4. Ce compas affiche le cap actuel en faisant pivoter la face afin que la direction actuelle soit toujours en haut de l'appareil. Pour ce faire, faites pivoter la toile dans le sens opposé au titre actuel:

  // Rotate our perspective so that the 'top' is

  // facing the current bearing.   canvas.save();

  canvas.rotate(-mBearing, px, py);

 8.5 Il ne reste plus qu’à dessiner les marques. Faites pivoter la toile d’une rotation complète en traçant des repères tous les 15 degrés et la chaîne de direction abrégée tous les 45 degrés:

  int textWidth = (int)textPaint.measureText("W");

  int cardinalX = px-textWidth/2;   int cardinalY = py-radius+textHeight;

  // Draw the marker every 15 degrees and text every 45.

  for (int i = 0; i < 24; i++) {

    // Draw a marker.     canvas.drawLine(px, py-radius, px, py-radius+10, markerPaint);

    canvas.save();     canvas.translate(0, textHeight);

    // Draw the cardinal points

    if (i % 6 == 0) {

      String dirString = "";

      switch (i) {         case(0)  : {

                     dirString = northString;                      int arrowY = 2*textHeight;

                     canvas.drawLine(px, arrowY, px-5, 3*textHeight,

                                     markerPaint);

                     canvas.drawLine(px, arrowY, px+5, 3*textHeight,

                                     markerPaint);

                     break;

                   }

        case(6)  : dirString = eastString; break;         case(12) : dirString = southString; break;         case(18) : dirString = westString; break;

      }

      canvas.drawText(dirString, cardinalX, cardinalY, textPaint);     }

    else if (i % 3 == 0) {

      // Draw the text every alternate 45deg

      String angle = String.valueOf(i*15);       float angleTextWidth = textPaint.measureText(angle);

      int angleTextX = (int)(px-angleTextWidth/2);       int angleTextY = py-radius+textHeight;

      canvas.drawText(angle, angleTextX, angleTextY, textPaint);

    }     canvas.restore();

    canvas.rotate(15, px, py);

  }

  canvas.restore(); }

 9. L'étape suivante consiste à ajouter la prise en charge de l'accessibilité. La vue Boussole présente un titre de manière visuelle. Pour le rendre accessible, vous devez donc diffuser un événement d'accessibilité indiquant que le «texte» (dans ce cas, le contenu) a changé lorsque le relèvement a été modifié. Faites ceci en modifiant la méthode setBearing:

  public void setBearing(float bearing) {

  mBearing = bearing;   invalidate();   sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); }

 10. Remplacez dispatchPopulateAccessibilityEvent pour utiliser l'en-tête actuel en tant que valeur de contenu à utiliser pour les événements d'accessibilité:

 @Override

public boolean dispatchPopulateAccessibilityEvent(                  final AccessibilityEvent event) {   super.dispatchPopulateAccessibilityEvent(event);

  if (isShown()) {

    String bearingStr = String.valueOf(mBearing);

    event.getText().add(bearingStr);

    return true;

  }   else

    return false; }

 Exécutez l'activité et vous devriez voir le CompassView affiché. Reportez-vous au chapitre 16, "Capteurs matériels", pour savoir comment lier CompassView au capteur de compas du périphérique.

 Utilisation de contrôles personnalisés

 Après avoir créé vos propres vues personnalisées, vous pouvez les utiliser dans le code et les mises en page comme vous le feriez pour toute autre vue. Notez que vous devez spécifier le nom de classe complet lorsque vous ajoutez un nœud pour votre nouvelle vue dans la définition de présentation:

 < com.professionalandroid.apps.compass.CompassView

  android:id="@+id/compassView"   android:layout_width="match_parent"   android:layout_height="match_parent"   app:bearing="45" />

 Vous pouvez gonfler la présentation et obtenir une référence à CompassView, comme d'habitude, à l'aide du code suivant:

 @Override

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

  CompassView cv = findViewById(R.id.compassView);   // Update the bearing by calling setBearing as needed

}

 Vous pouvez également ajouter votre nouvelle vue à une présentation en code:

 @Override

public void onCreate(Bundle savedInstanceState) {   super.onCreate(savedInstanceState);

  CompassView cv = new CompassView(this);

  setContentView(cv);   cv.setBearing(45); }

Les vues personnalisées constituent un moyen puissant d’offrir des fonctionnalités distinctes à votre application. Une fois créés, ils peuvent être utilisés de la même manière que n'importe quelle vue Android Framework.