Quando un semplice input non basta

Logiche di UX e implementazione Android di un HorizontalPicker

Angelo Moroni
Inside Bemind
7 min readJul 11, 2017

--

Nel prossimo autunno, molto probabilmente, uscirà una nuova funzionalità di HYPE e volevamo cogliere l’occasione di fare esperimenti e miglioramenti sulla UX dell’applicazione. In questo articolo parleremo in particolare dell’implementazione di un campo di input per l’importo; parleremo delle idee da cui siamo partiti e delle soluzioni che abbiamo trovato. Infine, troverete anche il link al progetto di esempio su Github nel quale troverete tutto il codice nella sua interezza.

Swipe is better than tap

Per ottenere un’esperienza utente migliore, non ci accontentavamo di un semplice campo di input nel quale digitare l’importo corretto, ma volevamo qualcosa che fosse più agevole per l’utente. L’ideale, dunque, era avere un picker che si potesse scorrere tramite una serie di swipe.

Non stiamo assolutamente dicendo che il numero di swap necessari sia inferiore al numero di tap totali per inserire l’importo, anzi, potrebbe darsi che siano anche di più. Quello a cui volevamo arrivare, non è tanto un numero minore di ditate effettuate dall’utente, ma sostituire un’operazione meccanica con una più elegante.

Oltre a questo, comunque sia, ci sono anche altri motivi per preferire questo tipo di input:

  • L’importo da inserire non è libero, bensì ha valori discreti
  • L’insieme dei valori disponibili non è numero elevato e dunque ci si aspetta che l’utente non impieghi tantissimo ad arrivare all’importo desiderato

Non si può negare, allora, che lo swipe sia una buona soluzione per raggiungere questo obiettivo.

Horizontal picker con l’elemento più a sinistra selezionato

Entriamo nello specifico: quello di cui avevamo bisogno era un picker orizzontale che avesse come elemento selezionato il primo elemento visibile, quello più a sinistra per intenderci. Inoltre, ciò che distingue l’elemento selezionato dagli altri è semplicemente il valore dell’alpha: totale per l’elemento selezionato e il 50% per tutti gli altri.

Cosa avevamo a disposizione

La prima cosa che insegnano a chi sta imparando la programmazione è che non si deve reinventare la ruota. Quindi, la prima cosa che abbiamo fatto è cercare un componente già implementato che potesse fare al caso nostro. Spoiler: non l’abbiamo trovato, altrimenti non saremmo stati qui a scrivere questo articolo.

Le prime difficoltà con cui ci siamo scontrati è che, di norma, tutti i picker, orizzontali o verticali che sia, hanno l’elemento centrale come quello selezionato.

Horizontal picker standard

L’idea, dunque, di farne uno proprio in casa è venuta quando ci siamo imbattuti in un componente molto simile: horizontal view di Adityagohad. Ci siamo detti, perché non modificare questo codice? Ci sembrava l’impegno meno oneroso, ma alla fine da questo codice non abbiamo preso altro che l’idea di modificare il LinearLayoutManager. Comunque sia, ci teniamo tanto a ringraziare Adityagohad perchè ci ha dato lo spunto iniziale su cui lavorare.

All’opera

In sostanza, l’Horizontal Picker che abbiamo implementato non è altro che un’estensione della RecyclerView nella quale non si fa altro che inizializzare i componenti utili:

  • Il LinearLayoutManager
  • L’adapter
  • Lo SnapHelper
  • L’itemDecoration

LeftmostElementPickerLayoutManager

La vera magia avviene dentro il LinearLayoutManager: è questo il suggerimento che abbiamo preso da Adityagohad.

All’interno della nostra classe custom abbiamo dovuto fare l’override dei seguenti metodi:

public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state)public void onScrollStateChanged(int state)

All’interno del primo metodo possiamo trovare il pezzo di codice che fa la vera selezione:

Il secondo metodo invece, non fa altro che aspettare che lo scroll termini e, solo in quel caso, notificare al listener (la definizione l’interfaccia di questo listener è interna alla classe) del nuovo elemento selezionato.

Piccola osservazione: abbiamo dovuto inserire anche il metodo findFirstVisibleItem(). Il nome del metodo è molto indicativo, infatti ci è servito per ottenere il primo elemento visibile e permettere la selezione (non importa che sia completamente visibile, perché, poi, sarà compito dello SnapHelper, di cui parleremo dopo, a farcelo vedere interamente).

In ogni caso, insieme a questo nuovo metodo, abbiamo inserito altri metodi utili. Utilizziamo la parola inseriti e non implementati perché in effetti non li abbiamo implementati, ma li abbiamo presi da questa gist di Mustafasevgi che ci è stata utilissima! In parole povere, questi metodi non fanno altro che restituirci con esattezza gli elementi. Di seguito potrete vedere tutti i metodi inseriti:

View findFirstVisibleItem()int findFirstVisibleItemPosition()View findFirstCompletelyVisibleItem()int findFirstCompletelyVisibleItemPosition()private View findOneVisibleChildren()

Non abbiamo inserito i metodi che riguardano gli ultimi elementi perché non ci sarebbero serviti.

