Beautiful, Performant Android UI

Instagram Engineering
Instagram Engineering
9 min readJun 21, 2016

--

At Instagram, our mission is to help people capture and share the world’s moments. We care deeply about the moments that people share on our platform, so enhancing how people view these moments is really important. Instagram recently launched a new design for Explore that includes “video channels”, which play in a new, full-screen, immersive experience. We optimized the user interface to make videos easier to watch, and it serves as a great example of how we approach building user interfaces on Android. We’ll share our approach and techniques, and hope they help you improve the look and feel of your own apps!

Overview: The Immersive Viewer

As we were building this video viewer, we had several goals in mind. We wanted to provide an immersive user experience where you come to “sit back and watch” funny, creative, and engaging videos from holidays and special events like Halloween, New Year’s Eve, and the Oscars. The viewer automatically scrolls for you at the end of each video, but you still have control over the viewer by being able to scroll at the pace you want (as in feed).

We also gave the immersive viewer a new look to make it a distinct experience from feed. Important differences to note are:

  • The viewer paginates and centers the playing video in the middle of screen for you so that you don’t have to scroll. In feed, you have to manually drag and make sure the video is on the screen. In the viewer, switching to the previous or the next video is as simple as a fling or tap.
  • We remove nonessential UI (such as video icons and feedback tools) to create fewer distractions.
  • We use full-screen mode, hiding the status bar, to make the viewer feel immersive.
  • The viewer has a dark theme and only the center video is highlighted to make it easier to focus while it’s playing.

The immersive viewer is built as a subclass of ListView, to which we added custom touch event handling by overriding dispatchTouchEvent(). This allows us to reroute the touch events through a GestureDetector to our gesture listener, which would then determine whether it should consume these events and perform a custom action, or do nothing and just let the ListView handle them.

Let’s take a fling action as an example. When you fling a video in the viewer, our gesture listener consumes it and initiates a custom pagination animation. The normal ListView fling action will not be triggered:

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
.. If velocityY meets threshold for pagination, initiate pagination in the correct direction ..
.. else, snap back to current video ..

return true;
}

Other scroll actions like dragging are not consumed by our gesture listener, and are instead dispatched to the ListView so that you can still drag the viewer as you would with a normal ListView.

Beautiful UI: Delight Users with Subtle, Natural Animations

Animations are fun. For this reason, they are often misused. It’s tempting to add flashy transitions that look really cool the first few times they are viewed, but quickly get obnoxious.

The best animations bridge the gap between action and reaction, clarifying changes in UI elements in response to an external or internal trigger (i.e. user input, video finished playing, etc). They are quick, smooth, and subtle. In fact, they are so subtle that they may not be consciously registered by the user as animations, rather contributing to a general feeling of delight.

For the immersive viewer, we used a combination of alpha and scale animations to accentuate certain UI elements during navigation. We applied spring-based interpolations, rather than polynomial, to make these animations feel natural.

Alpha Animations

Let’s say you need a view to appear or disappear in your UI. To achieve this effect, you’d normally just change its visibility via setVisibility(View.VISIBLE) or setVisibility(View.GONE). But when you test, it can feel quite jarring because there is no transition whatsoever but your UI is suddenly changed. Alpha animations can make this experience much nicer. We use many alpha animations in immersive viewer, including:

  • Fade in/out the video header containing the username
  • Fade in/out the dark color overlay for an idle video
  • Fade in/out the blurred cover overlay for an idle video
  • Fade in/out the top and bottom shadows for the center video
  • Fade in/out the heart that appears when double tapping to like

Taking the video header as an example, here is how it looks like with and without the alpha animation when navigating to another video:

Left (without animation), Right (with animation):

The alpha animations make these experiences more natural and smooth as the current header fades out and the next header fades in. Without the animation, the headers pop in and out and the UI feels choppy.

Spring Physics

Rebound is a library built by Will Bailey (a fellow Instagram engineer) that makes it easy to apply spring physics to animations. We use Rebound for many animations in our app because we believe the spring dynamics make them look more natural than the polynomial-based interpolations provided by Android’s native Animation and Animator classes. Also, Rebound gives us the ability to incorporate real world properties, such as the velocity at which the user flings their finger on screen, into a spring’s motion.

The pagination animation respects the velocity of the fling such that if you fling slowly, the viewer paginates with the respective low velocity. If you fling quickly, the viewer paginates with the respective high velocity. This natural experience is the reason why we chose springs to drive animations in the viewer.

When building the viewer, we started with a single spring to sync together all the animations that run in parallel as you navigate: the pagination animation and the alpha animations. We made this decision because if, for example, the pagination animation finishes before the alpha animations, the views would continue to change in opacity even after they stop moving.

Code structure and fling example:

Our scroller object, PagingListViewScroller, encapsulates spring operations and tracks information like the current list position of the center video. We also have the custom ListView, scrubber, and other components (e.g. fragment) that listen to notifications from the scroller.

When you fling a video in the viewer, it is handled as follows:

  • The ListView consumes the action and triggers a vertical scroll event through the scroller, which sets the underlying spring in motion with the appropriate velocity and target offset.
  • Then whenever the spring value updates, the scroller gets notified which in turn invokes the appropriate callback on each listener to take action.

