Cyril Mottier

β€œIt’s the little details that are vital. Little things make big things happen.” – John Wooden

Custom Animations With Fragments

Note: I generally blog about subjects I don’t deal with in my day to day life at work. However, the article below mentions some work I have done at Capitaine Train. As a consequence, I think a disclaimer is needed here: I work for Capitaine Train, but the opinions expressed on my blog or anywhere else (Twitter, Google+, etc.), are my own, and have nothing to do with my employer.

In the past few months, I have been working on developing an Android application from the ground up. This app named after the name of the company, Capitaine Train, can be downloaded on the Google Play Store. Capitaine Train - which can literally be translated as “Captain Train” in English - is a 3-year-old startup born from a simple truth: getting train tickets in Europe was a pain in the ass. We, at Capitaine Train, aim to revolutionize the way people travel all around Europe by simplifying the overall train experience. The release of the Android application clearly represented an important step forward in this direction.

Trying to revolutionize the train experience in Europe is not easy. It requires us to achieve a tremendous amount of work: getting to know the various carriers, learning about the document/reservation requirements for each of them, integrating their price/time tables, binding our servers to their systems, etc. From a user point of view all of this is the hidden, but vital, part of the iceberg. Indeed, a travel need or desire starts from a simple search request: From where? To where? When? Who? Although these questions are simple, the search step is extremely important in the booking process. This is where the trip actually begins after all! We designed the Android app keeping this essential idea in mind by simplifying every bit of the process. In this article, I would like to tell you the story behind the implementation of the search experience in the Android app and how we used animations to enrich the user experience.

From web to mobile

When I arrived at Capitaine Train to work on the Android application, I started looking at all of the current ongoing UI-based projects. Some, such as the iOS app, were private but shaping up rapidly. Some others, the web app for instance, were already public and rather well appreciated from our users. My main job, at that time, was to imagine an Android application that could make users feel they were using the best Android app out there to book train ticket. The app had to reflect both the Capitaine Train essence and the Android look ‘n feel. Because the web app was the only public app at this time, I obviously based most of my drafts on top of it. Here is what the search form looks like on capitainetrain.com1:

Play mp4

While the two-panes (search form + options) design works perfectly on desktop we rapidly faced an issue on mobile: we did not have enough space to put both the form and the options panes on the same screen. Because mobile screens are small, we had no other choice than falling-back to a master/detail pattern of some kind. Two well-known and simple options were available to us: the master/detail pattern and the edition dialogs pattern. But we were not satisfied by these patterns. Indeed, dialogs completely breaks the user flow and would have been extremely annoying when filling at least 4 fields in the form (i.e. 4 dialogs). On the other end, opening a fullscreen “option” Activity for each field edition would have lost the user in an extremely complex screen hierarchy and app structure. I seriously thought none of these patterns were effective nor a good fit for the Capitaine Train Android app.

We definitely wanted to replicate the simplicity and obviousness of the desktop search so we finally ended up with a nice approach. Rather than opening a modal screen for each edited form fields, we managed to merge the form pane and the options pane into a single screen. By default, the application displays a search form with all of the available fields. Tapping on a field switches the screen to an “edit mode” where the edited field is visible on top and the rest of the form disappears to reveal the options available on the field. The video below shows an entire search flow use case:

Play mp4

The user flow demonstrated above works very nicely because of the transitions we designed. Indeed, none of this would have been usable without them2. Adding transitions into your application is the best way to enrich user experience by making your users understand the consequences of their actions. As Newton said, to every action there is a reaction: transitions explain what is between two UI states. They also reduce the impression of “stacking screens” when navigating from one screen to another. It makes the user feel the application is made of a single screen where UI elements animate to show and/or dismiss some parts of the app. In other words, transitions break barriers and transform app navigation into a natural flow.

Splitting the transition apart

Transitions are generally quick and barely noticeable. In order to better understand, create and/or reverse-engineer them it is interesting to consider slowing them down. In case you are in control of the application’s code, you can obviously switch all animation durations to some greater values. If you’re not, you can screencast the application and watch the resulting video frame by frame or in slow motion. Fortunately, Android comes with another extremely useful technique: a developer option called “Animator duration scale”. As its name states, this options scales all animation durations system-wide with the chosen scale.

In order to better understand what is happening when transitioning between the search form and the date/time edition mode, let’s use the aforementioned technique. The screencast below shows what the transition looks like at a 10x scale:

Play mp4

