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 valeurView.MeasureSpecapproprié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 appelantsetMeasuredDimension(). L'appel de cette méthode est obligatoire. Si vous omettez cet appel, la classeViewgé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 appelantsetTypeface()et la couleur du texte en appelantsetColor(). - Dessinez des formes primitives à l'aide de
drawRect(),drawOval()etdrawArc(). Déterminez si les formes sont remplies, décrites ou les deux en appelantsetStyle(). - Dessinez des formes plus complexes à l'aide de la classe
Path. Définissez une forme en ajoutant des lignes et des courbes à un objetPath, puis dessinez la forme à l'aide dedrawPath(). Comme pour les formes primitives, les chemins peuvent être décrits, remplis ou les deux, selonsetStyle(). -
Définissez les dégradés en créant des objets
LinearGradient. AppelezsetShader()pour utiliser votreLinearGradientsur 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().