Scale Animations

Scale animations can be used to create visual effects that complement the UI. When entering the immersive viewer, we have an opening animation where a black background starts with zero height in the middle of the screen and then scales up to fill the entire screen, after which we fade in the immersive viewer. The motivation behind this was to mimic the behavior of turning on an old television and give the transition a fun, nostalgic touch.

Left (Without animation), Right (With animation):

The transition is much smoother with the animation, and we avoid the sudden change in UI that we discussed previously with alpha animations.

Performant UI: It’s Not Optional

Building UI with cool animations and beautiful designs is important, but none of it is worthwhile if it isn’t performant. By making the correct optimizations, we can improve the design and feel of the UI without degrading performance. Using the Hierarchy Viewer to analyze view render times and the Traceview to profile method time spent, we optimized the immersive viewer to provide a smooth experience.

Reduce Number of Views

One optimization we made to the viewer was to use the minimum number of views. This is something to consider in any application; the more views you have on screen, the more work your device has to do to render them. Furthermore, they take up memory and time to be instantiated, which could significantly impact memory on lower end devices and slow down loading of the UI.

Our first implementation had four views per video:

  • Blurred cover image overlay
  • Dark color overlay
  • Top shadow
  • Bottom shadow

We reduced these views into one custom ImageView where the source is set to the blurred cover image. Then we overrode its onDraw() method to draw color for the dark color overlay and two drawables, one for each shadow. On animation steps, we set a custom alpha on each of these elements and invalidate the view, so that its onDraw() method will be called to be redrawn and reflect the current state of the animation.

Optimize Alpha Animations

There are numerous alpha animations in the immersive viewer that take place as you navigate, so we had to optimize the way we change the alpha property of elements.

There are different ways to change the alpha property — one is all setAlpha(float) on a View. This method is a two step process, where the first step is to allocate an off-screen buffer in GPU memory called a hardware layer and draw the view onto it. Then in the second step, the GPU copies pixels from the hardware layer to the screen, applying any alpha value that we set. The advantage of this two-step process is that the alpha blending will be correct on overlapping content on the screen. However, the significant downside is that the first step adds a lot of overhead.

There are two approaches to optimize this method, but each has tradeoffs:

view.setLayerType(View.LAYER_TYPE_HARDWARE, null);

  • One approach is to set hardware layer type on the view you’re animating. This caches the hardware layer and reuses it so that we only do the first step once. While this approach keeps the alpha blending correct and makes the alpha animation performant, it still takes up GPU memory, so you have to remember to release the layer when you’re done using it. Also, this approach loses much of its performance gains if you invalidate the view too many times because the first step is run again on each invalidation. Hence it is not applicable to the immersive viewer because the viewer is built on ListView, which frequently invalidates its children views due to recycling and view rebinding.

@Override
public boolean hasOverlappingRendering() {
return false;
}

  • Another approach is to create a custom view and override this method to return false. This bypasses the hardware layer and the view is drawn directly to screen when you change its alpha property. A downside is that the alpha blending will be incorrect on overlapping content, but this may not be a problem depending on your application. You may also have to create numerous custom views if you want alpha animations on multiple views, as in the case of immersive viewer. Most importantly, this method is only supported on API level 16 and above.

Another way to change the alpha property is to call setAlpha(int) on a Drawable. We chose this method for the alpha animations in immersive viewer as neither of the optimization approaches above fit our use case. This changes the alpha property of the drawable instance, as opposed to a view. It also doesn’t use any hardware layer, so the drawable is drawn directly onto screen. Compared to the hasOverlappingRendering() approach, this is better on two accounts. Not only is the method supported on all API levels, but we can also easily change the alpha property of the drawable underlying our view instead of defining a custom view. The downside is that the alpha blending will be incorrect on overlapping content. However, this is acceptable for the viewer as there are very few instances of overlapping content and we do not need 100% correct alpha blending even when elements overlap.

Here’s an example in the immersive viewer to show the difference in performance between the two setAlpha methods:

Left (setAlpha on views), Right (setAlpha on drawables):

By setting alpha on views, the viewer begins to stutter as we scroll and its performance becomes worse with all the allocation of hardware layers. By setting alpha on drawables, the viewer remains smooth throughout the navigation.

Redraw Specific Views

It’s important to redraw only the views that changed, especially during animations. For a ListView, this means invalidating only the child views whose properties have changed instead of calling notifyDataSetChanged() on the adapter, which redraws the entire ListView. In immersive viewer, we do not call notifyDataSetChanged() during our animations because we only want to reflect the UI state changes of the current video and the next video to play instead of the entire list.

Wrapping Up

For every one of our features, we strive to provide the best user experience possible without regressing performance. Immersive viewer is no exception. We used various animations to make video viewing on Instagram more delightful, but we also put a ton of effort into making performance optimizations to ensure this delight is shared amongst all users, not just those with high-end devices. We hope that you can incorporate some of our approaches to building beautiful and performant experiences into your own Android development!

Kevin Jung is a software engineer at Instagram.

--

--