Creare un disegno personalizzato

Prova il metodo Scrivi
Jetpack Compose è il toolkit consigliato per la UI per Android. Scopri come utilizzare i layout in Compose.

La parte più importante di una visualizzazione personalizzata è il suo aspetto. Disegno personalizzato può essere semplice o complesso, a seconda delle esigenze della tua applicazione. Questo documento copre alcune delle operazioni più comuni.

Per ulteriori informazioni, vedi Panoramica dei drawable.

Esegui l'override di onDraw()

Il passaggio più importante per tracciare una vista personalizzata è eseguire l'override onDraw() . Il parametro per onDraw() è un Canvas l'oggetto che la vista può utilizzare per disegnare. Il corso Canvas definisce metodi per disegnare testo, linee, bitmap e molti altri elementi grafici e i primitive. Puoi utilizzare questi metodi in onDraw() per creare un'interfaccia utente personalizzata.

Per iniziare, crea un Paint oggetto. Nella sezione successiva vengono trattati Paint in modo più dettagliato.

Creare oggetti di disegno

La android.graphics suddivide il disegno in due aree:

  • Cosa disegnare, gestito da Canvas.
  • Come disegnare, gestito da Paint.

Ad esempio, Canvas offre un metodo per tracciare una linea e Paint fornisce metodi per definire il colore di quella linea. Canvas prevede 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, Paint definisce il colore, lo stile, il carattere e così via di ogni forma disegni.

Prima di disegnare qualsiasi cosa, crea uno o più oggetti Paint. La nell'esempio seguente esegue questa operazione in un metodo chiamato init. Questo metodo è chiamato dal costruttore da Java, ma può essere inizializzato 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 visualizzazioni sono vengono ridisegnati spesso e molti oggetti di disegno richiedono un'inizializzazione costosa. Creazione di oggetti di disegno con il metodo onDraw() in modo significativo riduce le prestazioni e può rendere la UI più lenta.

Gestire gli eventi di layout

Per disegnare correttamente la visualizzazione personalizzata, scopri di che dimensione è. Personalizzazione complessa viste spesso devono eseguire più calcoli del layout a seconda delle dimensioni e la forma dell'area sullo schermo. Non fare mai ipotesi sulle dimensioni visualizzazione sullo schermo. Anche se solo un'app utilizza la tua vista, l'app deve gestire schermi di diverse dimensioni, densità e vari sia in modalità verticale che orizzontale.

Sebbene View offre molti metodi per la gestione della misurazione, la maggior parte dei quali non è con override. Se la visualizzazione non ha bisogno di un controllo speciale sulle sue dimensioni, sostituisci un metodo: onSizeChanged().

La chiamata a onSizeChanged() viene chiamata quando viene assegnata per la prima volta un e di nuovo se per qualsiasi motivo le dimensioni della visualizzazione cambiano. Calcola posizioni, dimensioni e qualsiasi altro valore correlato alle dimensioni della visualizzazione onSizeChanged(), invece di ricalcolarle ogni volta che disegni. Nell'esempio seguente, onSizeChanged() è la posizione in cui viene visualizzata calcola il rettangolo di delimitazione del grafico e la posizione relativa del un'etichetta di testo e altri elementi visivi.

Quando alla visualizzazione vengono assegnate dimensioni, Gestione layout presuppone che le dimensioni include la spaziatura interna della vista. Gestire i valori di spaziatura interna quando calcoli dimensioni della visualizzazione. Ecco uno snippet di onSizeChanged() che mostra come Per fare questo:

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 tua visualizzazione, implementa onMeasure(). I parametri di questo metodo View.MeasureSpec che indicano l'entità della visualizzazione desiderata dal genitore e sia che la dimensione sia un limite massimo o solo un suggerimento. Nell'ambito dell'ottimizzazione, questi valori vengono memorizzati come numeri interi compressi e si utilizzano i metodi statici View.MeasureSpec per decomprimere le informazioni archiviate in ogni numero intero.

Di seguito è riportato un esempio di implementazione di onMeasure(). In questo cerca di rendere l'area abbastanza grande da rendere il grafico altrettanto grande come 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);
}

Il codice contiene tre importanti aspetti:

  • I calcoli tengono conto della spaziatura interna della vista. Come menzionato in precedenza, questa è la responsabilità della vista.
  • Il metodo helper resolveSizeAndState() viene utilizzato per creare i valori finali di larghezza e altezza. Questo aiutante restituisce un valore View.MeasureSpec appropriato confrontando le dimensioni necessarie della visualizzazione al valore trasmesso a onMeasure().
  • onMeasure() non ha un valore restituito. Invece, il metodo comunica i suoi risultati setMeasuredDimension(). La chiamata a questo metodo è obbligatoria. Se ometti questa chiamata, il parametro La classe View 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 che la maggior parte delle visualizzazioni condivide:

  • Disegna testo usando drawText(). Specifica il carattere tipografico richiamando setTypeface() e il colore del testo richiamando setColor().
  • Disegnare forme primitive usando drawRect(), drawOval(), e drawArc(). Modifica se le forme sono piene, contornate o entrambe richiamando setStyle().
  • Disegna forme più complesse utilizzando Path . Definisci una forma aggiungendo linee e curve a un Path oggetto, poi disegna la forma usando drawPath(). Come con le forme primitive, i percorsi possono essere contorni, riempiti o entrambi, in base a setStyle().
  • Definisci i riempimenti gradienti creando LinearGradient di oggetti strutturati. Chiama setShader() per utilizzare LinearGradient sulle forme piene.
  • Traccia bitmap con 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;
}    

Applica effetti grafici

Android 12 (livello API 31) aggiunge RenderEffect , che applica effetti grafici comuni come sfocature, filtri colorati Effetti Shar Android e altro ancora per View oggetti e di rendering delle gerarchie. Puoi combinare gli effetti sotto forma di effetti di catena, ovvero di un effetto interno ed esterno o di effetti misti. Supporto per questa funzionalità varia in base alla potenza di elaborazione del dispositivo.

Puoi anche applicare effetti RenderNode per View chiamando View.setRenderEffect(RenderEffect).

Per implementare un oggetto RenderEffect:

view.setRenderEffect(RenderEffect.createBlurEffect(radiusX, radiusY, SHADER_TILE_MODE))

Puoi creare la vista in modo programmatico o gonfiarla da un layout XML. recuperarlo utilizzando Visualizza associazione o findViewById()