La parte più importante di una visualizzazione personalizzata è il suo aspetto. Il disegno personalizzato può essere semplice o complesso a seconda delle esigenze dell'applicazione. Questo documento illustra alcune delle operazioni più comuni.
Per maggiori informazioni, consulta la panoramica dei disegnabili.
Eseguire l'override di onDraw()
Il passaggio più importante per disegnare una visualizzazione personalizzata è eseguire l'override del metodo onDraw(). Il parametro di onDraw() è un oggetto Canvas che la visualizzazione può utilizzare per disegnarsi. La classe Canvas definisce i metodi per disegnare testo, linee, bitmap e molte altre primitive grafiche. Puoi utilizzare questi metodi in onDraw() per creare la tua interfaccia utente (UI) personalizzata.
Inizia creando un oggetto Paint.
La sezione successiva descrive Paint in modo più dettagliato.
Creare oggetti di disegno
Il framework android.graphics divide il disegno in due aree:
- Cosa disegnare, gestito da
Canvas. - Come disegnare, gestito da
Paint.
Ad esempio, Canvas fornisce un metodo per disegnare una linea e Paint fornisce metodi per definire il colore della linea.
Canvas ha un metodo per disegnare un rettangolo e Paint definisce se riempire il rettangolo con un colore o lasciarlo vuoto.
Canvas definisce le forme che puoi disegnare sullo schermo e Paint definisce il colore, lo stile, il carattere e così via di ogni forma che disegni.
Prima di disegnare qualsiasi cosa, crea uno o più oggetti Paint. L'esempio seguente lo fa in un metodo chiamato init. Questo metodo viene chiamato dal costruttore da Java, ma può essere inizializzato in linea in 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 creazione di oggetti in anticipo è un'ottimizzazione importante. Le visualizzazioni vengono ridisegnate di frequente e molti oggetti di disegno richiedono un'inizializzazione costosa.
La creazione di oggetti di disegno all'interno del metodo onDraw() riduce significativamente le prestazioni e può rendere l'UI lenta.
Gestire gli eventi di layout
Per disegnare correttamente la visualizzazione personalizzata, scopri le sue dimensioni. Le visualizzazioni personalizzate complesse spesso devono eseguire più calcoli di layout a seconda delle dimensioni e della forma della loro area sullo schermo. Non fare mai ipotesi sulle dimensioni della visualizzazione sullo schermo. Anche se una sola app utilizza la tua visualizzazione, questa deve gestire diverse dimensioni dello schermo, più densità dello schermo e vari formati sia in modalità verticale sia orizzontale.
Sebbene View abbia molti metodi per la gestione della misurazione, la maggior parte di essi non deve essere sottoposta a override. Se la visualizzazione non richiede un controllo speciale sulle sue dimensioni, esegui l'override di un solo metodo: onSizeChanged().
onSizeChanged() viene chiamato quando alla visualizzazione viene assegnata una dimensione per la prima volta e di nuovo se la dimensione della visualizzazione cambia per qualsiasi motivo. Calcola le posizioni, le dimensioni e qualsiasi altro valore correlato alle dimensioni della visualizzazione in onSizeChanged(), anziché ricalcolarli ogni volta che disegni.
Nell'esempio seguente, onSizeChanged() è il punto in cui la visualizzazione calcola il rettangolo di delimitazione del grafico e la posizione relativa dell'etichetta di testo e di altri elementi visivi.
Quando alla visualizzazione viene assegnata una dimensione, il gestore del layout presuppone che la dimensione includa il padding della visualizzazione. Gestisci i valori di padding quando calcoli le dimensioni della visualizzazione. Ecco uno snippet di onSizeChanged() che mostra come farlo:
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); }
Se hai bisogno di un controllo più preciso sui parametri di layout della visualizzazione, implementa onMeasure().
I parametri di questo metodo sono valori View.MeasureSpec che indicano le dimensioni che il parent della visualizzazione vuole che la visualizzazione abbia e se questa dimensione è un massimo rigido o solo un suggerimento. Per ottimizzare, questi valori vengono memorizzati come numeri interi compressi e utilizzi i metodi statici di View.MeasureSpec per decomprimere le informazioni memorizzate in ogni numero intero.
Ecco un'implementazione di esempio di onMeasure(). In questa implementazione, tenta di rendere la sua area abbastanza grande da rendere il grafico grande quanto la sua etichetta:
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); }
In questo codice ci sono tre cose importanti da notare:
- I calcoli tengono conto del padding della visualizzazione. Come accennato in precedenza, questa è responsabilità della visualizzazione.
- Il metodo helper
resolveSizeAndState()viene utilizzato per creare i valori finali di larghezza e altezza. Questo helper restituisce un valoreView.MeasureSpecappropriato confrontando le dimensioni necessarie della visualizzazione con il valore passato aonMeasure(). onMeasure()non ha un valore restituito. Il metodo comunica invece i risultati chiamandosetMeasuredDimension(). La chiamata a questo metodo è obbligatoria. Se ometti questa chiamata, laViewclasse genera un'eccezione di runtime.
Disegnare
Dopo aver definito il codice di creazione e misurazione degli oggetti, puoi implementare onDraw(). Ogni visualizzazione implementa onDraw() in modo diverso, ma esistono alcune operazioni comuni che la maggior parte delle visualizzazioni condivide:
- Disegna il testo utilizzando
drawText(). Specifica il tipo di carattere chiamandosetTypeface()e il colore del testo chiamandosetColor(). - Disegna forme primitive utilizzando
drawRect(),drawOval(), edrawArc(). Modifica se le forme sono riempite, contornate o entrambe chiamandosetStyle(). - Disegna forme più complesse utilizzando la
Pathclasse. Definisci una forma aggiungendo linee e curve a unPathoggetto, quindi disegna la forma utilizzandodrawPath(). Come per le forme primitive, i tracciati possono essere contornati, riempiti o entrambi, a seconda disetStyle(). -
Definisci i riempimenti sfumati creando
LinearGradientoggetti. ChiamasetShader()per utilizzareLinearGradientsulle forme riempite. - Disegna bitmap utilizzando
drawBitmap().
Il codice seguente disegna un mix di testo, linee e forme:
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; }
Applicare effetti grafici
Android 12 (livello API 31) aggiunge la classe RenderEffect, che applica effetti grafici comuni come sfocature, filtri colore, effetti shader Android e altro ancora agli oggetti View e alle gerarchie di rendering. Puoi combinare gli effetti come effetti a catena, che consistono in un effetto interno ed esterno, o effetti combinati. Il supporto per questa funzionalità varia a seconda della potenza di elaborazione del dispositivo.
Puoi anche applicare effetti a sottostante
RenderNode per
una View chiamando
View.setRenderEffect(RenderEffect).
Per implementare un oggetto RenderEffect:
view.setRenderEffect(RenderEffect.createBlurEffect(radiusX, radiusY, SHADER_TILE_MODE))
Puoi creare la visualizzazione in modo programmatico o espanderla da un layout XML e
recuperarla utilizzando il binding della visualizzazione o
findViewById().