Illustration af Virginia Poltrack

Animering på en tidsplan

Animationer i Google I / O-appen

Jeg var for nylig en del af et godt team, der arbejdede på Google I / O 2018 Android-appen. Dette er en konference-ledsager-app, der tillader deltagere og fjerntliggende folk at finde sessioner, opbygge en personlig plan og reservere pladser på lokaliten (hvis du er heldig nok til at være der!). Vi byggede en række interessante animerede funktioner i appen, som jeg mener har forbedret oplevelsen meget. Koden til denne app er netop blevet åbent, og jeg ønskede at fremhæve et par af disse tilfælde og nogle interessante implementeringsdetaljer.

Nogle animerede elementer i I / O-appen

Generelt er der 3 typer animationer, vi brugte i appen:

  1. Heltanimationer - bruges til at styrke branding og bringe øjeblikke af glæde
  2. Skærmovergange
  3. Tilstandsændringer

Jeg vil gerne gå nærmere ind på et par af disse.

Nedtælling

En del af appens rolle er at opbygge spænding og forventning til konferencen. Som sådan i år inkluderede vi en stor animeret nedtælling til konferencestart vist både på ind-board-skærmen og i Info-sektionen. Dette var også en fantastisk mulighed for at integrere begivenhedens branding i appen, hvilket bragte en masse karakter.

Nedtællingen til konferencen starter

Denne animation blev designet af en bevægelsesdesigner og blev afleveret som en serie af Lottie json-filer: hver 1 sekund lang viser et nummer, der animerer 'ind' og derefter 'ud'. Lottie-formatet gjorde det nemt at slippe filerne i aktiver og tilbød endda bekvemmelighedsmetoder som setMinAndMaxProgress, som gjorde det muligt for os at spille bare den første eller sidste halvdel af en animation (for at vise et nummer, der animeres ind eller ud).

Den interessante del var faktisk orkestrering af disse flere animationer i den samlede nedtælling. For at gøre dette oprettede vi en brugerdefineret CountdownView, som er en temmelig kompleks ConstraintLayout, der indeholder et antal LottieAnimationViews. I dette oprettede vi en Kotlin-delegat til at indkapsle start af den relevante animation. Dette gjorde det muligt for os blot at tildele en Int til hver delegeret af det ciffer, det skulle vise, og delegaten ville opsætte og starte animationen (e). Vi udvidede delegationen ObservableProperty, som sikrer, at vi kun kørte en animation, når cifferet ændres. Vores animationssløjfe sendte derefter simpelthen en kørsel hvert sekund (når visningen er vedhæftet), som beregnet hvilket ciffer hver visning skulle vise og opdateret delegerede.

Reservation