Looking at the slowed down video, we can look at the edition mode transition in details. More specifically, you may have noticed the final transition is actually divided into several sub-animations that are played in parallel with the exact same timing properties (duration, interpolator, etc.):

  • The focus animation consists of translating towards the top the edited field (i.e. the one the user tapped on) and all fields on top of it. The translation distance is the difference between the focused field’s top and the container’s top. Translating the focused field using this distance results in having the focused field stick to the ActionBar’s bottom.
  • The fadeOutToBottom animation consists of dismissing all fields below the “focused field” to the bottom while fading them out away at the same time. The main purpose of this animation is to demonstrate the dismissed fields are not useful in the edition mode we are entering in.
  • The slideInToTop animation translates the options/edition panel in. It reveals the edition panel by translating it into the screen and fading it in at the same time.
  • The stickTo animation is optional and depends on the edited field. Because the “From/To” and “Depart/Return” are grouped, focusing on “From” or “Depart” requires hiding/overlaying the “To”/“Return” counter parts with a gray band. stickTo is just a y-axis-based translation of the gray band so that its top sticks to the focused field bottom.

The previously described sub-animations composed together creates the search form to edition mode transition. The counter part transition (i.e edition mode to search form) is not described here as it mainly consists on reversing the animations: unfocus, fadeInToTop, slideOutToBottom and unstickFrom.

Back to the code

Prior deep diving into the implementation details, it is important to point out Capitaine Train Android is compatible with Android 4.0+. I personally choose this minimum requirement in order to have full access to the ActionBar features as well as the new property-based animation framework. I obviously could have chosen to target a lower API level but this would have implied multiple code paths (ActionBarCompat VS built-in ActionBar) and the use of support libraries (ActionBarCompat, NineOldAndroids, etc.). I clearly thought we couldn’t match our quality minimum requirements targeting pre-4.0 Android releases. Finally targeting older releases of Android wouldn’t have helped us targeting our rather “tech-familiar” clients. As a side note, at the time of the writing, more than 50% of our install base run the lastest version of Android (4.4) while the official Android dashboard indicates only 8.5%.

Implementation details

Implementing the entire search form flow was a nice challenge. Indeed, we wanted the application to run as greatly as possible on every devices. Thus, we had do deal with a mammoth amount of screen sizes, densities and orientation. While it is generally not a problem at all with Android, it may start to become a small one when you create a fairly complex design. We mainly solved these issues by using a ScrollView as the root ViewGroup, using orientation-dependent field height and developing orientation-dependent layouts (for instance the date/time picker looks different in landscape).

From a developer point of view, Capitaine Train Android search form is part of a quite complex Activity: the HomeActivity. HomeActivity is clearly the first and main screen of the application. It is where 80% of our trip information can be found. HomeActivity is built on top of a ViewPager featuring 3 Fragment-based pages: SearchFragment, CartFragment and TicketsFragment. Each of these Fragments is represented by a tab in the UI.

As you can easily understand, SearchFragment is where most of the code lies. SearchFragment is made of a fairly complex View hierarchy that can be reduced to the simple layout below:

layout/fragment_search.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    android:id="@+id/main_container"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:ct="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.capitainetrain.android.widget.ScrollView
        android:id="@+id/normal_mode_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fillViewport="true"
        ct:autoScrollEnabled="false">

        <RelativeLayout
            android:id="@+id/form_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:clipToPadding="false"
            android:orientation="vertical"
            android:paddingBottom="@dimen/spacing_large">

            <!-- ... -->

        </RelativeLayout>

    </com.capitainetrain.android.widget.ScrollView>

    <FrameLayout
        android:id="@+id/edit_mode_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingTop="@dimen/form_field_height"
        android:visibility="invisible">

        <!-- ... -->

    </FrameLayout>

</FrameLayout>

Basically, SearchFragment is made of two distinct layouts. The first one, @id/normal_mode_container is the actual search form as you can see it when opening the application while the second one, @id/edit_mode_container is a simple container the field-dependent options pane will be added to.

Now that we know what the layout actually looks like, let’s finally focus on how the overall transition is performed. Whenever a field is tapped, SearchFragment adds (or replaces) a new Fragment to @id/edit_mode_container, switches the ActionBar to an ActionMode and starts animating to the “edition mode” using the animations described earlier. The newly added Fragment depends on the edit mode the user is entering in: SuggestionsFragment, DateTimePickerFragment, PassengersFragment. Just like we can put Views into ViewGroup, we put Fragments inside other Fragments. Nested Fragments have been introduced in JellyBean MR2 and are a great way of making sure your code is safely modularized and maintainable3. Although nested Fragments are API 17+, they have been back-ported back to API 4 and are available through the support library.

