Créer un dessin personnalisé

Essayer Compose
Jetpack Compose est le kit d'outils d'interface utilisateur recommandé pour Android. Découvrez comment utiliser les mises en page dans Compose.

L'aspect le plus important d'une vue personnalisée est son apparence. Le dessin personnalisé peut être facile ou complexe selon les besoins de votre application. Ce document couvre certaines des opérations les plus courantes.

Pour en savoir plus, consultez la section Présentation des drawables.

Remplacer onDraw()

L'étape la plus importante du dessin d'une vue personnalisée consiste à remplacer la méthode onDraw(). Le paramètre de onDraw() est un objet Canvas que la vue peut utiliser pour dessiner. La classe Canvas définit des méthodes pour dessiner du texte, des lignes, des bitmaps et de nombreuses autres primitives graphiques. Vous pouvez utiliser ces méthodes dans onDraw() pour créer votre interface utilisateur personnalisée.

Commencez par créer un objet Paint. La section suivante traite de Paint plus en détail.

Créer des objets de dessin

Le framework android.graphics divise le dessin en deux domaines:

  • Élément à dessiner, géré par Canvas.
  • Comment dessiner, géré par Paint.

Par exemple, Canvas fournit une méthode pour tracer une ligne, et Paint fournit des méthodes pour définir la couleur de cette ligne. Canvas dispose d'une méthode pour dessiner un rectangle, et Paint détermine s'il faut remplir ce rectangle avec une couleur ou le laisser vide. Canvas définit les formes que vous pouvez dessiner à l'écran, tandis que Paint définit la couleur, le style, la police, etc. de chaque forme que vous dessinez.

Avant de dessiner, créez un ou plusieurs objets Paint. L'exemple suivant effectue cette opération dans une méthode appelée init. Cette méthode est appelée à partir du constructeur de Java, mais elle peut être initialisée de manière intégrée dans Kotlin.

Kotlin

@ColorInt
private var textColor    // Obtained from style attributes.

@Dimension
private var textHeight   // Obtained from style attributes.

private val textPaint = Paint(ANTI_ALIAS_FLAG).apply {
    color = textColor
    if (textHeight == 0f) {
        textHeight = textSize
    } else {
        textSize = textHeight
    }
}

private val piePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    style = Paint.Style.FILL
    textSize = textHeight
}

private val shadowPaint = Paint(0).apply {
    color = 0x101010
    maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.NORMAL)
}

Java

private Paint textPaint;
private Paint piePaint;
private Paint shadowPaint;

@ColorInt
private int textColor;       // Obtained from style attributes.

@Dimension
private float textHeight;    // Obtained from style attributes.

private void init() {
   textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   textPaint.setColor(textColor);
   if (textHeight == 0) {
       textHeight = textPaint.getTextSize();
   } else {
       textPaint.setTextSize(textHeight);
   }

   piePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   piePaint.setStyle(Paint.Style.FILL);
   piePaint.setTextSize(textHeight);

   shadowPaint = new Paint(0);
   shadowPaint.setColor(0xff101010);
   shadowPaint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL));
   ...
}

Créer des objets à l'avance est une optimisation importante. Les vues sont fréquemment redessinées, et de nombreux objets de dessin nécessitent une initialisation coûteuse. La création d'objets de dessin dans la méthode onDraw() réduit considérablement les performances et peut ralentir l'interface utilisateur.

Gérer les événements de mise en page

Pour dessiner correctement votre vue personnalisée, déterminez sa taille. Les vues personnalisées complexes doivent souvent effectuer plusieurs calculs de mise en page en fonction de la taille et de la forme de leur zone à l'écran. Ne faites jamais d'hypothèses sur la taille de votre vue à l'écran. Même si une seule application utilise votre vue, elle doit gérer différentes tailles d'écran, plusieurs densités d'écran et différents formats en mode portrait et paysage.

Bien que View propose de nombreuses méthodes pour gérer les mesures, la plupart d'entre elles n'ont pas besoin d'être remplacées. Si votre affichage ne nécessite pas de contrôle particulier sur sa taille, ne remplacez qu'une seule méthode : onSizeChanged().

onSizeChanged() est appelé lorsqu'une taille est attribuée pour la première fois à votre vue, puis à nouveau si sa taille change pour une raison quelconque. Calculez les positions, les dimensions et toute autre valeur liée à la taille de votre vue dans onSizeChanged(), au lieu de les recalculer chaque fois que vous dessinez. Dans l'exemple suivant, onSizeChanged() correspond à l'endroit où la vue calcule le rectangle de délimitation du graphique, ainsi que la position relative du libellé textuel et des autres éléments visuels.