En af appens vigtigste handlinger er at lade deltagere reservere pladser. Som sådan viste vi denne handling fremtrædende i en FAB på skærmen med sessiondetaljer. Vi følte, at det var vigtigt kun at rapportere, at sessionen var reserveret, når den var blevet afsluttet på backend (i modsætning til mindre vigtige handlinger som at star med en session, hvor vi opdaterer UI'en med det samme). Dette kan tage lidt tid, mens vi venter på et svar fra backend, så for at gøre dette mere lydhør, brugte vi et animeret ikon til at give feedback om, at vi arbejder på det, og for at overgå til den nye tilstand.

Feedback, mens du reserverer et sæde på en session

Dette kompliceres af det faktum, at der var et antal tilstande, som dette ikon var nødvendigt for at afspejle: sessionen kan være reserverbar, de har muligvis allerede reserveret et sæde, hvis sessionen er fuld, så er en venteliste muligvis tilgængelig, eller de kan være på venteliste eller tæt på sessionens startbestillinger er deaktiveret. Dette resulterede i mange permutationer fra forskellige stater til at animere imellem. For at forenkle disse overgange besluttede vi at altid gennemgå en 'arbejdende' tilstand; det animerede timeglas ovenfor. Derfor er hver overgang faktisk et par af: tilstand 1 → arbejder & arbejder → tilstand 2. Dette forenklet tingene meget. Vi byggede hver af disse animationer ved hjælp af shapeshifter; se avd_state_to_state-filer her.

For at få vist dette brugte vi en brugerdefineret visning og en AnimatedStateListDrawable (ASLD). Hvis du ikke har brugt ASLD før, er det (som navnet antyder) en animeret version af StateListDrawable, som du sandsynligvis er stødt på - hvilket giver dig mulighed for ikke kun at give forskellige tegnelige ting pr. Stat, men også overgange mellem tilstande (i form af en AnimatedVectorDrawable eller en AnimationDrawable). Her er tegningen, der definerer de statiske billeder og overgange til og ud af arbejdstilstanden for reservationsikonet.

Vi oprettede en brugerdefineret visning til understøttelse af vores egne brugerdefinerede tilstande. Visninger tilbyder nogle standardtilstande som presset eller valgt. Tilsvarende kan du definere din egen og få vist ruten Vis til alle tegnelige tegn, den viser. Vi definerede vores egen state_reservable, state_reserved osv. Vi oprettede derefter et enum af disse forskellige tilstande, indkapslede visningstilstanden plus eventuelle relaterede attributter, såsom en tilknyttet indholdsbeskrivelse. Vores forretningslogik kunne derefter blot indstille den passende værdi fra dette enum på visningen (via databinding), der ville opdatere tegnestatens tilstand, der startede en animation via ASLD. Kombinationen af ​​brugerdefinerede stater og AnimatedStateListDrawable var en pæn måde at implementere dette på, idet mange stater holdes i deklarative lag, hvilket resulterede i minimal visningskode.

Højttalerovergang

Mange af skærmovergange fungerede godt med standardvindueanimationer. Et sted, vi afvigede fra dette, er overgangen til højttalerdetaljer-skærmen. Dette viste højttalerbilledet på hver side af overgangen og var en perfekt kandidat til en delt elementovergang. Dette hjælper med at lette kontekstændringen mellem skærme.

En delt elementovergang

Dette er en temmelig standard delet elementovergang ved hjælp af platformene ChangeBounds og ArcMotion på en ImageView.

Det, der var mere interessant, er, hvordan indledningen af ​​denne overgang passede ind i det hændelsesmønster, vi brugte til navigation. I det væsentlige afkobler dette mønster inputbegivenheder (som at tape på en højttaler) fra navigationshændelser, hvilket sætter ViewModel ansvaret for, hvordan man reagerer på input. I dette tilfælde betyder denne afkobling, at ViewModel udsatte en LiveData of Events, som kun kendte højttalernes ID til at navigere til. At starte en overgang med delt element kræver den delte visning, som vi ikke havde på dette tidspunkt. Vi løste dette ved at gemme højttalerens ID som et mærke på visningen, når det er bundet, så visningen senere kan hentes, når vi skal navigere til en bestemt skærm for højttalerdetaljer.

filtre

En kerne del af konference-appen er at filtrere de mange begivenheder ned til dem, du er interesseret i. Hvert emne havde en farve forbundet med det for let genkendelse, og vi modtog et fantastisk design til en brugerdefineret 'chip', der skal bruges, når du vælger filtre:

Animerede filterchips

Vi kiggede på Chip fra materialekomponenter, men valgte at implementere vores egen brugerdefinerede visning for større kontrol over skærmen og animationen mellem 'markerede' tilstande. Dette implementeres ved hjælp af lærredstegning og en StaticLayout til visning af tekst. Visningen har en enkelt statusegenskab [0–1], som ikke er markeret - markeret. For at skifte tilstand, animerer vi simpelthen denne værdi og annullerer visningen og gengivelseskoden lineært interpolerer positioner og størrelser af elementer baseret på dette.

Oprindeligt, når jeg implementerede dette, fik jeg visningen til at implementere grænsefladen, der kan kontrolleres, og startede animationen, da metoden setChecked satte en ny tilstand. Da vi viser flere filtre i en RecyclerView, havde dette den uheldige effekt af at køre animationen, hvis et valgt filter rullede ud, og visningen blev rebound til et ikke valgt filter, der ruller i. Whoops. Vi tilføjede derfor en separat metode til at starte animationen, så vi kan skelne mellem den skiftes med et klik og en øjeblikkelig opdatering, når nye data bindes til visningen.

Da vi introducerede denne skiftanimation, fandt vi desuden, at den rykkede, dvs. tab af rammer. Var min animationskode skylden? Disse filtre vises i et bundskema foran skærmbilledet med hovedkonference. Når der skiftes et filter, starter vi den filtreringslogik, der skal anvendes på skemaet (og opdaterer antallet af matchende begivenheder i filterarkets titel). Nogle systrace-spillunking senere, fastlagde vi, at problemet var, at når filtre blev anvendt, ViewPager af RecyclerViews, der viser tidsplanen, pligtmæssigt gik og opdateret til de nyligt leverede data. Dette fik et antal visninger til at blive oppustet og bundet. Alt dette arbejde sprængte vores rammebudget ... men opdateringsplanen var ikke synlig, som den var bag filterarket. Vi tog beslutningen om at udskyde udførelsen af ​​den faktiske filtrering, indtil animationen var kørt, vi handlede noget mere implementeringskompleksitet for en jævnere brugeroplevelse. Oprindeligt implementerede jeg dette ved hjælp af postDelayed, men dette medførte problemer for UI-test. I stedet skiftede vi vores metode, der startede animationen til at acceptere en lambda, der skal køres på ende. Dette gjorde det muligt for os at respektere brugerens animationsindstillinger bedre og teste udførelsen korrekt.

Bliv animeret

Generelt har jeg lyst til, at animationerne virkelig har bidraget til appens oplevelse, karakter, branding og lydhørhed. Forhåbentlig har dette indlæg hjulpet med at forklare både hvorfor og hvordan de blev brugt og givet dig tip til deres implementering.