Animating search form UI elements is done thanks to the property-based animation framework introduced in Android 3.0. Because we wanted to use a simple and fluent API, we used ViewPropertyAnimator. ViewPropertyAnimator let’s you run optimized animations of select properties on View objects. However, ViewPropertyAnimator was not enough in some cases. Indeed, we sometimes had to manually compute the translation distance. For instance the “focus” animation requires the computation of the tapped field top to the root container top distance. If the focused field was a direct child of the container, we could have used the getTop() method. Unfortunately, this was not always the case. Fortunately, the framework comes with some handy methods that can offset View coordinates into a ancestor coordinate system. The trick consists of retrieving the View drawing rectangle (i.e. in its parent coordinate system) with View#getDrawingRect(Rect) and translating it into the ancestor coordinate system with ViewGroup#offsetDescendantRectToMyCoords(View, Rect). This is what the “focus” animation looks like in code (note that you can decide to animate or not - animation-less transitions are used when restoring the UI state after a configuration change):

1
2
3
4
5
6
7
8
9
10
11
12
13
private final Rect mTmpRect = new Rect();

private void focusOn(View v, View movableView, boolean animated) {

    v.getDrawingRect(mTmpRect);
    mMainContainer.offsetDescendantRectToMyCoords(v, mTmpRect);

    movableView.animate().
            translationY(-mTmpRect.top).
            setDuration(animated ? ANIMATION_DURATION : 0).
            setInterpolator(ANIMATION_INTERPOLATOR).
            start();
}

The fadeOutToBottom animation translates the View from half the height of @id/edit_mode_container. Note that precomputing the “half height” of @id/edit_mode_container requires the entire View hierarchy to be laid out. In order to do so, Capitaine Train Android relies on the OnLayoutChangeListener and its onLayoutChanged method:

1
2
3
4
5
6
7
8
private void fadeOutToBottom(View v, boolean animated) {
    v.animate().
            translationYBy(mHalfHeight).
            alpha(0).
            setDuration(animated ? ANIMATION_DURATION : 0).
            setInterpolator(ANIMATION_INTERPOLATOR).
            start();
}

Animating the edition panel in is done thanks to the slideInToTop animation:

1
2
3
4
5
6
7
private void slideInToTop(View v, boolean animated) {
  v.animate().
      translationY(0).
        alpha(1).
        setDuration(animated ? ANIMATION_DURATION : 0).
        setInterpolator(ANIMATION_INTERPOLATOR);
}

Finally the stickTo animation consists on translating a gray bar according to the focused field bottom:

1
2
3
4
5
6
7
8
9
10
11
private void stickTo(View v, View viewToStickTo, boolean animated) {

  v.getDrawingRect(mTmpRect);
    mMainContainer.offsetDescendantRectToMyCoords(v, mTmpRect);

    v.animate().
            translationY(viewToStickTo.getHeight() - mTmpRect.top).
            setDuration(animated ? ANIMATION_DURATION : 0).
            setInterpolator(ANIMATION_INTERPOLATOR).
            start();
}

I have not explained how Capitaine Train Android relies on ActionMode to switch the ActionBar to a contextual ActionBar. Doing so is fairly straight-forward and you only have to rely on the ActionBar APIs to do so. ActionModes are used extensively in SearchFragment in order to display a title and some optional actions that either describe or are in relationship with the displayed options pane. For instance, when selecting passengers, the ActionBar displays a “Passengers” title and give the user the opportunity to create new passengers.

Performance improvements tips

When everything was finally working perfectly I started to give a closer look at how smooth animations were. While animations were running almost correctly on a Nexus 5 running KitKat, I wasn’t satisfied at all when I switched to a plain old Galaxy Nexus running Android 4.3. Depending on the device, animations were sometimes always janky, sometimes only lagging once, sometimes not janky at all. Investigating the code, I managed to tweaked the animation a little bit and get an almost jank-free transition.

Hardware layers