Lorsqu'une taille est attribuée à votre vue, le gestionnaire de mises en page suppose que celle-ci inclut la marge intérieure de la vue. Gérez les valeurs de marge intérieure lorsque vous calculez la taille de votre vue. Voici un extrait de onSizeChanged() qui montre comment procéder:

Kotlin

private val showText    // Obtained from styled attributes.
private val textWidth   // Obtained from styled attributes.

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    // Account for padding.
    var xpad = (paddingLeft + paddingRight).toFloat()
    val ypad = (paddingTop + paddingBottom).toFloat()

    // Account for the label.
    if (showText) xpad += textWidth.toFloat()
    val ww = w.toFloat() - xpad
    val hh = h.toFloat() - ypad

    // Figure out how big you can make the pie.
    val diameter = Math.min(ww, hh)
}

Java

private Boolean showText;    // Obtained from styled attributes.
private int textWidth;       // Obtained from styled attributes.

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    // Account for padding.
    float xpad = (float)(getPaddingLeft() + getPaddingRight());
    float ypad = (float)(getPaddingTop() + getPaddingBottom());

    // Account for the label.
    if (showText) xpad += textWidth;

    float ww = (float)w - xpad;
    float hh = (float)h - ypad;

    // Figure out how big you can make the pie.
    float diameter = Math.min(ww, hh);
}

Si vous avez besoin d'un contrôle plus précis sur les paramètres de mise en page de votre vue, implémentez onMeasure(). Les paramètres de cette méthode sont des valeurs View.MeasureSpec qui vous indiquent la taille souhaitée par le parent de votre vue, et si cette taille est un maximum strict ou simplement une suggestion. À des fins d'optimisation, ces valeurs sont stockées sous forme d'entiers empaquetés, et vous utilisez les méthodes statiques de View.MeasureSpec pour décompresser les informations stockées dans chaque entier.

Voici un exemple d'implémentation de onMeasure(). Dans cette implémentation, il tente d'agrandir sa surface pour que le graphique soit aussi grand que son libellé:

Kotlin

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    // Try for a width based on your minimum.
    val minw: Int = paddingLeft + paddingRight + suggestedMinimumWidth
    val w: Int = View.resolveSizeAndState(minw, widthMeasureSpec, 1)

    // Whatever the width is, ask for a height that lets the pie get as big as
    // it can.
    val minh: Int = View.MeasureSpec.getSize(w) - textWidth.toInt() + paddingBottom + paddingTop
    val h: Int = View.resolveSizeAndState(minh, heightMeasureSpec, 0)

    setMeasuredDimension(w, h)
}

Java

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   // Try for a width based on your minimum.
   int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
   int w = resolveSizeAndState(minw, widthMeasureSpec, 1);

   // Whatever the width is, ask for a height that lets the pie get as big as it
   // can.
   int minh = MeasureSpec.getSize(w) - (int)textWidth + getPaddingBottom() + getPaddingTop();
   int h = resolveSizeAndState(minh, heightMeasureSpec, 0);

   setMeasuredDimension(w, h);
}

Il y a trois points importants à noter dans ce code:

  • Les calculs tiennent compte de la marge intérieure de la vue. Comme indiqué précédemment, cette responsabilité incombe à la vue.
  • La méthode d'assistance resolveSizeAndState() permet de créer les valeurs finales de largeur et de hauteur. Cet outil d'aide renvoie une valeur View.MeasureSpec appropriée en comparant la taille requise pour la vue à la valeur transmise dans onMeasure().
  • Aucune valeur renvoyée pour onMeasure(). Au lieu de cela, la méthode communique ses résultats en appelant setMeasuredDimension(). L'appel de cette méthode est obligatoire. Si vous omettez cet appel, la classe View génère une exception d'exécution.

Dessin

Après avoir défini la création de vos objets et mesuré le code, vous pouvez implémenter onDraw(). Chaque vue implémente onDraw() différemment, mais la plupart des vues partagent certaines opérations:

  • Dessinez du texte avec drawText(). Spécifiez la police de caractères en appelant setTypeface() et la couleur du texte en appelant setColor().
  • Dessinez des formes primitives à l'aide de drawRect(), drawOval() et drawArc(). Indiquez si les formes sont remplies, avec contours ou les deux en appelant setStyle().
  • Dessinez des formes plus complexes à l'aide de la classe Path. Définissez une forme en ajoutant des lignes et des courbes à un objet Path, puis dessinez la forme à l'aide de drawPath(). Comme pour les formes primitives, les tracés peuvent être délimités, remplis ou les deux, en fonction de setStyle().
  • Définissez des remplissages en dégradé en créant des objets LinearGradient. Appelez setShader() pour utiliser votre LinearGradient sur des formes pleines.
  • Dessinez des bitmaps à l'aide de drawBitmap().

