Personalizzazione Android: come creare un componente dell'interfaccia utente che faccia ciò che desideri

Pubblicato: 2022-03-11

Non è raro che gli sviluppatori si trovino nella necessità di un componente dell'interfaccia utente che non è fornito dalla piattaforma a cui stanno puntando o è, in effetti, fornito, ma manca di una determinata proprietà o comportamento. La risposta a entrambi gli scenari è un componente dell'interfaccia utente personalizzato.

Il modello dell'interfaccia utente Android è intrinsecamente personalizzabile e offre i mezzi per la personalizzazione, il test e la possibilità di creare componenti dell'interfaccia utente personalizzati in vari modi:

  • Eredita un componente esistente (ad esempio TextView , ImageView , ecc.) e aggiungi/sovrascrivi le funzionalità necessarie. Ad esempio, un CircleImageView che eredita ImageView , sovrascrivendo la funzione onDraw() per limitare l'immagine visualizzata a un cerchio e aggiungendo una funzione loadFromFile() per caricare un'immagine dalla memoria esterna.

  • Crea un componente composto da diversi componenti. Questo approccio di solito sfrutta i layout per controllare la disposizione dei componenti sullo schermo. Ad esempio, un LabeledEditText che eredita LinearLayout con orientamento orizzontale e contiene sia un TextView che funge da etichetta sia un EditText che funge da campo di immissione di testo.

    Questo approccio potrebbe sfruttare anche il precedente, ovvero i componenti interni potrebbero essere nativi o personalizzati.

  • L'approccio più versatile e complesso consiste nel creare un componente autodisegnato . In questo caso, il componente erediterebbe la classe View generica e sovrascriverebbe funzioni come onMeasure() per determinarne il layout, onDraw() per visualizzarne il contenuto, ecc. I componenti creati in questo modo di solito dipendono fortemente dall'API di disegno 2D di Android.

Caso di studio per la personalizzazione di Android: CalendarView

Android fornisce un componente CalendarView nativo. Funziona bene e fornisce la funzionalità minima prevista da qualsiasi componente del calendario, visualizzando un mese intero ed evidenziando il giorno corrente. Alcuni potrebbero dire che ha anche un bell'aspetto, ma solo se stai cercando un aspetto nativo e non hai alcun interesse a personalizzare l'aspetto.

Ad esempio, il componente CalendarView non fornisce alcun modo per modificare il modo in cui viene contrassegnato un determinato giorno o il colore di sfondo da utilizzare. Inoltre, non è possibile aggiungere testo o grafica personalizzati, ad esempio per celebrare un'occasione speciale. In breve, il componente si presenta così e quasi nulla può essere modificato:

Immagine dello schermo
CalendarView nel tema AppCompact.Light .

Crea il tuo

Quindi, come si fa a creare la propria visualizzazione del calendario? Qualsiasi degli approcci di cui sopra funzionerebbe. Tuttavia, la praticità di solito esclude la terza opzione (grafica 2D) e ci lascia con gli altri due metodi, e in questo articolo utilizzeremo una combinazione di entrambi.

Per seguire, puoi trovare il codice sorgente qui.

1. Il layout dei componenti

Innanzitutto, iniziamo con l'aspetto del componente. Per semplificare, visualizziamo i giorni in una griglia e, in alto, il nome del mese insieme ai pulsanti "mese successivo" e "mese precedente".

Immagine dello schermo
Visualizzazione calendario personalizzata.

Questo layout è definito nel file control_calendar.xml , come segue. Nota che alcuni markup ripetitivi sono stati abbreviati con ... :

 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white"> <!-- date toolbar --> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="12dp" android:paddingBottom="12dp" android:paddingLeft="30dp" android:paddingRight="30dp"> <!-- prev button --> <ImageView android: android:layout_width="30dp" android:layout_height="30dp" android:layout_centerVertical="true" android:layout_alignParentLeft="true" android:src="@drawable/previous_icon"/> <!-- date title --> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toRightOf="@+id/calendar_prev_button" android:layout_toLeftOf="@+id/calendar_next_button" android:gravity="center" android:textAppearance="@android:style/TextAppearance.Medium" android:textColor="#222222" android:text="current date"/> <!-- next button --> <ImageView android: ... Same layout as prev button. android:src="@drawable/next_icon"/> </RelativeLayout> <!-- days header --> <LinearLayout android: android:layout_width="match_parent" android:layout_height="40dp" android:gravity="center_vertical" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center_horizontal" android:textColor="#222222" android:text="SUN"/> ... Repeat for MON - SAT. </LinearLayout> <!-- days view --> <GridView android: android:layout_width="match_parent" android:layout_height="340dp" android:numColumns="7"/> </LinearLayout>

