Postponed Shared Element Transitions (part 3b)

Posted

This post continues our in-depth analysis of shared element transitions by discussing an important feature of the Lollipop Transition API: postponed shared element transitions. It is the fourth of a series of posts I will be writing on the topic:

Until I write part 4, an example application demonstrating some advanced activity transitions is available here.

We begin by discussing the need to postpone certain shared element transitions due to a common problem.

Understanding the Problem

A common source of problems when dealing with shared element transitions stems from the fact that they are started by the framework very early in the Activity lifecycle. Recall from part 1 that Transitions must capture both the start and end state of its target views in order to build a properly functioning animation. Thus, if the framework starts the shared element transition before its shared elements are given their final size and position and size within the called Activity, the transition will capture the incorrect end values for its shared elements and the resulting animation will fail completely (see Video 3.3 for an example of what the failed enter transition might look like).

Video 3.3 - Fixing a broken shared element enter animation by postponing the transition. Click to play.

Whether or not the shared elements’ end values will be calculated before the transition begins depends mainly on two factors: (1) the complexity and depth of the called activity’s layout and (2) the amount of time it takes for the called activity to load its required data. The more complex the layout, the longer it will take to determine the shared elements’ position and size on the screen. Similarly, if the shared elements’ final appearance within the activity depends on asynchronously loaded data, there is a chance that the framework might automatically start the shared element transition before that data is delivered back to the main thread. Listed below are some of the common cases in which you might encounter these issues:

  • The shared element lives in a Fragment hosted by the called activity. FragmentTransactions are not executed immediately after they are committed; they are scheduled as work on the main thread to be done at a later time. Thus, if the shared element lives inside the Fragment’s view hierarchy and the FragmentTransaction is not executed quickly enough, it is possible that the framework will start the shared element transition before the shared element is properly measured and laid out on the screen.1

  • The shared element is a high-resolution image. Setting a high resolution image that exceeds the ImageView’s initial bounds might end up triggering an additional layout pass on the view hierarchy, therefore increasing the chances that the transition will begin before the shared element is ready. The asynchronous nature of popular bitmap loading/scaling libraries, such as Volley and Picasso, will not reliably fix this problem: the framework has no prior knowledge that images are being downloaded, scaled, and/or fetched from disk on a background thread and will start the shared element transition whether or not images are still being processed.

  • The shared element depends on asynchronously loaded data. If the shared elements require data loaded by an AsyncTask, an AsyncQueryHandler, a Loader, or something similar before their final appearance within the called activity can be determined, the framework might start the transition before that data is delivered back to the main thread.

postponeEnterTransition() and startPostponedEnterTransition()

At this point you might be thinking, “If only there was a way to temporarily delay the transition until we know for sure that the shared elements have been properly measured and laid out.” Well, you’re in luck, because the Activity Transitions API2 gives us a way to do just that!

To temporarily prevent the shared element transition from starting, call postponeEnterTransition() in your called activity’s onCreate() method. Later, when you know for certain that all of your shared elements have been properly positioned and sized, call startPostponedEnterTransition() to resume the transition. A common pattern you’ll find useful is to start the postponed transition in an OnPreDrawListener, which will be called after the shared element has been measured and laid out:3

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // Postpone the shared element enter transition.
    postponeEnterTransition();

    // TODO: Call the "scheduleStartPostponedTransition()" method
    // below when you know for certain that the shared element is
    // ready for the transition to begin.
}

/**
 * Schedules the shared element transition to be started immediately
 * after the shared element has been measured and laid out within the
 * activity's view hierarchy. Some common places where it might make
 * sense to call this method are:
 * 
 * (1) Inside a Fragment's onCreateView() method (if the shared element
 *     lives inside a Fragment hosted by the called Activity).
 *
 * (2) Inside a Picasso Callback object (if you need to wait for Picasso to
 *     asynchronously load/scale a bitmap before the transition can begin).
 *
 * (3) Inside a LoaderCallback's onLoadFinished() method (if the shared
 *     element depends on data queried by a Loader).
 */
private void scheduleStartPostponedTransition(final View sharedElement) {
    sharedElement.getViewTreeObserver().addOnPreDrawListener(
        new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                sharedElement.getViewTreeObserver().removeOnPreDrawListener(this);
                startPostponedEnterTransition();
                return true;
            }
        });
}

Despite their names, these two methods can also be used to postpone shared element return transitions as well. Simply postpone the return transition within the calling Activity’s onActivityReenter() method instead:4

/**
 * Don't forget to call setResult(Activity.RESULT_OK) in the returning
 * activity or else this method won't be called!
 */
@Override
public void onActivityReenter(int resultCode, Intent data) {
    super.onActivityReenter(resultCode, data);

    // Postpone the shared element return transition.
    postponeEnterTransition();

    // TODO: Call the "scheduleStartPostponedTransition()" method
    // above when you know for certain that the shared element is
    // ready for the transition to begin.
}

Despite making your shared element transitions smoother and more reliable, it’s important to also be aware that introducing postponed shared element transitions into your application could also have some potentially harmful side-effects:

  • Never forget to call startPostponedEnterTransition() after calling postponeEnterTransition. Forgetting to do so will leave your application in a state of deadlock, preventing the user from ever being able to reach the next Activity screen.

  • Never postpone a transition for longer than a fraction of a second. Postponing a transition for even a fraction of a second could introduce unwanted lag into your application, annoying the user and slowing down the user experience.

As always, thanks for reading! Feel free to leave a comment if you have any questions, and don’t forget to +1 and/or share this blog post if you found it helpful!


1 Of course, most applications can usually workaround this issue by calling FragmentManager#executePendingTransactions(), which will force any pending FragmentTransactions to execute immediately instead of asynchronously.

2 Note that the postponeEnterTransition() and startPostponedEnterTransition() methods only work for Activity Transitions and not for Fragment Transitions. For an explanation and possible workaround, see this StackOverflow answer and this Google+ post.

3 Pro tip: you can verify whether or not allocating the OnPreDrawListener is needed by calling View#isLayoutRequested() beforehand, if necessary. View#isLaidOut() may come in handy in some cases as well.

4 A good way to test the behavior of your shared element return/reenter transitions is by going into the Developer Options and enabling the “Don’t keep activities” setting. This will help test the worst case scenario in which the calling activity will need to recreate its layout, requery any necessary data, etc. before the return transition begins.

+1 this blog!

Android Design Patterns is a website for developers who wish to better understand the Android application framework. The tutorials here emphasize proper code design and project maintainability.

Find a typo?

Submit a pull request! The code powering this site is open-source and available on GitHub. Corrections are appreciated and encouraged! Click here for instructions.

Apps by me

Shape Shifter simplifies the creation of AnimatedVectorDrawable path morphing animations. View on GitHub.
2048++ is hands down the cleanest, sleekest, most responsive 2048 app for Android!