Creare componenti di visualizzazione personalizzata

Prova la modalità Scrivi
Jetpack Compose è il toolkit dell'interfaccia utente consigliato per Android. Scopri come utilizzare i layout in Compose.

Android offre un modello con componenti sofisticato e potente per creare la tua UI, basato sulle classi di layout fondamentali View e ViewGroup. La piattaforma include una serie di sottoclassi View e ViewGroup predefinite, denominate rispettivamente widget e layout, che puoi utilizzare per realizzare la tua UI.

Un elenco parziale dei widget disponibili include Button, TextView, EditText, ListView, CheckBox, RadioButton, Gallery, Spinner, e i widget più specifici AutoCompleteTextView, ImageSwitcher e TextSwitcher.

Tra i layout disponibili ci sono LinearLayout, FrameLayout, RelativeLayout e altri. Per altri esempi, consulta la sezione Layout comuni.

Se nessuno dei widget o dei layout predefiniti soddisfa le tue esigenze, puoi creare la tua sottoclasse View. Se hai solo bisogno di apportare piccole modifiche a un widget o layout esistente, puoi creare una sottoclasse per il widget o il layout e sovrascrivere i relativi metodi.

La creazione di sottoclassi View personalizzate ti offre un controllo preciso sull'aspetto e sul funzionamento di un elemento dello schermo. Per darti un'idea del livello di controllo che puoi ottenere con le visualizzazioni personalizzate, ecco alcuni esempi di come utilizzarle:

  • Puoi creare un tipo di View con rendering personalizzato, ad esempio una manopola di "controllo del volume", il cui rendering viene eseguito con grafica 2D, che assomiglia a un controllo elettronico analogico.
  • Puoi combinare un gruppo di componenti View in un nuovo componente singolo, magari per formare una casella combinata (una combinazione di elenco popup e campo di testo a immissione libera), un controllo selettore a due riquadri (un riquadro a destra e uno a sinistra con un elenco in ciascuno in cui puoi riassegnare l'elemento di un determinato elenco) e così via.
  • Puoi eseguire l'override del modo in cui un componente EditText viene visualizzato sullo schermo. L'app di esempio NotePad utilizza questa soluzione per creare una pagina di blocco note a righe.
  • Puoi acquisire altri eventi, come le pressioni dei tasti, e gestirli in modo personalizzato, ad esempio per un gioco.

Le sezioni seguenti spiegano come creare viste personalizzate e utilizzarle nella tua applicazione. Per informazioni di riferimento dettagliate, consulta la classe View.

L'approccio di base

Ecco una panoramica generale di ciò che devi sapere per creare i tuoi componenti View:

  1. Estendi una classe o una sottoclasse View esistente al tuo corso.
  2. Esegui l'override di alcuni metodi della superclasse. I metodi della superclasse da sostituire iniziano con on, ad esempio onDraw(), onMeasure() e onKeyDown(). È simile agli eventi on in Activity o ListActivity che esegui l'override per gli hook del ciclo di vita e di altri elementi.
  3. Utilizza il nuovo corso di estensione. Al termine, puoi utilizzare la nuova classe di estensione al posto della visualizzazione su cui si basa.

Componenti completamente personalizzati

Puoi creare componenti grafici completamente personalizzati che puoi visualizzare nel modo che preferisci. Potresti voler usare un VU meter grafico che assomiglia a un vecchio indicatore analogico o una visualizzazione di testo da cantare in cui una palla che rimbalza si muove lungo le parole mentre canti insieme a un apparecchio per il karaoke. Potresti volere qualcosa che i componenti integrati non sono in grado di fare, a prescindere da come li combini.

Fortunatamente, puoi creare componenti dall'aspetto e dal comportamento che preferisci, limitati solo dalla tua immaginazione, dalle dimensioni dello schermo e dalla potenza di elaborazione disponibile, tenendo presente che la tua applicazione potrebbe dover essere eseguita su qualcosa con una potenza notevolmente inferiore a quella della tua workstation desktop.

Per creare un componente completamente personalizzato, considera quanto segue:

  • La visualizzazione più generica che puoi estendere è View, quindi in genere inizia con l'estensione per creare il tuo nuovo componente super.
  • Puoi fornire un costruttore, che può prendere attributi e parametri dal file XML, e puoi utilizzare i tuoi attributi e parametri, come il colore e l'intervallo del VU meter o la larghezza e lo smorzamento dell'ago.
  • Probabilmente vorrai creare listener di eventi, funzioni di accesso alle proprietà e modificatori personalizzati, nonché comportamenti più sofisticati nella classe del componente.
  • Quasi certamente vorrai eseguire l'override di onMeasure() e probabilmente dovrai anche eseguire l'override di onDraw() se vuoi che il componente mostri qualcosa. Sebbene entrambi abbiano un comportamento predefinito, il valore predefinito onDraw() non fa nulla, mentre il valore predefinito onMeasure() imposta sempre una dimensione di 100 x 100, cosa che probabilmente non ti interessa.
  • Puoi anche eseguire l'override di altri metodi on, se necessario.

Estendi onDraw() e onMeasure()

Il metodo onDraw() fornisce un Canvas in cui puoi implementare tutto ciò che vuoi: grafica 2D, altri componenti standard o personalizzati, testo con stili o qualsiasi altra cosa ti venga in mente.

onMeasure() è un po' più coinvolto. onMeasure() è un elemento critico del contratto di rendering tra il tuo componente e il relativo container. onMeasure() deve essere sostituito per generare report precisi ed efficienti sulle misurazioni delle parti contenute. Ciò è reso leggermente più complesso dai requisiti di limite dell'elemento padre, che vengono trasferiti al metodo onMeasure(), e dal requisito di chiamare il metodo setMeasuredDimension() con la larghezza e l'altezza misurate dopo il calcolo. Se non chiami questo metodo da un metodo onMeasure() sottoposto a override, verrà visualizzata un'eccezione al momento della misurazione.

A livello generale, l'implementazione di onMeasure() si presenta in modo simile al seguente:

  • Il metodo onMeasure() sostituito viene chiamato con le specifiche di larghezza e altezza, che vengono trattate come requisiti per le limitazioni sulle misurazioni di larghezza e altezza prodotte. I parametri widthMeasureSpec e heightMeasureSpec sono entrambi codici interi che rappresentano dimensioni. Un riferimento completo al tipo di limitazioni che queste specifiche possono richiedere è disponibile nella documentazione di riferimento alla sezione View.onMeasure(int, int). Questa documentazione di riferimento illustra inoltre l'intera operazione di misurazione.
  • Il metodo onMeasure() del componente calcola la larghezza e l'altezza, necessarie per il rendering del componente. Deve cercare di rimanere entro le specifiche trasmesse, anche se può superarle. In questo caso, il genitore può scegliere cosa fare, ad esempio tagliare, scorrere, lanciare un'eccezione o chiedere all'onMeasure() di riprovare, magari con specifiche di misurazione diverse.
  • Quando vengono calcolate larghezza e altezza, chiama il metodo setMeasuredDimension(int width, int height) con le misurazioni calcolate. In caso contrario, verrà applicata un'eccezione.

Ecco un riepilogo di altri metodi standard che il framework chiama sulle viste:

Categoria Metodi Descrizione
creazione Costruttori Esiste un costruttore che viene richiamato quando la vista viene creata dal codice e un modulo che viene richiamato quando la visualizzazione viene gonfiata da un file di layout. Il secondo modulo analizza e applica gli attributi definiti nel file di layout.
onFinishInflate() Chiamato dopo una visualizzazione e tutti i relativi figli vengono aumentati in modo artificioso da XML.
Layout onMeasure(int, int) Chiamato per determinare i requisiti di dimensioni per questa vista e per tutte le sue viste secondarie.
onLayout(boolean, int, int, int, int) Chiamata quando questa vista deve assegnare una dimensione e una posizione a tutti i suoi elementi secondari.
onSizeChanged(int, int, int, int) Richiamato quando vengono modificate le dimensioni di questa visualizzazione.
Disegno onDraw(Canvas) Chiamato quando la visualizzazione deve eseguire il rendering dei propri contenuti.
Elaborazione degli eventi onKeyDown(int, KeyEvent) Richiamato quando si verifica un evento keydown.
onKeyUp(int, KeyEvent) Richiamato quando si verifica un evento chiave.
onTrackballEvent(MotionEvent) Richiamato quando si verifica un evento di movimento con la trackball.
onTouchEvent(MotionEvent) Richiamato quando si verifica un evento di movimento del touchscreen.
Fulcro del video onFocusChanged(boolean, int, Rect) Richiamato quando la visualizzazione acquisisce o perde la messa a fuoco.
onWindowFocusChanged(boolean) Richiamato quando la finestra contenente la vista acquisisce o perde lo stato attivo.
Collegamento onAttachedToWindow() Chiamato quando la vista è agganciata a una finestra.
onDetachedFromWindow() Chiamato quando la vista è scollegata dalla finestra.
onWindowVisibilityChanged(int) Richiamato quando viene modificata la visibilità della finestra contenente la visualizzazione.

Controlli composti

Se non vuoi creare un componente completamente personalizzato, ma vuoi creare un componente riutilizzabile costituito da un gruppo di controlli esistenti, la creazione di un componente composto (o controllo composto) potrebbe essere la soluzione migliore. In sintesi, questa opzione riunisce una serie di controlli o viste più atomici in un gruppo logico di elementi che possono essere trattati come un unico elemento. Ad esempio, una casella combinata può essere una combinazione di un campo EditText a riga singola e di un pulsante adiacente con un elenco popup allegato. Se l'utente tocca il pulsante e seleziona un elemento dall'elenco, viene compilato il campo EditText ma, se preferisce, l'utente può anche digitare qualcosa direttamente in EditText.

In Android sono disponibili altre due visualizzazioni per eseguire questa operazione: Spinner e AutoCompleteTextView. In ogni caso, questo concetto per una casella combinata è un buon esempio.

Per creare un componente composto:

  • Come con Activity, utilizza l'approccio dichiarativo (basato su XML) per creare i componenti contenuti o nidificali in modo programmatico dal codice. Il punto di partenza solitamente è un Layout di qualche tipo, quindi crea un corso che estenda un Layout. Nel caso di una casella combinata, puoi utilizzare un elemento LinearLayout con orientamento orizzontale. Puoi nidificare altri layout all'interno, in modo che il componente composto possa essere arbitrariamente complesso e strutturato.
  • Nel costruttore della nuova classe, prendi i parametri previsti dalla superclasse e trasmettili prima al costruttore della superclasse. Successivamente, puoi impostare le altre viste da utilizzare all'interno del nuovo componente. Qui puoi creare il campo EditText e l'elenco popup. Potresti introdurre nel file XML attributi e parametri che il tuo costruttore può estrarre e utilizzare.
  • Se vuoi, crea listener per gli eventi che potrebbero essere generati dalle viste contenute. Un esempio è un metodo listener per il listener dei clic su un elemento dell'elenco per aggiornare i contenuti di EditText se viene effettuata una selezione dall'elenco.
  • Se vuoi, puoi creare le tue proprietà con funzioni di accesso e modificatori. Ad esempio, lascia che il valore EditText venga impostato inizialmente nel componente e, se necessario, esegui una query per i suoi contenuti.
  • (Facoltativo) Esegui l'override di onDraw() e onMeasure(). In genere questa operazione non è necessaria quando si estende un elemento Layout, poiché il layout ha un comportamento predefinito che probabilmente funziona correttamente.
  • Facoltativamente, sostituisci altri metodi on, come onKeyDown(), ad esempio per scegliere determinati valori predefiniti dall'elenco popup di una casella combinata quando viene toccata una determinata chiave.

L'utilizzo di un Layout come base per un controllo personalizzato offre alcuni vantaggi, tra cui:

  • Puoi specificare il layout utilizzando i file XML dichiarativi, proprio come con una schermata di attività, oppure puoi creare viste in modo programmatico e nidificarle nel layout a partire dal tuo codice.
  • I metodi onDraw() e onMeasure(), oltre alla maggior parte degli altri metodi on, hanno un comportamento appropriato, quindi non è necessario sostituirli.
  • Puoi creare rapidamente viste composte arbitrariamente complesse e riutilizzarle come se fossero un singolo componente.

Modificare un tipo di visualizzazione esistente

Se esiste un componente simile a quello desiderato, puoi estenderlo e sostituire il comportamento che desideri modificare. Puoi fare tutto ciò che fai con un componente completamente personalizzato, ma iniziando con una classe più specializzata nella gerarchia View puoi ottenere un comportamento che faccia quello che vuoi, senza costi.

Ad esempio, l'app di esempio NotePad illustra molti aspetti dell'utilizzo della piattaforma Android. Tra questi c'è l'estensione di una visualizzazione EditText per creare un blocco note a righe. Questo non è un esempio perfetto e le API per farlo potrebbero cambiare, ma è una dimostrazione dei principi.

Se non l'hai ancora fatto, importa l'esempio di Blocco note in Android Studio o controlla la fonte utilizzando il link fornito. In particolare, consulta la definizione di LinedEditText nel file NoteEditor.java.

Ecco alcune cose da considerare in questo file:

  1. Definizione

    La classe viene definita con la seguente riga:
    public static class LinedEditText extends EditText

    LinedEditText è definito come una classe interna nell'attività NoteEditor, ma è pubblico in modo che sia possibile accedervi come NoteEditor.LinedEditText dall'esterno della classe NoteEditor.

    Inoltre, LinedEditText è static, il che significa che non genera i cosiddetti "metodi sintetici" che gli consentono di accedere ai dati della classe padre. Ciò significa che si comporta come una classe separata anziché come qualcosa di fortemente correlato a NoteEditor. Questo è un modo più semplice per creare classi interne se non hanno bisogno dell'accesso allo stato dalla classe esterna. La classe generata rimane ridotta e può essere utilizzata facilmente da altre classi.

    LinedEditText estende EditText, che è la visualizzazione da personalizzare in questo caso. Al termine, il nuovo corso può sostituire una normale visualizzazione EditText.

  2. Inizializzazione dei corsi

    Come sempre, il super viene chiamato per primo. Questo non è un costruttore predefinito, ma è uno con parametri. Il valore EditText viene creato con questi parametri quando viene aumentato in modo artificioso da un file di layout XML. Di conseguenza, il costruttore deve prenderli e passarli anche al costruttore della superclasse.

  3. Metodi con override

    Questo esempio sostituisce solo il metodo onDraw(), ma potrebbe essere necessario eseguire l'override di altri quando crei i tuoi componenti personalizzati.

    Per questo esempio, l'override del metodo onDraw() consente di colorare le linee blu sul canvas di visualizzazione EditText. Il canvas viene trasferito al metodo onDraw() sottoposto a override. Il metodo super.onDraw() viene chiamato prima della fine. Il metodo superclass deve essere richiamato. In questo caso, richiamalo alla fine dopo aver dipinto le linee che vuoi includere.

  4. Componente personalizzato

    Ora hai il tuo componente personalizzato, ma come puoi usarlo? Nell'esempio di Blocco note, il componente personalizzato viene utilizzato direttamente dal layout dichiarativo, quindi guarda note_editor.xml nella cartella res/layout:

    <view xmlns:android="http://schemas.android.com/apk/res/android"
        class="com.example.android.notepad.NoteEditor$LinedEditText"
        android:id="@+id/note"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/transparent"
        android:padding="5dp"
        android:scrollbars="vertical"
        android:fadingEdge="vertical"
        android:gravity="top"
        android:textSize="22sp"
        android:capitalize="sentences"
    />
    

    Il componente personalizzato viene creato come visualizzazione generica nel file XML e la classe viene specificata utilizzando il pacchetto completo. Viene fatto riferimento alla classe interna definita utilizzando la notazione NoteEditor$LinedEditText, che è un modo standard per fare riferimento alle classi interne nel linguaggio di programmazione Java.

    Se il componente View personalizzato non è definito come una classe interna, puoi dichiararlo con il nome dell'elemento XML ed escludere l'attributo class. Ad esempio:

    <com.example.android.notepad.LinedEditText
      id="@+id/note"
      ... />
    

    Tieni presente che il corso LinedEditText ora è un file del corso separato. Quando la classe è nidificata nella classe NoteEditor, questa tecnica non funziona.

    Gli altri attributi e parametri della definizione sono quelli trasferiti al costruttore del componente personalizzato e poi trasmessi al costruttore EditText; di conseguenza, sono gli stessi parametri che utilizzi per una vista EditText. Puoi anche aggiungere parametri personalizzati.

Creare componenti personalizzati è complicato solo se serve.

Un componente più sofisticato può eseguire l'override di un numero maggiore di metodi on e introdurre i propri metodi helper, personalizzandone in modo sostanziale le proprietà e il comportamento. L'unico limite è la tua immaginazione e ciò di cui il componente ha bisogno per fare ciò.