Building VTS

The Official VTS Engineering Blog

Smooth Shared Android Transitions

By Jack Mann

Our group recently introduced shared Android transitions between activities in the VTS application. Most shared transitions were easy to implement and impressive in keeping the user focused on key elements of the application between primary views. When we tried to accomplish the same with downloaded image files, we encountered unreasonable delays and inconsistent presentation of shared views. For instance, the destination image would contain only a portion of the target image. The following is a recipe for shared transitions between ImageView’s that use downloaded png or jpeg files.

Benefits of VTS Approach The VTS approach is elegant. It produces an image overlay of the first activity’s thumb image that overlays each window and moves to the final image location in the destination activity. The eye stays focused on this key element providing clarity and the addition of newly requested information. When the first transition is complete a low resolution image is smoothly transitioned to its full resolution counterpart. And the same occurs in the reverse direction for consistency.

Basic Settup In this situation, Android shared transitions use Google’s recommended tagging of both the source view’s ImageView and destination view’s ImageView in the xml layout with a single line similar to the following:

android:transitionName=”SharedTransitionElement”

The source activity intent, uses this name along with a reference to the source view as follows:

startActivity(destinationIntent, ActivityOptions.makeSceneTransitionAnimation(this, create((View)sharedImageView, “SharedTransitionElement”);

While any popular Image Management and Storage Library can be used, VTS uses Picasso for image downloads and Cloudinary for storage. Cloudinary offers the advantage of image resizing and positioning on the server and Picasso provides bitmap downloads with listeners when on success or error conditions.

Approach for Source ImageView Activity

In the source ImageView Activity the developer must:

  • present the initial square thumb image and prepare for the transition. We use the following Picasso command to retrieve a low pixel count thumb image:
1
2
3
4
Picasso.with(context)
  .load(imageUrl)
  .fit()
  .into(thumbImageView);
  • prepare the destination ImageView’s low resolution image. In our application, the thumb and header images are the same, but have different proportions. Because the full resolution destination ImageView image will take time to upload, we fill Picasso’s cache with low resolution bitmaps formatted for the proportions of the destination view using the command:
1
Picasso.with(context).load(lowResolutionHeaderUrl);
  • once the cache contains low resolution destination images, we are ready for the shared transition. This involves grabbing the low resolution bitmap from cache and attaching it as an extra to the shared transition intent. For brevity, we created a Picasso Target to provide onBitmapLoaded and onBitmapFailed callbacks and use the same command as above, but append:
1
.into(target);

Then we can simply attach the bitmap to the intent using:

intent.putExtra(“HEADER_BITMAP_KEY”, lowResolutionHeaderBitmap);

before firing the intent, we will need to know if the image received by the destination activity came from cache or the cloud. The easiest way to determine this using Picasso, is to track the difference in total cache hits. A cache hit count can be obtained using:

Picasso.with(context).getSnapShot().cacheHits;

and should be attached to the intent as well.

Approach for Destination ImageView Activity

In the destination ImageView Activity, the developer’s goal is to populate the ImageView with an initial view (with either the low resolution bitmap or full resolution bitmap if available) and to fade in the high resolution bitmap once the shared Android transition is complete. Two code segments are required to do so:

  • to load the initial view, we use Picasso to grab the full resolution bitmap as before using targets. If the end user has visited this page before, the image is immediately available and can be used as the destination snapshot. Otherwise, it can be transitioned to when available:
1
2
3
4
5
6
7
8
9
10
11
@override
public void onBitmapAvailable(Bitmap bitmap) {
  mFullResolutionBitmap = bitmap;

  if (mSharedTransitionIsComplete) {
      transitionFromLowToFullResolutionBitmap();
  }
}

Bitmap initialBitmap = mFullResolutionBitmap != null ? mFullResolutionBitmap : mThumbBitmap;
destinationImageView.setImageBitmap(initialBitmap);
  • if a full resolution image is loading, we must wait for the shared element transition to complete. This can be accomplished with the following code segment:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mSharedTransitionIsComplete = true;

final Transition sharedEnterTransition = getWindow().getSharedElementEnterTransition();
sharedEnterTransition.addListener(new TransitionListener() {

  @Override
  public void onTransitionEnd(Transition transition) {
      super.onTransitionEnd(transition);

      determineThumbToFullResolutionTransitionStrategy();

      sharedEnterTransition.removeListener(this);
  }

}
  • then we must determine if a full resolution image should be faded in. The fade in is only required if the image is not from cache (was inserted start-up) or if both the low and high resolution images are available. To determine if the image is from cache, the current cache count can be subtracted from the count fed from the source activity. Any change greater than 1 would indicate that the image is from cache. The method referenced above is as follows:
1
2
3
if (! headerImageIsFromCache() && LowAndFullResolutionImageAreAvailable()) {
  transitionFromBitmapToFullResolutionImage();
}
  • a TransitionDrawable is used to fade in the full resolution image as follows:
1
2
3
4
5
6
7
8
9
10
private void transitionFromBitmapToFullResolutionImage() {

  TransitionDrawable finalImageTransitionDrawable = new TransitionDrawable(new Drawable[] {
      new BitmapDrawable(getResources(), mLowResolutionBitmap),
      new BitmapDrawable(getResources(), mFullResolutionBitmap)
  });

  destinationImageView.setImageDrawable(finalImageTransitionDrawable);
  finalImageTransitionDrawable.startTransition(TRANSITION_DURATION);
   }

Other Considerations

Several other options should be considered when implementing this solution. A default image should be presented when no network connectivity is available or a bitmap is simply not available for the destination (and source) ImageView. Use the same scaling rule for the destination ImageView’s Low and High resolution bitmaps. You want the blurred background image and the full resolution image features to overlap so the fade in looks natural.

Let me know how this approach works for you.