L'aspetto più importante di una visualizzazione personalizzata è il suo aspetto. Il disegno personalizzato può essere facile o complesso, a seconda delle esigenze della tua applicazione. Questo documento descrive alcune delle operazioni più comuni.
Per ulteriori informazioni, consulta la panoramica di Drawables.
Esegui override di onDraw()
Il passaggio più importante per disegnare una vista personalizzata è eseguire l'override del metodo onDraw()
. Il parametro per onDraw()
è un oggetto Canvas
che la vista 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.
Per iniziare, crea un
oggetto Paint
.
Nella prossima sezione parleremo di Paint
in modo più dettagliato.
Creare oggetti di disegno
Il framework android.graphics
suddivide il disegno in due aree:
- Cosa disegnare, gestito da
Canvas
. - Come disegnare, gestita da
Paint
.
Ad esempio, Canvas
fornisce un metodo per tracciare una linea e
Paint
fornisce metodi per definire il colore di tale linea.
Canvas
dispone di 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, mentre
Paint
definisce il colore, lo stile, il carattere e così via di ogni forma
che tracci.
Prima di disegnare qualsiasi cosa, crea uno o più oggetti Paint
. Nell'esempio seguente viene utilizzato 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 anticipata di oggetti è un'ottimizzazione importante. Le viste vengono
ridisegnate spesso e molti oggetti di disegno richiedono un'inizializzazione costosa.
La creazione di oggetti di disegno all'interno del metodo onDraw()
riduce notevolmente le prestazioni e può rendere l'interfaccia utente lenta.
Gestire gli eventi di layout
Per disegnare correttamente la visualizzazione personalizzata, determina le sue dimensioni. Spesso, le visualizzazioni personalizzate complesse richiedono più calcoli del layout a seconda delle dimensioni e della forma dell'area sullo schermo. Non dare mai ipotesi sulle dimensioni della visualizzazione sullo schermo. Anche se la visualizzazione viene utilizzata da una sola app, questa deve gestire diverse dimensioni dello schermo, diverse densità dello schermo e diverse proporzioni sia in modalità verticale che orizzontale.
Anche se View
ha diversi metodi per gestire la misurazione, la maggior parte di questi non è necessario
eseguire l'override. Se la visualizzazione non richiede un controllo speciale sulle sue dimensioni, esegui solo l'override di un metodo: onSizeChanged()
.
onSizeChanged()
viene richiamato quando viene assegnata per la prima volta una
dimensione alla visualizzazione e di nuovo se, per qualsiasi motivo, la dimensione della visualizzazione cambia. Calcola posizioni, dimensioni e qualsiasi altro valore relativo alle dimensioni della tua vista in onSizeChanged()
, invece di ricalcolarli ogni volta che disegni.
Nell'esempio seguente, onSizeChanged()
è il punto in cui la vista 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 di layout presuppone che la dimensione includa la spaziatura interna della visualizzazione. Gestire i valori di spaziatura interna quando calcoli le dimensioni della vista. Ecco uno snippet di onSizeChanged()
che mostra come fare:
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 vista, implementa
onMeasure()
.
I parametri di questo metodo sono
View.MeasureSpec
valori che indicano quanto l'elemento padre della tua vista vuole che sia la tua vista e
se si tratta di un valore massimo fisso o solo di un suggerimento. Per ottimizzazione, questi valori vengono memorizzati come numeri interi compressi e utilizzi i metodi statici di View.MeasureSpec
per estrarre le informazioni archiviate in ogni numero intero.
Di seguito è riportato un esempio di implementazione di onMeasure()
. In questa implementazione, cerca di rendere l'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); }
Ci sono tre aspetti importanti da notare in questo codice:
- I calcoli tengono conto della spaziatura interna della visualizzazione. Come accennato in precedenza, questa è la responsabilità dell'opinione.
- Il metodo helper
resolveSizeAndState()
viene utilizzato per creare i valori finali di larghezza e altezza. Questo helper restituisce un valoreView.MeasureSpec
appropriato confrontando la dimensione necessaria della visualizzazione con il valore trasmesso inonMeasure()
. onMeasure()
non ha un valore restituito. Il metodo comunica invece i risultati chiamandosetMeasuredDimension()
. È obbligatorio chiamare questo metodo. Se ometti questa chiamata, la classeView
genera un'eccezione di runtime.
Disegna
Dopo aver definito il codice per la creazione e la misurazione degli oggetti, puoi implementare
onDraw()
. Ogni vista implementa onDraw()
in modo diverso,
ma ci sono alcune operazioni comuni condivise dalla maggior parte delle visualizzazioni:
- Disegna testo usando
drawText()
. Specifica il carattere richiamandosetTypeface()
e il colore del testo chiamandosetColor()
. - Disegna forme primitive utilizzando
drawRect()
,drawOval()
edrawArc()
. Cambia se le forme sono riempite, con contorni o entrambi chiamandosetStyle()
. - Disegnare forme più complesse utilizzando la
classe
Path
. Definisci una forma aggiungendo linee e curve a un oggettoPath
, quindi disegna la forma condrawPath()
. Come per le forme primitive, i percorsi possono essere contornati, riempiti o entrambi, a seconda disetStyle()
. -
Definisci i riempimenti a gradiente creando
LinearGradient
oggetti. ChiamasetShader()
per utilizzareLinearGradient
sulle forme colorate. - Disegna bitmap utilizzando
drawBitmap()
.
Il seguente codice disegna una combinazione 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 colorati, effetti Shadr Android e altro ancora a View
oggetti e gerarchie di rendering. Puoi combinare gli effetti sotto forma di catena, costituita da un effetto interno ed esterno, o da effetti misti. Il supporto di questa funzionalità varia a seconda della potenza di elaborazione del dispositivo.
Puoi anche applicare effetti all'elemento RenderNode
sottostante per
una View
chiamando
View.setRenderEffect(RenderEffect)
.
Per implementare un oggetto RenderEffect
, segui questi passaggi:
view.setRenderEffect(RenderEffect.createBlurEffect(radiusX, radiusY, SHADER_TILE_MODE))
Puoi creare la visualizzazione in modo programmatico o aumentarla da un layout XML e
recuperarla utilizzando l'associazione di visualizzazioni o
findViewById()
.