As described earlier, the search form transitions heavily rely on alpha animations. When switching from a normal mode to the edit mode, the edition pane fades in and some search form field fades out at the same time. Because the system can’t directly draw the alpha animated elements on screen, it uses an offscreen buffer to render the frame and then draws the frame on screen with the alpha value of the current interpolation. The offscreen rendering mechanism is a mandatory (at least 95% of the time, the other 5% are addressed by the View#hasOverlappingRendering() method) and expensive process.

In order to avoid offscreen rendering on each animation frame, you can enable hardware layers on the animated View hierarchy for the duration of the animation. Enabling hardware layers basically asks the system to render the View hierarchy into an offscreen layer that can be considered as a rasterized bitmap copy of the actual View. With hardware layers on, all subsequent View property changes (translation, alpha, scale, etc.) are forwarded directly to the layer itself rather than invalidating the whole View and redrawing it.

Due to the offscreen rendering phase, hardware layers are generally enabled only during the time frame of the animation. Indeed, keeping hardware layers on when a View invalidates itself, requires the system to redraw its backing layer entirely prior compositing it on screen. To prevent such a performance drop, we created a special AnimatorListenerAdapter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class LayerEnablingAnimatorListener extends AnimatorListenerAdapter {

    private final View mTargetView;

    private int mLayerType;

    public LayerEnablingAnimatorListener(View targetView) {
        mTargetView = Objects.requireNonNull(targetView, "Target view cannot be null");
    }

    public View getTargetView() {
        return mTargetView;
    }

    @Override
    public void onAnimationStart(Animator animation) {
        super.onAnimationStart(animation);
        mLayerType = mTargetView.getLayerType();
        mTargetView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
    }

    @Override
    public void onAnimationEnd(Animator animation) {
        super.onAnimationEnd(animation);
        mTargetView.setLayerType(mLayerType, null);
    }
}

The LayerEnablingAnimatorListener is simply set as a listener to the ViewPropertyAnimators described above with by calling setListener(AnimatorListener).

Flattened View hierarchy

The early alpha (internal-only) releases of Capitaine Train was based on a calendar library from Square called TimeSquare. Although TimeSquare was a library that nicely fit our needs, it was also completely screwing our transitions up. Indeed, TimeSquare’s CalendarPickerView is a ListView made of several CalendarGridView (months) containing several CalendarRowView (weeks) in turn composed of several CalendarCellView (day). Because of the complex View hierarchy, we sometimes were displaying more than 400 Views at once. Inflating such a huge amount of Views requires a lot of time we don’t had. The first time the SuggestionsFragment were displayed inflation was taking around 300ms on my Nexus 5, completely wasting the 333ms-long transition.

The trick here was simply to flatten the View hierarchy. We completely dropped TimeSquare and designed a calendar from scratch. The current CalendarView implementation is also based on a ListView but where each MonthView draws directly on the Canvas (i.e. a single View renders a complete month)

Fragments reuse

SearchFragment allow users to set 5 different search properties. Nested Fragments are all added to the FragmentManager in SearchFragment’s onCreate. As discussed earlier, inflating View hierarchy can slow down the renderer waiting for completion. We minimized this issue by simply reusing Fragments whenever possible. As a consequence, “From” and “To” both use the same instance of “SuggestionsFragment” and “Depart” and “Return” also both rely on the same instance of DateTimePickerFragment. In addition to reducing inflation UI thread pauses, it also reduced memory consumption.

Furture improvements tracks

Being kind of a maniac person, I don’t consider the current release public release of Capitaine Train as perfect. I spent a lot of time tweaking the Capitaine Train application prior to the initial release but couldn’t do everything I had in my mind. Lack of time and startup reality just struck me. As an engineer, I simply made the best I could from the various components I had (time, design, code quality, performance, etc.). Here are some of the improvements I still have in mind to make things a little bit smoother:

  • The current implementation adds and hides edition Fragments in the SearchFragment onCreate method. When starting an edition mode, we show the corresponding Fragment. Internally, the system switches the Fragment visibility from GONE to VISIBLE. Because all nested Fragments uses a ListView, a bunch of View inflation happens the first time a Fragment is shown. In fact, ListView populates itself after it has been laid out. We could force the ListView to inflate its items as soon as the field is touched by using MotionEvent.ACTION_DOWN instead of MotionEvent.ACTION_UP. This could save us the amount of time between these two events (around 40 to 60ms).
  • SearchFragment make an extensive use of ViewPropertyAnimator. When transitioning to the edition mode, a bunch of ViewPropertyAnimator are started and run in parallel. We could prevent the animation system from managing all animations independently and use a single ValueAnimator of our own.

Conclusion

With the introduction of the new property-based animation framework and Fragments in Android 3.0, the framework provides developers with all the necessary tools to create wonderful and meaningful UIs while still keeping a maintainable and modularized code. Animating Fragments is generally a single ViewPropertyHolder API call away and may drastically improve the way users understand your application. Designing an application is not only about creating a nice static design. It is also about moving graphical elements in a way it is meaningful to users. Transitions both give life to an application and enrich user experience.


  • 1: Feel free to register and have fun with the Capitaine Train web application. Just like the Android app, it is available in English, French, German and Italian.

  • 2: The best way to understand the importance of transitions is to disable them temporarily. You can do so by disabling animations system-wide in the developer settings. Open the Settings application, go to “Developer options” and set the “Animator duration scale” to “Animation off”. Note that it may be required to restart the application so that the setting takes effect.

  • 3: Since their introduction, Fragments have been overwhelmingly used. They also have been overwhelmingly criticized for their complexity. Their lifecycle is extremely complex, they are quite verbose, they have several “modes” (created via code or via XML inflation), etc. Nested Fragments have been even more criticized. The purpose of this article is not to tell you how to develop your own application. Fragments and nested Fragments are complex indeed but once you control and master them, you can start enjoying them. Using them is a great way to create independent portion of code inside your application.