2. La classe dei componenti

Il layout precedente può essere incluso così com'è in Activity o in un Fragment e funzionerà correttamente. Ma incapsularlo come componente dell'interfaccia utente autonomo impedirà la ripetizione del codice e consentirà un design modulare, in cui ogni modulo gestisce una responsabilità.

Il nostro componente dell'interfaccia utente sarà un LinearLayout , per corrispondere alla radice del file di layout XML. Si noti che solo le parti importanti sono mostrate dal codice. L'implementazione del componente risiede in CalendarView.java :

 public class CalendarView extends LinearLayout { // internal components private LinearLayout header; private ImageView btnPrev; private ImageView btnNext; private TextView txtDate; private GridView grid; public CalendarView(Context context) { super(context); initControl(context); } /** * Load component XML layout */ private void initControl(Context context) { LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(R.layout.control_calendar, this); // layout is inflated, assign local variables to components header = (LinearLayout)findViewById(R.id.calendar_header); btnPrev = (ImageView)findViewById(R.id.calendar_prev_button); btnNext = (ImageView)findViewById(R.id.calendar_next_button); txtDate = (TextView)findViewById(R.id.calendar_date_display); grid = (GridView)findViewById(R.id.calendar_grid); } }

Il codice è piuttosto semplice. Al momento della creazione, il componente gonfia il layout XML e, al termine, assegna i controlli interni alle variabili locali per un accesso più semplice in seguito.

3. È necessaria una certa logica

Per fare in modo che questo componente si comporti effettivamente come una vista calendario, è necessaria una logica aziendale. All'inizio potrebbe sembrare complicato, ma in realtà non c'è molto da fare. Analizziamolo:

  1. La visualizzazione del calendario è ampia sette giorni ed è garantito che tutti i mesi inizieranno da qualche parte nella prima riga.

  2. Per prima cosa, dobbiamo capire in quale posizione inizia il mese, quindi riempire tutte le posizioni precedenti con i numeri del mese precedente (30, 29, 28.. ecc.) fino a raggiungere la posizione 0.

  3. Quindi, compiliamo i giorni per il mese corrente (1, 2, 3... ecc.).

  4. Dopo di che vengono i giorni per il mese successivo (di nuovo, 1, 2, 3.. ecc), ma questa volta riempiamo solo le posizioni rimanenti nell'ultima riga/e della griglia.

Il diagramma seguente illustra questi passaggi:

Immagine dello schermo
Logica aziendale vista calendario personalizzata.

La larghezza della griglia è già specificata per essere sette celle, che denotano un calendario settimanale, ma che ne dici dell'altezza? La dimensione massima della griglia può essere determinata dallo scenario peggiore di un mese di 31 giorni che inizia di sabato, che è l'ultima cella della prima riga e avrà bisogno di altre 5 righe per essere visualizzata per intero. Quindi, impostare il calendario per visualizzare sei righe (per un totale di 42 giorni) sarà sufficiente per gestire tutti i casi.

Ma non tutti i mesi hanno 31 giorni! Possiamo evitare complicazioni derivanti da ciò utilizzando la funzionalità di data integrata di Android, evitando la necessità di calcolare noi stessi il numero di giorni.

Come accennato in precedenza, le funzionalità date fornite dalla classe Calendar rendono l'implementazione piuttosto semplice. Nel nostro componente, la funzione updateCalendar() implementa questa logica:

 private void updateCalendar() { ArrayList<Date> cells = new ArrayList<>(); Calendar calendar = (Calendar)currentDate.clone(); // determine the cell for current month's beginning calendar.set(Calendar.DAY_OF_MONTH, 1); int monthBeginningCell = calendar.get(Calendar.DAY_OF_WEEK) - 1; // move calendar backwards to the beginning of the week calendar.add(Calendar.DAY_OF_MONTH, -monthBeginningCell); // fill cells (42 days calendar as per our business logic) while (cells.size() < DAYS_COUNT) { cells.add(calendar.getTime()); calendar.add(Calendar.DAY_OF_MONTH, 1); } // update grid ((CalendarAdapter)grid.getAdapter()).updateData(cells); // update title SimpleDateFormat sdf = new SimpleDateFormat("MMM yyyy"); txtDate.setText(sdf.format(currentDate.getTime())); }

4. Personalizzabile a cuore

Poiché il componente responsabile della visualizzazione dei singoli giorni è GridView , un buon posto per personalizzare la modalità di visualizzazione dei giorni è Adapter , poiché è responsabile della conservazione dei dati e del gonfiaggio delle visualizzazioni per le singole celle della griglia.

Per questo esempio, avremo bisogno di quanto segue dal nostro CalendearView :

  • Il giorno presente dovrebbe essere in grassetto blu .
  • I giorni al di fuori del mese corrente dovrebbero essere disattivati .
  • I giorni con un evento dovrebbero visualizzare un'icona speciale.
  • L'intestazione del calendario dovrebbe cambiare colore a seconda della stagione (estate, autunno, inverno, primavera).

I primi tre requisiti sono semplici da raggiungere modificando gli attributi del testo e le risorse in background. Implementiamo un CalendarAdapter per svolgere questo compito. È abbastanza semplice da poter essere una classe membro in CalendarView . Sovrascrivendo la funzione getView() , possiamo ottenere i requisiti di cui sopra:

 @Override public View getView(int position, View view, ViewGroup parent) { // day in question Date date = getItem(position); // today Date today = new Date(); // inflate item if it does not exist yet if (view == null) view = inflater.inflate(R.layout.control_calendar_day, parent, false); // if this day has an event, specify event image view.setBackgroundResource(eventDays.contains(date)) ? R.drawable.reminder : 0); // clear styling view.setTypeface(null, Typeface.NORMAL); view.setTextColor(Color.BLACK); if (date.getMonth() != today.getMonth() || date.getYear() != today.getYear()) { // if this day is outside current month, grey it out view.setTextColor(getResources().getColor(R.color.greyed_out)); } else if (date.getDate() == today.getDate()) { // if it is today, set it to blue/bold view.setTypeface(null, Typeface.BOLD); view.setTextColor(getResources().getColor(R.color.today)); } // set text view.setText(String.valueOf(date.getDate())); return view; }

Il requisito di progettazione finale richiede un po' più di lavoro. Innanzitutto, aggiungiamo i colori per le quattro stagioni in /res/values/colors.xml :

 <color name="summer">#44eebd82</color> <color name="fall">#44d8d27e</color> <color name="winter">#44a1c1da</color> <color name="spring">#448da64b</color>

Quindi, utilizziamo un array per definire la stagione per ogni mese (assumendo l'emisfero settentrionale, per semplicità; scusa Australia!). In CalendarView aggiungiamo le seguenti variabili membro:

 // seasons' rainbow int[] rainbow = new int[] { R.color.summer, R.color.fall, R.color.winter, R.color.spring }; int[] monthSeason = new int[] {2, 2, 3, 3, 3, 0, 0, 0, 1, 1, 1, 2};

In questo modo, la selezione di un colore appropriato avviene selezionando la stagione appropriata ( monthSeason[currentMonth] ) e quindi selezionando il colore corrispondente ( rainbow[monthSeason[currentMonth] ), questo viene aggiunto ad updateCalendar() per assicurarsi che sia selezionato il colore appropriato ogni volta che si cambia calendario.

 // set header color according to current season int month = currentDate.get(Calendar.MONTH); int season = monthSeason[month]; int color = rainbow[season]; header.setBackgroundColor(getResources().getColor(color));

Con ciò, otteniamo il seguente risultato:

Personalizzazione Android
Il colore dell'intestazione cambia in base alla stagione.

Nota importante a causa del modo in cui HashSet confronta gli oggetti, il controllo sopra eventDays.contains(date) in updateCalendar() non produrrà true per gli oggetti date a meno che non siano esattamente identici. Non esegue alcun controllo speciale per il tipo di dati Date . Per ovviare a questo problema, questo controllo viene sostituito dal codice seguente:

 for (Date eventDate : eventDays) { if (eventDate.getDate() == date.getDate() && eventDate.getMonth() == date.getMonth() && eventDate.getYear() == date.getYear()) { // mark this day for event view.setBackgroundResource(R.drawable.reminder); break; } }

5. Sembra brutto in fase di progettazione

La scelta di Android per i segnaposto in fase di progettazione può essere discutibile. Fortunatamente, Android crea un'istanza del nostro componente per renderizzarlo nel designer dell'interfaccia utente e possiamo sfruttarlo chiamando updateCalendar() nel costruttore del componente. In questo modo il componente avrà effettivamente un senso in fase di progettazione.

Immagine dello schermo

Se l'inizializzazione del componente richiede molte elaborazioni o carica molti dati, può influire sulle prestazioni dell'IDE. In questo caso, Android fornisce una funzione ingegnosa chiamata isInEditMode() che può essere utilizzata per limitare la quantità di dati utilizzati quando il componente viene effettivamente istanziato nella finestra di progettazione dell'interfaccia utente. Ad esempio, se ci sono molti eventi da caricare in CalendarView , possiamo usare isInEditMode() all'interno della funzione updateCalendar() per fornire un elenco di eventi vuoto/limitato in modalità progettazione e caricare quello reale in caso contrario.

6. Invocare il componente

Il componente può essere incluso nei file di layout XML (un esempio di utilizzo può essere trovato in activity_main.xml ):

 <samples.aalamir.customcalendar.CalendarView android: android:layout_width="match_parent" android:layout_height="wrap_content"/>

E recuperato per interagire una volta caricato il layout:

 HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events);

Il codice precedente crea un HashSet di eventi, aggiunge il giorno corrente, quindi lo passa a CalendarView . Di conseguenza, CalendarView visualizzerà il giorno corrente in grassetto blu e vi inserirà anche l'indicatore dell'evento:

Immagine dello schermo
CalendarView che mostra un evento

7. Aggiunta di attributi

Un'altra funzionalità fornita da Android consiste nell'assegnare attributi a un componente personalizzato. Ciò consente agli sviluppatori Android che utilizzano il componente di selezionare le impostazioni tramite il layout XML e di vedere immediatamente il risultato nel designer dell'interfaccia utente, invece di dover aspettare e vedere come appare CalendarView in runtime. Aggiungiamo la possibilità di modificare la visualizzazione del formato della data nel componente, ad esempio per sillabare il nome completo del mese anziché l'abbreviazione di tre lettere.

Per fare ciò, sono necessari i seguenti passaggi:

  • Dichiara l'attributo. Chiamiamolo dateFormat e diamogli il tipo di dati string . Aggiungilo a /res/values/attrs.xml :
 <resources> <declare-styleable name="CalendarDateElement"> <attr name="dateFormat" format="string"/> </declare-styleable> </resources>
  • Utilizza l'attributo nel layout che utilizza il componente e assegnagli il valore "MMMM yyyy" :
 <samples.aalamir.customcalendar.CalendarView xmlns:calendarNS="http://schemas.android.com/apk/res/samples.aalamir.customcalendar" android: android:layout_width="match_parent" android:layout_height="wrap_content" calendarNS:dateFormat="MMMM yyyy"/>
  • Infine, chiedi al componente di utilizzare il valore dell'attributo:
 TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CalendarView); dateFormat = ta.getString(R.styleable.CalendarView_dateFormat);

] Crea il progetto e noterai le modifiche alla data visualizzata nel designer dell'interfaccia utente per utilizzare il nome completo del mese, ad esempio "Luglio 2015". Prova a fornire valori diversi e guarda cosa succede.

Immagine dello schermo
Modifica degli attributi di CalendarView .

8. Interagire con il componente

Hai provato a premere in un giorno specifico? Gli elementi interni dell'interfaccia utente nel nostro componente si comportano ancora nel modo normale previsto e attiveranno eventi in risposta alle azioni dell'utente. Quindi, come gestiamo questi eventi?

La risposta si compone di due parti:

  • Cattura eventi all'interno del componente e
  • Segnala gli eventi al genitore del componente (potrebbe essere un Fragment , Activity o anche un altro componente).

La prima parte è piuttosto semplice. Ad esempio, per gestire gli elementi della griglia che premono a lungo, assegniamo un listener corrispondente nella nostra classe componente:

 // long-pressing a day grid.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> view, View cell, int position, long id) { // handle long-press if (eventHandler == null) return false; Date date = view.getItemAtPosition(position); eventHandler.onDayLongPress(date); return true; } });

Esistono diversi metodi per segnalare gli eventi. Uno diretto e semplice consiste nel copiare il modo in cui lo fa Android: fornisce un'interfaccia agli eventi del componente implementata dal genitore del componente ( eventHandler nel frammento di codice sopra).

Alle funzioni dell'interfaccia possono essere trasmessi tutti i dati rilevanti per l'applicazione. Nel nostro caso, l'interfaccia deve esporre un gestore di eventi, a cui è passata la data del giorno premuto. La seguente interfaccia è definita in CalendarView :

 public interface EventHandler { void onDayLongPress(Date date); }

L'implementazione fornita dal genitore può essere fornita alla vista calendario tramite un setEventHandler() . Ecco un esempio di utilizzo da `MainActivity.java':

 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events); // assign event handler cv.setEventHandler(new CalendarView.EventHandler() { @Override public void onDayLongPress(Date date) { // show returned day DateFormat df = SimpleDateFormat.getDateInstance(); Toast.makeText(MainActivity.this, df.format(date), LENGTH_SHORT).show(); } }); }

La pressione prolungata di un giorno attiverà un evento di pressione prolungata che viene acquisito e gestito da GridView e segnalato chiamando onDayLongPress() nell'implementazione fornita, che, a sua volta, mostrerà la data del giorno premuto sullo schermo:

Immagine dello schermo

Un altro modo più avanzato per gestirlo è utilizzare Intents e BroadcastReceivers di Android. Ciò è particolarmente utile quando è necessario notificare l'evento del calendario a più componenti. Ad esempio, se la pressione di un giorno nel calendario richiede la visualizzazione di un testo in Activity e il download di un file da parte di un Service in background.

L'utilizzo dell'approccio precedente richiederà che l' Activity fornisca un EventHandler al componente, gestendo l'evento e quindi passandolo al Service . Invece, il fatto che il componente trasmetta un Intent e sia l' Activity che il Service lo accetti tramite i propri BroadcastReceivers non solo semplifica la vita, ma aiuta anche a disaccoppiare l' Activity e il Service in questione.

Conclusione

Guarda l'incredibile potenza della personalizzazione di Android!
Twitta

Quindi, ecco come creare il tuo componente personalizzato in pochi semplici passaggi:

  • Crea il layout XML e modellalo in base alle tue esigenze.
  • Deriva la tua classe del componente dal componente padre appropriato, in base al tuo layout XML.
  • Aggiungi la logica aziendale del tuo componente.
  • Utilizzare gli attributi per consentire agli utenti di modificare il comportamento del componente.
  • Per semplificare l'utilizzo del componente nella finestra di progettazione dell'interfaccia utente, utilizzare la funzione isInEditMode() di Android.

In questo articolo, abbiamo creato una vista calendario come esempio, principalmente perché la vista calendario stock è, per molti versi, carente. Ma non sei in alcun modo limitato sul tipo di componenti che puoi creare. Puoi usare la stessa tecnica per creare tutto ciò di cui hai bisogno, il cielo è il limite!

Grazie per aver letto questa guida, ti auguro buona fortuna per i tuoi sforzi di programmazione!