La logica della selezione è completa, ora andiamo a vedere l’adapter.

Adapter

Per implementare l’adapter non potevamo che usare la bellissima libreria fornita da MEiDIK: in poche righe di codice si riesce a fare tutto quello che prima si doveva fare implementando un proprio adapter. Vi invitiamo, davvero, a dargli un’occhiata, vi farà risparmiare tantissimo tempo.

In ogni caso, l’uso che ne abbiamo fatto noi è abbastanza limitante perchè non è per nulla dinamico. La cosa ideale sarebbe di rendere dinamica questa parte cosicché si possa adattare a tutte le necessità, purtroppo però, nell’esempio che troverete su Github, per ora troverete la versione base.

SnapHelper

Android ha, da non troppo tempo, introdotto un oggetto che aiuta a fare lo snap di una lista, purtroppo, però, quello ufficiale fa lo snap solo sull’elemento centrale della lista, ma noi avevamo bisogno che lo snap avvenisse prendendo come riferimento il primo. Anche qui, abbiamo deciso di non reinventare la ruota e abbiamo utilizzato un’implementazione trovata all’interno di questo progetto presente su Github . Lo snap aumenta di tantissimo la qualità del componente, lo rende elegante e molto user friendly.

Grazie allo StartSnapHelper, inoltre, potevamo selezionare il primo elemento anche se non totalmente visibile, perché grazie a questo componente l’elemento selezionato si riesce a mettere automaticamente in prima posizione totalmente visibile.

A questo punto, il componente sembrava praticamente finito, ma come ogni film la parte succulenta è subito prima del finale, e infatti…

EndOffsetDecoration

A questo punto, quello che succedeva è che la lista, appena arrivata alla fine non concedeva ulteriori scroll e quindi l’ultimo elemento non sarebbe mai arrivato in prima posizione per essere selezionato. L’idea quindi era questa: dovevamo trovare un modo per aggiungere dello spazio in fondo, dopo l’ultimo elemento.

Non potevamo semplicemente aggiungere del padding all’ultima view, perchè altrimenti lo avremmo ritrovato in altri elementi, poiché l’adapter riutilizza le view, allora abbiamo provato ad aggiungere del padding proprio alla RecyclerView. Dopo i primi esperimenti avevamo capito che questa soluzione era la strada giusta solo che non ci piaceva tanto aggiungere il padding, bensì abbiamo deciso di inserire un ItemDecoration.

Ne abbiamo implementato uno custom, che non fa altro che aggiungere un offset in fondo alla lista.

Ma quanto doveva essere lungo l’offset da aggiungere? Come potrete ben immaginare, la lunghezza dell’offset deve essere dinamica perchè varia a seconda del dispositivo su cui ci troviamo.

Il nostro obiettivo era portare l’ultimo elemento in prima posizione e per tanto l’offest dovrebbe essere quasi pari alla lunghezza della RecyclerView stessa. La parola chiave, in questo caso, è proprio quel quasi: infatti se fosse lungo quanto il totale della lista, l’ultimo elemento sarebbe andato oltre e la magia dello snap non sarebbe avvenuta, quindi basterebbe sottrarre un qualcosa e il gioco è fatto. Sarebbe stato ideale sottrarre la lunghezza media di un elemento, ma alla fine, nella versione base, viene sottratta una costante di 50 px.

A questo punto, però, è sorto un altro problema: quando inserire questo ItemDecoration? Per necessità di implementazione non può essere messo durante lo scroll, ma allo stesso tempo dovevamo essere sicuri di quanto fosse lunga la RecyclerView. L’unica cosa da fare, dunque, era quello di aggiungere un GlobalLayoutListener all’interno dell’HorizontalPicker.

Ora ci siamo, il picker è completamente funzionante.

Diamo una limata finale

Tuttavia, il picker così implementato avvia la selezione solo nel momento in cui inizia lo scroll e questo causa che all’inizio non troveremo nessun elemento selezionato.

Dunque, per far sì che venga selezionato un elemento anche alla creazione del picker, non abbiamo fatto altro che creare un metodo nel quale ci sarà la logica di selezione e anche tale metodo verrà richiamato nel OnGlobalLayoutListener.

Risultato finale

Alla fine

Alla fine non abbiamo fatto altro che fare di necessità virtù, avevamo un obiettivo da raggiungere e abbiamo cercato di raggiungerlo il più velocemente possibile scrivendo un codice facilmente manutenibile.

In origine, comunque, non era nostra intenzione fare una libreria da inglobare nei progetti e per questo vi rimandiamo semplicemente all’esempio di Github. Non escludiamo, allo stesso tempo, però, che in futuro potremmo farci un pensierino.

Infine, volevamo dirvi che siamo aperti a qualsiasi suggerimento per migliorare l’HorizontalPicker anche al fine di creare una libreria.

--

--

Angelo Moroni
Inside Bemind

Code Monkey #Android per Bemind Interactive e ciarlatano digitale. (Aspirante) Scrittore e Filosofo. http://instagram.com/hooloovoochimi…