Creare componenti di visualizzazione personalizzata

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

Android offre un modello sofisticato e potente basato su componenti per la creazione dell'interfaccia utente, basato sulle classi di layout fondamentali View e ViewGroup. La piattaforma include una varietà di classi View e ViewGroup predefinite, chiamate rispettivamente widget e layout, che puoi utilizzare per creare la tua UI.

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

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

Se nessuno dei widget o dei layout predefiniti soddisfa le tue esigenze, puoi creare una sottoclasse View personalizzata. Se devi apportare solo piccole modifiche a un widget o a un layout esistente, puoi creare una sottoclasse del widget o del layout e sostituire i relativi metodi.

La creazione di sottoclassi View personalizzate ti consente di controllare con precisione l'aspetto e la funzione di un elemento dello schermo. Per darti un'idea del controllo che ottieni con le visualizzazioni personalizzate, ecco alcuni esempi di ciò che puoi fare:

  • Puoi creare un tipo View completamente personalizzato, ad esempio una manopola di "controllo del volume", visualizzata utilizzando la grafica 2D, che assomiglia a un controllo elettronico analogico.
  • Puoi combinare un gruppo di View componenti in un unico nuovo componente, ad esempio per creare una casella combinata (una combinazione di elenco popup e campo di testo a inserimento libero), un controllo selettore a due riquadri (un riquadro a sinistra e uno a destra con un elenco in ciascuno in cui puoi riassegnare l'elemento a un elenco), e così via.
  • Puoi ignorare il modo in cui un componente EditText viene visualizzato sullo schermo. L'app di esempio NotePad utilizza questo elemento in modo efficace per creare una pagina di blocco note a righe.
  • Puoi acquisire altri eventi, ad esempio la pressione di tasti, e gestirli in modo personalizzato, ad esempio per un gioco.

Le sezioni seguenti spiegano come creare viste personalizzate e utilizzarle nell'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 con la tua classe.
  2. Esegui l'override di alcuni metodi della superclasse. I metodi della superclasse da sostituire iniziano con on, ad esempio onDraw(), onMeasure(), e onKeyDown(). Questi sono simili agli eventi on in Activity o ListActivity che esegui l'override per i hook del ciclo di vita e di altre funzionalità.
  3. Utilizza la nuova classe di estensione. Una volta completata, 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 vengono visualizzati nel modo che preferisci. Forse vuoi un VU meter grafico che assomigli a un vecchio indicatore analogico o una visualizzazione del testo per cantare in cui una palla rimbalza sulle parole mentre canti insieme a una macchina per il karaoke. Potresti volere qualcosa che i componenti integrati non possono fare, indipendentemente da come li combini.

Fortunatamente, puoi creare componenti che hanno l'aspetto e il 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 un dispositivo con una potenza significativamente inferiore rispetto alla tua workstation desktop.

Per creare un componente completamente personalizzato, tieni presente quanto segue:

  • La visualizzazione più generica che puoi estendere è View, quindi di solito inizi estendendo questa per creare il nuovo supercomponente.
  • Puoi fornire un costruttore, che può accettare attributi e parametri dall'XML, e puoi utilizzare i tuoi attributi e parametri, ad esempio il colore e l'intervallo del VU meter o la larghezza e lo smorzamento dell'ago.
  • Probabilmente vorrai creare i tuoi listener di eventi, gli accessor e i modificatori di proprietà, nonché un comportamento più sofisticato nella classe del componente.
  • Quasi sicuramente vuoi sovrascrivere onMeasure() e probabilmente dovrai anche sovrascrivere onDraw() se vuoi che il componente mostri qualcosa. Sebbene entrambi abbiano un comportamento predefinito, onDraw() non fa nulla, mentre onMeasure() imposta sempre una dimensione di 100x100, che probabilmente non ti interessa.
  • Puoi anche ignorare altri metodi di on, se necessario.

Estendi onDraw() e onMeasure()

Il metodo onDraw() fornisce un Canvas su cui puoi implementare qualsiasi cosa tu voglia: grafica 2D, altri componenti standard o personalizzati, testo formattato o qualsiasi altra cosa ti venga in mente.

onMeasure() è un po' più complesso. onMeasure() è un elemento fondamentale del contratto di rendering tra il componente e il relativo contenitore. onMeasure() deve essere ignorato per segnalare in modo efficiente e accurato le misurazioni delle parti contenute. Ciò è reso leggermente più complesso dai requisiti di limite del genitore, che vengono passati al metodo onMeasure(), e dal requisito di chiamare il metodo setMeasuredDimension() con la larghezza e l'altezza misurate una volta calcolate. Se non chiami questo metodo da un metodo onMeasure() sottoposto a override, si verifica un'eccezione al momento della misurazione.

A livello generale, l'implementazione di onMeasure() è simile alla seguente:

  • L'override del metodo onMeasure() viene chiamato con le specifiche di larghezza e altezza, che vengono trattate come requisiti per le limitazioni delle misurazioni di larghezza e altezza che produci. I parametri widthMeasureSpec e heightMeasureSpec sono entrambi codici interi che rappresentano le dimensioni. Un riferimento completo al tipo di limitazioni che queste specifiche possono richiedere è disponibile nella documentazione di riferimento in View.onMeasure(int, int) Questa documentazione di riferimento spiega anche l'intera operazione di misurazione.
  • Il metodo onMeasure() del componente calcola una larghezza e un'altezza di misurazione, necessarie per il rendering del componente. Deve cercare di rispettare le specifiche fornite, anche se può superarle. In questo caso, il genitore può scegliere cosa fare, ad esempio tagliare, scorrere, generare un'eccezione o chiedere a onMeasure() di riprovare, magari con specifiche di misurazione diverse.
  • Una volta calcolate la larghezza e l'altezza, chiama il metodo setMeasuredDimension(int width, int height) con le misurazioni calcolate. In caso contrario, si verifica un'eccezione.

Ecco un riepilogo di altri metodi standard che il framework chiama nelle visualizzazioni:

Categoria Metodi Descrizione
Creazione Costruttori Esiste una forma del costruttore chiamata quando la visualizzazione viene creata dal codice e una forma chiamata quando la visualizzazione viene creata da un file di layout. Il secondo modulo analizza e applica gli attributi definiti nel file di layout.
onFinishInflate() Chiamato dopo che una visualizzazione e tutti i relativi elementi secondari sono stati caricati da XML.
Layout onMeasure(int, int) Chiamato per determinare i requisiti di dimensioni per questa visualizzazione e tutti i relativi elementi secondari.
onLayout(boolean, int, int, int, int) Chiamato quando questa visualizzazione deve assegnare una dimensione e una posizione a tutti i suoi elementi secondari.
onSizeChanged(int, int, int, int) Chiamato quando le dimensioni di questa visualizzazione vengono modificate.
Disegno onDraw(Canvas) Chiamato quando la visualizzazione deve eseguire il rendering dei contenuti.
Elaborazione degli eventi onKeyDown(int, KeyEvent) Chiamato quando si verifica un evento di pressione di un tasto.
onKeyUp(int, KeyEvent) Chiamato quando si verifica un evento di rilascio del tasto.
onTrackballEvent(MotionEvent) Chiamato quando si verifica un evento di movimento del trackball.
onTouchEvent(MotionEvent) Chiamato quando si verifica un evento di movimento del touchscreen.
Concentrazione onFocusChanged(boolean, int, Rect) Chiamato quando la visualizzazione acquisisce o perde lo stato attivo.
onWindowFocusChanged(boolean) Chiamato quando la finestra contenente la visualizzazione acquisisce o perde lo stato attivo.
Collegamento in corso onAttachedToWindow() Chiamato quando la visualizzazione è collegata a una finestra.
onDetachedFromWindow() Chiamato quando la visualizzazione viene scollegata dalla finestra.
onWindowVisibilityChanged(int) Chiamato quando viene modificata la visibilità della finestra contenente la visualizzazione.

Controlli composti

Se non vuoi creare un componente completamente personalizzato, ma vuoi assemblare 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, questo raggruppa una serie di controlli o visualizzazioni più atomici in un gruppo logico di elementi che possono essere trattati come un'unica entità. Ad esempio, una casella combinata può essere una combinazione di un campo EditText a una sola riga e un pulsante adiacente con un elenco popup allegato. Se l'utente tocca il pulsante e seleziona un elemento dall'elenco, il campo EditText viene compilato, ma può anche digitare qualcosa direttamente nel campo EditText, se preferisce.

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

Per creare un componente composto:

  • Come per un Activity, utilizza l'approccio dichiarativo (basato su XML) per creare i componenti contenuti o nidificarli in modo programmatico dal codice. Il punto di partenza abituale è un Layout di qualche tipo, quindi crea una classe che estenda un Layout. Nel caso di una casella combinata, puoi utilizzare un 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 tutti i parametri previsti dalla superclasse e passali prima al costruttore della superclasse. Poi, puoi configurare le altre visualizzazioni da utilizzare all'interno del nuovo componente. Qui puoi creare il campo EditText e l'elenco a comparsa. Puoi introdurre attributi e parametri personalizzati nell'XML che il costruttore può estrarre e utilizzare.
  • (Facoltativo) Crea listener per gli eventi che le visualizzazioni incorporate potrebbero generare. Un esempio è un metodo di listener per il click listener sull'elemento dell'elenco per aggiornare i contenuti di EditText se viene effettuata una selezione dell'elenco.
  • Se vuoi, crea le tue proprietà con funzioni di accesso e modificatori. Ad esempio, consenti di impostare inizialmente il valore di EditText nel componente e di eseguire query sui relativi contenuti quando necessario.
  • (Facoltativo) Esegui l'override di onDraw() e onMeasure(). In genere non è necessario quando si estende un Layout, poiché il layout ha un comportamento predefinito che probabilmente funziona bene.
  • Se vuoi, esegui l'override di altri metodi on, ad esempio onKeyDown(), per scegliere determinati valori predefiniti dall'elenco popup di una casella combinata quando viene toccato un tasto specifico.

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

  • Puoi specificare il layout utilizzando i file XML dichiarativi, proprio come con una schermata di attività, oppure puoi creare viste a livello di programmazione e nidificarle nel layout dal codice.
  • I metodi onDraw() e onMeasure(), oltre alla maggior parte degli altri metodi on, hanno un comportamento adatto, quindi non devi eseguirne l'override.
  • 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 che vuoi, puoi estenderlo e sostituire il comportamento che vuoi modificare. Puoi fare tutto ciò che fai con un componente completamente personalizzato, ma iniziando con una classe più specializzata nella gerarchia View, puoi ottenere senza costi un comportamento che fa ciò che vuoi.

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

Se non l'hai ancora fatto, importa l'esempio NotePad in Android Studio o esamina il codice sorgente utilizzando il link fornito. In particolare, consulta la definizione di LinedEditText nel file NoteEditor.java.

Ecco alcuni aspetti da notare in questo file:

  1. La definizione

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

    LinedEditText è definita come classe interna all'attività NoteEditor, ma è pubblica, quindi è 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 strettamente correlato a NoteEditor. Questo è un modo più pulito per creare classi interne se non hanno bisogno di accedere allo stato della classe esterna. Mantiene la classe generata piccola e ne consente l'utilizzo semplice da altre classi.

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

  2. Inizializzazione della classe

    Come sempre, viene chiamato prima il super. Questo non è un costruttore predefinito, ma è un costruttore parametrizzato. EditText viene creato con questi parametri quando viene gonfiato da un file di layout XML. Pertanto, il costruttore deve prenderli e passarli anche al costruttore della superclasse.

  3. Metodi sostituiti

    Questo esempio esegue l'override solo del metodo onDraw(), ma potresti dover eseguire l'override di altri metodi durante la creazione dei tuoi componenti personalizzati.

    Per questo esempio, l'override del metodo onDraw() consente di disegnare le linee blu sul canvas della visualizzazione EditText. La tela viene passata al metodo onDraw() di override. Il metodo super.onDraw() viene chiamato prima della fine del metodo. Il metodo della superclasse deve essere richiamato. In questo caso, invoca la funzione alla fine dopo aver disegnato le linee che vuoi includere.

  4. Componente personalizzato

    Ora hai il tuo componente personalizzato, ma come puoi utilizzarlo? Nell'esempio di NotePad, il componente personalizzato viene utilizzato direttamente dal layout dichiarativo, quindi esamina 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 in XML e la classe viene specificata utilizzando il pacchetto completo. La classe interna che definisci viene referenziata utilizzando la notazione NoteEditor$LinedEditText, che è un modo standard per fare riferimento alle classi interne nel linguaggio di programmazione Java.

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

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

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

    Gli altri attributi e parametri nella definizione sono quelli passati al costruttore del componente personalizzato e poi al costruttore EditText, quindi sono gli stessi parametri che utilizzi per una visualizzazione EditText. È possibile aggiungere anche i tuoi parametri.

La creazione di componenti personalizzati è complicata solo quanto necessario.

Un componente più sofisticato può eseguire l'override di un numero ancora maggiore di metodi on e introdurre i propri metodi helper, personalizzando in modo sostanziale le sue proprietà e il suo comportamento. L'unico limite è la tua immaginazione e ciò che vuoi che faccia il componente.