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.
<ph type="x-smartling-placeholder"></ph> Dessin dans Compose →

La partie la plus importante d'une vue personnalisée est son apparence. Dessin personnalisé peuvent être simples ou complexes selon les besoins de votre application. Ce document certaines des opérations les plus courantes.

Pour en savoir plus, consultez Présentation des drawables

Ignorer onDraw()

L'étape la plus importante du dessin d'une vue personnalisée consiste à remplacer la onDraw() . Le paramètre de onDraw() est un Canvas que la vue peut utiliser pour elle-même. La classe Canvas définit des méthodes pour dessiner du texte, des lignes, des bitmaps et de nombreux autres éléments graphiques. primitives. Vous pouvez utiliser ces méthodes dans onDraw() pour créer vos personnalisée.

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

Créer des objets de dessin

La android.graphics divise le dessin en deux zones:

  • Objet à dessiner, géré par Canvas.
  • Comment dessiner, géré par Paint.

Par exemple, Canvas fournit une méthode pour tracer une ligne. 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. Paint définit la couleur, le style, la police, etc. de chaque forme lorsque vous dessinez.

Avant de dessiner, créez un ou plusieurs objets Paint. La L'exemple suivant le fait dans une méthode appelée init. Cette méthode est appelé à partir du constructeur depuis Java, mais il peut être initialisé 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 redessinées fréquemment, et de nombreux objets de dessin nécessitent une initialisation coûteuse. Création importante d'objets de dessin dans la méthode onDraw() ce qui réduit 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. Personnalisé complexe les vues doivent souvent effectuer plusieurs calculs de mise en page en fonction de la taille et la forme de sa zone à l'écran. Ne faites jamais de suppositions sur la taille à l'écran. Même si une seule application utilise votre vue, cette application doit gèrent différentes tailles et densités d'écran, en mode portrait et paysage.

Bien que View propose de nombreuses méthodes pour gérer les mesures, la plupart n'ont pas besoin d'être être remplacées. Si vous n'avez pas besoin d'exercer un contrôle spécial sur la taille de la vue, ignorer une méthode: onSizeChanged()

onSizeChanged() est appelé lorsque vous recevez pour la première fois un et encore une fois si la taille de la vue change pour une raison quelconque. Calculer les positions, les dimensions et toute autre valeur liée à la taille de la vue dans onSizeChanged(), au lieu de les recalculer chaque fois que vous dessinez. Dans l'exemple suivant, onSizeChanged() est l'endroit où la vue calcule le rectangle de délimitation du graphique et la position relative du une étiquette de texte et d'autres éléments visuels.

Lorsque vous attribuez une taille à votre vue, le gestionnaire de mises 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 votre la taille de l'affichage. Voici un extrait de onSizeChanged() qui montre comment en procédant comme suit:

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 View.MeasureSpec qui indiquent la taille souhaitée par le parent de la vue, et qu'il s'agisse d'un maximum strict ou d'une simple suggestion. À titre d'optimisation, ces valeurs sont stockées sous forme d'entiers empaquetés et vous utilisez les méthodes statiques View.MeasureSpec pour désimbriquer les informations stockées dans chaque entier.

Voici un exemple d'implémentation de onMeasure(). Dans ce il essaie de faire en sorte que le graphique soit aussi grand comme 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);
}

Trois points importants sont à noter dans ce code:

  • Les calculs tiennent compte de la marge intérieure de la vue. Comme indiqué précédemment il s'agit de la responsabilité de la vue.
  • Méthode d'assistance resolveSizeAndState() est utilisé pour créer les valeurs finales de largeur et de hauteur. Cet outil d'aide renvoie une valeur View.MeasureSpec appropriée en comparant les la taille requise de la vue à la valeur transmise à onMeasure().
  • onMeasure() n'a pas de valeur renvoyée. La méthode communique ses résultats en appelant setMeasuredDimension() Vous devez obligatoirement appeler cette méthode. Si vous omettez cet appel, le La classe View génère une exception d'exécution.

Dessiner

Après avoir défini votre code de création d'objet et de mesure, vous pouvez implémenter onDraw() Chaque vue implémente onDraw() différemment, mais il existe quelques opérations courantes que la plupart des vues partagent:

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

Le code suivant trace 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 RenderEffect , qui applique des effets graphiques courants tels que le floutage, les filtres de couleur, les effets du nuanceur Android, et plus encore Objets View et de rendu. Vous pouvez combiner plusieurs effets sous forme d'effets de chaîne, qui consistent d'un effet intérieur et extérieur, ou d'effets mixtes. Compatibilité avec cette fonctionnalité varie en fonction de la puissance de traitement de l'appareil.

Vous pouvez aussi appliquer des effets aux RenderNode 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 à les récupérer à l'aide de la fonctionnalité View Binding . findViewById().