Animating on a Schedule
Animations in the Google I/O app
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.
Generally there are 3 types of animations we used in the app:
- Hero animations — used to reinforce branding and bring moments of delight
- Screen transitions
- State changes
I’d like to go into detail of a few of these.
Part of the role of the app is to build excitement and anticipation for the conference. As such this year we included a large animated countdown to the conference start displayed both on the on-boarding screen and in the Info section. This was also a great opportunity to embed the event’s branding into the app, bringing a lot of character.
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.
One of the key actions of the app is letting attendees reserve seats. As such we displayed this action prominently in a FAB on the session details screen. We felt that it was important to only report that the session was reserved once it had successfully completed on the backend (unlike less important actions like starring a session where we optimistically update the UI immediately). This might take a little time while we wait for a response from the backend so to make this more responsive we used animated icon to provide feedback that we’re working on it and to smoothly transition into the new state.
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
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_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.
Many of the screen transitions worked well with the standard window animations. One place we deviated from this is the transition into the speaker details screen. This displayed the speakers image on either side of the transition and was a perfect candidate for a shared element transition. This helps ease the context change between screens.
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
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.
A core part of the conference app is filtering the many events down to those you’re interested in. Each topic had a color associated with it for easy recognition and we received a great design for a custom ‘chip’ to use when selecting filters:
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
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.
Overall I feel like the animations really contributed to the experience, character, branding and responsiveness of the app. Hopefully this post has helped explain both why and how they were used and given you pointers to their implementation.