Le code suivant dessine un mélange de texte, de lignes et de formes:

Kotlin

private val data = mutableListOf<Item>() // A list of items that are displayed.

private var shadowBounds = RectF()       // Calculated in onSizeChanged.
private var pointerRadius: Float = 2f    // Obtained from styled attributes.
private var pointerX: Float = 0f         // Calculated in onSizeChanged.
private var pointerY: Float = 0f         // Calculated in onSizeChanged.
private var textX: Float = 0f            // Calculated in onSizeChanged.
private var textY: Float = 0f            // Calculated in onSizeChanged.
private var bounds = RectF()             // Calculated in onSizeChanged.
private var currentItem: Int = 0         // The index of the currently selected item.

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)

    canvas.apply {
        // Draw the shadow.
        drawOval(shadowBounds, shadowPaint)

        // Draw the label text.
        drawText(data[currentItem].label, textX, textY, textPaint)

        // Draw the pie slices.
        data.forEach {item ->
            piePaint.shader = item.shader
            drawArc(
                bounds,
                360 - item.endAngle,
                item.endAngle - item.startAngle,
                true,
                piePaint
            )
        }

        // Draw the pointer.
        drawLine(textX, pointerY, pointerX, pointerY, textPaint)
        drawCircle(pointerX, pointerY, pointerRadius, textPaint)
    }
}

// Maintains the state for a data item.
private data class Item(
    var label: String,      
    var value: Float = 0f,

    @ColorInt
    var color: Int = 0,

    // Computed values.
    var startAngle: Float = 0f,
    var endAngle: Float = 0f,

    var shader: Shader
)

Java

private List<Item> data = new ArrayList<Item>();  // A list of items that are displayed.

private RectF shadowBounds;                       // Calculated in onSizeChanged.
private float pointerRadius;                      // Obtained from styled attributes.
private float pointerX;                           // Calculated in onSizeChanged.
private float pointerY;                           // Calculated in onSizeChanged.
private float textX;                              // Calculated in onSizeChanged.
private float textY;                              // Calculated in onSizeChanged.
private RectF bounds;                             // Calculated in onSizeChanged.
private int currentItem = 0;                      // The index of the currently selected item.

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    // Draw the shadow.
    canvas.drawOval(
            shadowBounds,
            shadowPaint
    );

    // Draw the label text.
    canvas.drawText(data.get(currentItem).label, textX, textY, textPaint);

    // Draw the pie slices.
    for (int i = 0; i < data.size(); ++i) {
        Item it = data.get(i);
        piePaint.setShader(it.shader);
        canvas.drawArc(
                bounds,
                360 - it.endAngle,
                it.endAngle - it.startAngle,
                true, 
                piePaint
        );
    }

    // Draw the pointer.
    canvas.drawLine(textX, pointerY, pointerX, pointerY, textPaint);
    canvas.drawCircle(pointerX, pointerY, pointerRadius, textPaint);
}

// Maintains the state for a data item.
private class Item {
    public String label;
    public float value;
    @ColorInt
    public int color;

    // Computed values.
    public int startAngle;
    public int endAngle;

    public Shader shader;
}    

Appliquer des effets graphiques

Android 12 (niveau d'API 31) ajoute la classe RenderEffect, qui applique des effets graphiques courants tels que les flous, les filtres de couleur, les effets du nuanceur Android et plus encore aux objets View et aux hiérarchies de rendu. Vous pouvez combiner des effets sous forme d'effets de chaîne, qui consistent en un effet interne et un effet externe, ou des effets combinés. La prise en charge de cette fonctionnalité varie en fonction de la puissance de traitement de l'appareil.

Vous pouvez également appliquer des effets au RenderNode sous-jacent pour un View en appelant View.setRenderEffect(RenderEffect).

Pour implémenter un objet RenderEffect, procédez comme suit:

view.setRenderEffect(RenderEffect.createBlurEffect(radiusX, radiusY, SHADER_TILE_MODE))

Vous pouvez créer la vue par programmation, ou la gonfler à partir d'une mise en page XML et la récupérer à l'aide de la liaison de vue ou de findViewById().