Créer un dessin personnalisé

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

L'aspect d'une vue personnalisée est la partie la plus importante. Le dessin personnalisé peut être simple ou complexe selon les besoins de votre application. Ce document couvre certaines des opérations les plus courantes.

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

Remplacer onDraw()

L'étape la plus importante pour dessiner 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 se dessiner. La classe Canvas définit des méthodes pour dessiner du texte, des lignes, des bitmaps et de nombreux autres éléments graphiques primitifs. Vous pouvez utiliser ces méthodes dans onDraw() pour créer votre interface utilisateur (UI) personnalisée.

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

Créer des objets de dessin

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

  • Ce qu'il faut dessiner, géré par Canvas.
  • Comment dessiner, géré par Paint.

Par exemple, Canvas fournit une méthode pour dessiner 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éfinit s'il faut remplir ce rectangle avec une couleur ou le laisser vide. Canvas définit les formes que vous pouvez dessiner à l'écran, et Paint définit la couleur, le style, la police, etc. de chaque forme que vous dessinez.

Avant de dessiner quoi que ce soit, 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 Java, mais elle peut être initialisée en ligne 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));
   ...
}

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

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 comporte de nombreuses méthodes de gestion des mesures, la plupart d'entre elles n'ont pas besoin d'être remplacées. Si votre vue n'a pas besoin d'un contrôle spécial sur sa taille, ne remplacez qu'une seule méthode : onSizeChanged().

onSizeChanged() est appelé lorsque votre vue est attribuée pour la première fois à une taille, et à nouveau si la taille de votre vue 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 englobant du graphique et la position relative du libellé de texte et des autres éléments visuels.

Lorsque vous attribuez une taille à votre vue, le gestionnaire de mise en page suppose que cette taille 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 une simple suggestion. Pour optimiser le processus, ces valeurs sont stockées sous forme d'entiers compressés. Vous pouvez utiliser 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 de rendre sa zone suffisamment grande 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);
}

Voici trois points importants à noter dans ce code :

  • Les calculs tiennent compte de la marge intérieure de la vue. Comme mentionné précédemment, c'est la responsabilité de la vue.
  • La méthode d'assistance resolveSizeAndState() est utilisée pour créer les valeurs finales de largeur et de hauteur. Cet utilitaire renvoie une valeur View.MeasureSpec appropriée en comparant la taille requise de la vue à la valeur transmise à onMeasure().
  • onMeasure() ne renvoie aucune valeur. 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.

Dessiner

Une fois que vous avez défini votre code de création et de mesure d'objet, vous pouvez implémenter onDraw(). Chaque vue implémente onDraw() différemment, mais la plupart des vues partagent certaines opérations courantes :

  • Dessinez du texte à l'aide de drawText(). Spécifiez la typographie en appelant setTypeface() et la couleur du texte en appelant setColor().
  • Dessinez des formes primitives à l'aide de drawRect(), drawOval() et drawArc(). Déterminez si les formes sont remplies, décrites 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 chemins peuvent être décrits, remplis ou les deux, selon setStyle().
  • Définissez les dégradés en créant des objets LinearGradient. Appelez setShader() pour utiliser votre LinearGradient sur les formes remplies.
  • 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 des flous, des filtres de couleur, des effets de nuanceur Android, etc., aux objets View et aux hiérarchies de rendu. Vous pouvez combiner des effets sous forme d'effets en chaîne, qui se composent d'un effet intérieur et d'un effet extérieur, ou d'effets combinés. La compatibilité de cette fonctionnalité varie en fonction de la puissance de traitement de l'appareil.

Vous pouvez également appliquer des effets à l'RenderNode sous-jacent d'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, puis la récupérer à l'aide de View Binding ou findViewById().