Recentemente ho fatto parte del fantastico team che ha lavorato all'app Android Google I/O 2018. È un'app complementare per conferenze che consente ai partecipanti e a chi si trova in remoto di cercare sessioni, creare programmi personalizzati e prenotare posti all'evento (se si ha la fortuna di essere lì!). Nell'app abbiamo sviluppato molte interessanti funzionalità animate che, a mio parere, hanno migliorato notevolmente l'esperienza utente. Il codice di questa app è appena stato reso open source e quindi vorrei segnalare alcuni dettagli e istanze di implementazione interessanti.
Ci sono 3 tipi di animazioni che abbiamo usato maggiormente nell'app:
Mi piacerebbe parlare più nel dettaglio di alcune di queste funzionalità.
Parte della funzione dell'app è creare entusiasmo e attesa per la conferenza. Pertanto, quest'anno abbiamo incluso un grande conto alla rovescia animato per marcare l'inizio della conferenza, visualizzato sia nella schermata di on-boarding sia nella sezione Info. È stata anche un'ottima opportunità per incorporare il marchio dell'evento nell'app dandole così più carattere.
Questa animazione è stata progettata da un motion designer e distribuita come serie di file JSON Lottie: ognuno di 1 secondo che mostra un numero animato in ingresso "in" e in uscita "out". Il formato Lottie ha semplificato il rilascio dei file in asset e ha persino offerto metodi convenienti come setMinAndMaxProgress che ci hanno permesso di riprodurre solo la prima o l'ultima metà di un'animazione (per mostrare un numero animato in ingresso o in uscita).
In generale è stato interessante gestire le varie animazioni del conto alla rovescia. Per ottenere questo obiettivo abbiamo creato una CountdownView personalizzata, che è un ConstraintLayout abbastanza complesso contenente diverse LottieAnimationViews. In questo caso abbiamo creato un delegato Kotlin per incapsulare l'avvio dell'animazione giusta. Ciò ci ha permesso di assegnare semplicemente un Int a ciascun delegato della cifra che doveva essere visualizzata e, a sua volta, il delegato eseguiva l'impostazione e l'avvio delle animazioni. Abbiamo esteso il delegato ObservableProperty per garantire che l'animazione fosse eseguita solo al variare della cifra. Quindi il nostro loop di animazione pubblicava un runnable ogni secondo (quando la visualizzazione era allegata) che calcolava la cifra da mostrare per ogni visualizzazione e poi aggiornava i delegati.
Una delle azioni chiave dell'app è consentire ai partecipanti di prenotarsi. Pertanto abbiamo messo questa azione in evidenza in un FAB nella schermata dei dettagli della sessione. Abbiamo ritenuto importante segnalare solamente che la sessione era stata riservata, una volta che l'operazione veniva completata correttamente sul backend (a differenza delle azioni meno importanti, come la valutazione di una sessione, per cui ottimisticamente l'UI veniva aggiornata subito). Ricevere una risposta dal backend poteva richiedere tempo quindi, per rendere il processo più dinamico, abbiamo creato un'icona animata per indicare che il processo è in corso e passare agevolmente al nuovo stato.
Il processo è complicato dal fatto che questa icona rappresenta diversi stati: la sessione che può essere prenotata, l'utente che può aver già prenotato, se la sessione è esaurita potrebbe essere disponibile una lista d'attesa o l’utente potrebbe essere già sulla lista d'attesa o, con l’avvicinarsi della sessione, le prenotazioni potrebbero non essere più disponibili. Ciò dà luogo a molte transizioni di stato da animare. Per semplificare queste transizioni, abbiamo deciso di passare sempre attraverso uno stato "in corso", ossia la clessidra animata mostrata sopra. Pertanto ogni transizione è costituita da una coppia di stati: stato 1 → in corso e in corso → stato 2. Questo approccio ha semplificato moltissimo le cose. Abbiamo creato ognuna di queste animazioni usando shapeshifter; vedi i file avd_state_to_state qui.
Per mostrare ciò, abbiamo utilizzato una visualizzazione personalizzata AnimatedStateListDrawable (ASLD). Se non hai mai usato ASLD, si tratta di una versione animata di StateListDrawable, come suggerisce il nome, che hai probabilmente già visto e consente, non solo di fornire diverse risorse drawable per stato ma anche transizioni tra stati (sotto forma di AnimatedVectorDrawable o AnimationDrawable). Ecco la risorsa drawable che definisce le immagini statiche e le transizioni in ingresso e uscita dello stato "in corso" per l'icona della prenotazione.
Abbiamo creato una visualizzazione personalizzata per supportare i nostri stati personalizzati. Le visualizzazioni offrono alcuni stati standard come Premuto o Selezionato. Analogamente, puoi definire il tuo stato e avere il Percorso di visualizzazione a tutte le risorse drawable mostrate. Abbiamo definito gli state_reservable, state_reserved ecc. Poi abbiamo creato un Enum dei diversi stati incapsulando lo stato della visualizzazione più eventuali attributi correlati, come la descrizione dei contenuti associati. La nostra logica business potrebbe semplicemente impostare il valore appropriato dall'Enum alla visualizzazione (tramite associazione dati) aggiornando lo stato della risorsa drawable che, a sua volta, ha avviato l'animazione tramite ASLD. La combinazione degli stati personalizzati e AnimatedStateListDrawable è stata un ottimo modo per implementare questo processo, mantenendo i diversi stati ai livelli dichiarativi e dando luogo a un codice di visualizzazione minimo.
Molte transizioni dello schermo funzionavano bene con le animazioni nelle finestre standard. La situazione era un po' diversa per la transizione nella schermata dei dettagli del relatore. Mostrava l'immagine dei relatori da entrambi i lati della transizione ed era una soluzione ideale per transizioni di elementi condivisi perché facilitava il cambio di contesto tra schermate.
Si tratta di una transizione di elementi condivisi standard che utilizza le classi ChangeBounds e ArcMotion della piattaforma su un ImageView.
L’aspetto più interessante è il modo in cui l'avvio di questa transizione si adattava al pattern Evento usato per la navigazione. In sostanza, questo pattern disaccoppia gli eventi di input (come l'azione di tocco di un relatore) dagli eventi di navigazione, incaricando ViewModel di rispondere all'input. In questo caso, il disaccoppiamento implicava che ViewModel esponesse un LiveData degli Eventi, che conosceva solo l'ID del relatore a cui indirizzarsi. L’inizializzazione di una transizione condivisa di elementi richiede la Visualizzazione condivisa che non avevamo a questo punto. Abbiamo risolto il problema memorizzando l'ID del relatore come tag sulla visualizzazione quando è associato, in modo che possa essere recuperato in un secondo momento per navigare verso una particolare schermata dei dettagli del relatore.
Una parte fondamentale dell'app per conferenze è filtrare gli eventi a cui siamo interessati. Ogni argomento viene associato a un colore per essere riconosciuto facilmente. Abbiamo anche ottenuto un ottimo design per il "chip" personalizzato da utilizzare per la selezione dei filtri.
Abbiamo preso in considerazione Chip di Material Components ma poi abbiamo optato per la nostra visualizzazione personalizzata che offriva un maggiore controllo sul display e sull'animazione tra i vari stati "selezionati". È stata implementata usando il disegno su canvas e uno StaticLayout per la visualizzazione del testo. La visualizzazione ha una singola proprietà di avanzamento [0-1] che indica la modalità selezionata-deselezionata. Per cambiare lo stato, il valore viene animato e la visualizzazione viene invalidata, consentendo al codice di rendering di interpolare linearmente le posizioni e le dimensioni degli elementi in base a ciò.
Inizialmente, durante l'implementazione, ho fatto in modo che la visualizzazione implementasse l'interfaccia Checkable e ho avviato l'animazione quando il metodo setChecked impostava un nuovo stato. Poiché si visualizzavano più filtri in un RecyclerView, ciò ha avuto lo sfortunato effetto di eseguire l'animazione se il filtro selezionato si spostava "out" e la visualizzazione veniva riassociata a un filtro non selezionato che si era spostato "in". Ops. Abbiamo quindi aggiunto un altro metodo per avviare l'animazione che ci permettesse di distinguere tra l'attivazione da clic e l'aggiornamento immediato quando si associavano nuovi dati alla visualizzazione.
Inoltre, quando abbiamo introdotto questa animazione toggle, abbiamo scoperto che si trattava di una procedura di janking, ovvero che comportava la perdita di fotogrammi. Era colpa del mio codice di animazione? Questi filtri sono visualizzati in un BottomSheet davanti alla schermata del programma principale della conferenza. Attivando un filtro, avviamo la logica del filtro da applicare al programma (e aggiorniamo il numero di eventi corrispondenti nel titolo del foglio del filtro). Dopo un'analisi approfondita di systrace abbiamo individuato il problema, ossia che quando i filtri venivano applicati, il ViewPager di RecyclerViews, che mostrava il programma diligentemente, si attivava e aggiornava in base ai dati appena forniti. Ciò determinava l'ingrandimento e l'associazione di diverse visualizzazioni. Tutto questo lavoro stava facendo saltare il budget dei fotogrammi... ma il programma di aggiornamento non era visibile perché era nascosto dal foglio del filtro. Abbiamo deciso di ritardare l'utilizzo del filtro vero e proprio fino a quando non veniva eseguita l'animazione, e quindi abbiamo rinunciato a una maggiore complessità di implementazione a favore di un'esperienza utente più fluida. Inizialmente ho implementato questa funzionalità usando PostDelayed ma ciò ha causato problemi durante i test dell'UI. Invece abbiamo cambiato il metodo che ha avviato l'animazione per accettare un lambda da eseguire alla fine. Ciò ci ha permesso di rispettare meglio le impostazioni di animazione dell'utente e testare correttamente l'esecuzione.
Nel complesso, penso che le animazioni abbiano davvero contribuito all'esperienza, al carattere, al branding e alla reattività dell'app. Speriamo che questo post abbia contribuito a spiegare sia il motivo sia il metodo utilizzati, e a fornirti buone indicazioni su come implementare le animazioni.
Animazione in un programma è stato originariamente pubblicato su Android Developers del blog Medium, utile per continuare la conversazione e offrire feedback su questa storia.