Illustration by Virginia Poltrack

Animating on a Schedule

Animations in the Google I/O app

Nick Butcher
Aug 22, 2018 · 7 min read

I was recently part of a great team working on the Google I/O 2018 Android app. This is a conference companion app, allowing attendees and remote folks to find sessions, build a personalized schedule and reserve seats at the venue (if you’re lucky enough to be there!). We built a number of interesting animated features in the app that I believe greatly enhanced the experience. The code for this app has just been open sourced and I wanted to highlight a few of these instances and some interesting implementation details.

Some animated elements in the I/O app

Generally there are 3 types of animations we used in the app:

  1. Hero animations — used to reinforce branding and bring moments of delight
  2. Screen transitions
  3. State changes

I’d like to go into detail of a few of these.

Countdown

The countdown to the conference start

This animation was designed by a motion designer and handed off as a series of Lottie json files: each 1 second long showing a number animating ‘in’ then ‘out’. The Lottie format made it easy to drop the files into assets and even offered convenience methods like setMinAndMaxProgress which allowed us to play just the first or last half of an animation (to show a number animating in or out).

The interesting part was actually orchestrating these multiple animations into the overall countdown. To do this we created a custom CountdownView which is a fairly complex ConstraintLayout holding a number of LottieAnimationViews. In this, we created a Kotlin delegate to encapsulate starting the appropriate animation. This allowed us to simply assign an Int to each delegate of the digit it should display and the delegate would set up and start the animation(s). We extended the ObservableProperty delegate which ensures that we only ran an animation when the digit changes. Our animation loop then simply posted a runnable every second (when the view is attached) which calculated which digit each view should display and updated the delegates.

Reservation

Feedback whilst reserving a seat at a session

This is complicated by the fact that there were a number of states this icon needed to reflect: the session might be reservable, they may already have reserved a seat, if the session is full then a waitlist might be available or they may be on the waitlist, or close to the session starting reservations are disabled. This resulted in many permutations of different states to animate between. To simplify these transitions, we decided to always go through a ‘working’ state; the animated hourglass above. Therefore each transition is actually a pair of: state 1 → working & working → state 2 . This simplified things greatly. We built each of these animations using shapeshifter; see the avd_state_to_state files here.

To display this, we used a custom view and an AnimatedStateListDrawable (ASLD). If you haven’t used ASLD before, it is (as its name implies) an animated version of StateListDrawable that you likely have encountered — allowing you to not only provide different drawables per state but also transitions between states (in the form of an AnimatedVectorDrawable or an AnimationDrawable). Here’s the drawable defining the static images and the transitions into and out of the working state for the reservation icon.

We created a custom view to support our own custom states. Views offer some standard states like pressed or selected. Similarly, you can define your own and have the View route that to any Drawables it is displaying. We defined our own state_reservable, state_reserved etc. We then created an enum of these different states, encapsulating the view state plus any related attributes such as an associated content description. Our business logic could then simply set the appropriate value from this enum on the view (via data binding) which would update the drawable’s state, which kicked off an animation via the ASLD. The combination of custom states and AnimatedStateListDrawable was a neat way to implement this, keeping the multitude of states in the declarative layers, resulting in minimal view code.

Speaker transition

A shared element transition

This is a pretty standard shared element transition, using the platform ChangeBounds and ArcMotion classes on an ImageView.

What was more interesting is how initiating this transition fitted into the Event pattern we used for navigation. Essentially, this pattern decouples input events (like taping on a speaker) from navigation events, putting the ViewModel in charge of how to respond to input. In this case, this decoupling means that the ViewModel exposed a LiveData of Events, which only knew the ID of the speaker to navigate to. Initiating a shared element transition requires the shared View, which we did not have at this point. We solved this by storing the speaker’s ID as a tag on the view when it is bound, so that the view can later be retrieved when we need to navigate to a particular speaker details screen.

Filters

Animated filter chips

We looked at Chip from Material Components but opted to implement our own custom view for greater control over the display and animation between ‘checked’ states. This is implemented using canvas drawing and a StaticLayout for displaying text. The view has a single progress property [0–1] modelling unchecked–checked. To toggle the state, we simply animate this value and invalidate the view and the rendering code linearly interpolates positions and sizes of elements based on this.

Initially when implementing this, I made the view implement the Checkable interface and kicked off the animation when the setChecked method set a new state. As we display multiple filters in a RecyclerView, this had the unfortunate effect of running the animation if a selected filter scrolled out and the view was rebound to an unselected filter scrolling in. Whoops. We therefore added a separate method to kick off the animation allowing us to differentiate between it being toggled by a click and an immediate update when binding new data to the view.

Additionally, when we introduced this toggle animation, we found that it was janking i.e. dropping frames. Was my animation code to blame? These filters are displayed in a BottomSheet in front of the main conference schedule screen. When a filter is toggled, we initiate the filtering logic to be applied to the schedule (and update the number of matching events in the filter sheet title). Some systrace spelunking later, we determined that the issue was that when the filters were applied, the ViewPager of RecyclerViews displaying the schedule dutifully went off and updated to the newly delivered data. This caused a number of views to be inflated and bound. All of this work was blowing our frame budget… but the updating schedule wasn’t visible as it was behind the filter sheet. We made the decision to delay performing the actual filtering until the animation had run, we traded off some more implementation complexity for a smoother user experience. Initially I implemented this using postDelayed but this caused issues for UI tests. Instead we switched our method which started the animation to accept a lambda to be run on end. This allowed us to better respect the user’s animation settings and test the execution properly.

Get Animated

Android Developers

The official Android Developers publication on Medium

Thanks to Florina Muntenescu and Jose Alcérreca

Nick Butcher

Written by

Android designer + developer @ Google

Android Developers

The official Android Developers publication on Medium

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade