Reveal Challenge

I saw an interesting take on the RevealTransition from Johannes Homeier. The circular reveal was to shrink and cross-fade to a circle, then move, then grow to the final position. At first I thought that this would be a fairly easy thing to do and the more I thought about it, the more interesting it was, so I thought I’d give it a go.

Fortunately for me, Johannes provided me a nice mockup so I could picture it:

reveal_move_transition

How would you make this transition work? I’m sure there are several ways to do this, but I decided to make a complete transition in the called Activity that replaces ChangeBounds. To accommodate the cross-faded Views, I needed to create Views and add them to the overlay and do all of the animation in the overlay.

I started with the project I used for the Reveal Activity Transition. First, let’s look at the view hierarchy for the called Activity:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin">

  <ImageView
      android:transitionName="hello"
      android:id="@+id/planter"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:adjustViewBounds="true"
      android:layout_alignParentTop="true"
      android:src="@drawable/planter"
      />
</RelativeLayout>

This is much simpler than we had in the Reveal Activity Transitions project. Because I’m replacing the entire shared element transition, including the ChangeBounds transition, I can just do a normal shared element transition. No funny stuff required.

Now, my shared element transition is very simple:

<transition
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    class="com.sample.revealactivitytransition.CircleTransition"
    app:color="@android:color/holo_green_dark"/>

Here, I’m using a custom Transition with a custom tag “color” in the transition XML. Very nice.

Now, what do we do in the CircleTransition?

  1. I need a bitmap image of the start state so that I can do a cross-fade.
  2. Reveal in reverse the shared element start state and a uniform color.
  3. Cross-fade in the color over the start state image.
  4. Move a circle View from the start position to the final position.
  5. Reveal the end state view and the solid color
  6. Cross-fade the solid color

That’s not so bad. First, I need to get a bitmap of the starting state. I could share this with my transition in a more efficient way, but I want my Transition to be generic and reusable in other situations. I just had it take a snapshot of the View when capturing the start state:

@Override
public void captureStartValues(TransitionValues transitionValues) {
    final View view = transitionValues.view;
        if (view.getWidth() <= 0 || view.getHeight() <= 0) {
        return;
    }
    captureValues(transitionValues);
    Bitmap bitmap = Bitmap.createBitmap(view.getWidth(),
                                        view.getHeight(),
                                        Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    view.draw(canvas);
    transitionValues.values.put(PROPERTY_IMAGE, bitmap);
}

I don’t need the bitmap of the end state because the View will exist in the end state. I can already cross-fade to that View.

When creating the Animator, I need to add the bitmap into the overlay as well as the solid-color Views that are cross-faded in and out. Then, it is just a matter of using the ObjectAnimator and the circular reveal Animator. There is one trick: the circular reveal Animator runs on the render thread and after the animator runs, the view is completely revealed. If I tie the removal of view to the onAnimatorEnd, then there may be a frame in which the animator completes and the view is revealed, showing a blink. I needed to hide the view one frame early to make sure that View doesn’t blink exposed.

That crossed-out section is wrong! I learned from John Reck that the RevealAnimator guarantees that the onAnimatorEnd will be received prior to the next UI draw call. If we set view Visibility, it will register in the correct frame.

 shrinkingAnimator.addListener(new AnimatorListenerAdapter() {
     @Override
     public void onAnimationEnd(Animator animation) {
         shrinkingView.setVisibility(View.INVISIBLE);
         startView.setVisibility(View.INVISIBLE);
         circleView.setVisibility(View.VISIBLE);
     }
 });

Because we’re doing reveal and fade simultaneously on the solid Views, we really should make the system work a little more efficiently. Typically, we would want any fading View to be on its own hardware layer. However, we really shouldn’t do that here — the reveal would cause the layer to be redrawn on every frame (bad!). Having hasOverlappingRendering() return false will make it more efficient. ImageViews do that normally, but I didn’t think of that before making the project.

There’s still some work to do in the Activity. We need to have the start state show the Hello World button instead of a shrunken planter image, so just like in the previous project, we need to move the snapshot into the layout. I don’t want to use the snapshot View because I want the shared element to do the entire Transition.

@Override
public void onSharedElementStart(List sharedElementNames,
                                 List sharedElements,
                                 List sharedElementSnapshots) {
    ImageView sharedElement = (ImageView) findViewById(R.id.planter);
    for (int i = 0; i < sharedElements.size(); i++) {
        if (sharedElements.get(i) == sharedElement) {
            View snapshot = sharedElementSnapshots.get(i);
            Drawable snapshotDrawable = snapshot.getBackground();
            sharedElement.setBackground(snapshotDrawable);
            sharedElement.setImageAlpha(0);
            forceSharedElementLayout();
            break;
        }
    }
}

@Override
public void onSharedElementEnd(List sharedElementNames,
                               List sharedElements,
                               List sharedElementSnapshots) {
    ImageView sharedElement = (ImageView) findViewById(R.id.planter);
    sharedElement.setBackground(null);
    sharedElement.setImageAlpha(255);
}

I’m just setting the background of the ImageView to the snapshot and hiding the planter picture with setImageAlpha(0). The captureStartValues will capture a bitmap of the View with the shared element snapshot. Now, we don’t have any guarantees about the snapshot View — it could be any class and the image may not be in the background. Therefore, I need to create the snapshot View myself:

@Override
public View onCreateSnapshotView(Context context,
                                 Parcelable snapshot) {
    View view = new View(context);
    view.setBackground(new BitmapDrawable((Bitmap) snapshot));
    return view;
}

and in the calling Activity, I have to provide the right Parcelable:

@Override
public Parcelable onCaptureSharedElementSnapshot(View sharedElement, Matrix viewToGlobalMatrix, RectF screenBounds) {
    int bitmapWidth = Math.round(screenBounds.width());
    int bitmapHeight = Math.round(screenBounds.height());
    Bitmap bitmap = null;
    if (bitmapWidth > 0 && bitmapHeight > 0) {
        Matrix matrix = new Matrix();
        matrix.set(viewToGlobalMatrix);
        matrix.postTranslate(-screenBounds.left, -screenBounds.top);
        bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight,
                                     Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        canvas.concat(matrix);
        sharedElement.draw(canvas);
    }
    return bitmap;
}

This is stolen right from the support library code (and simplified a bit). Take a look at the results:

You can download the project here. Enjoy!

14 thoughts on “Reveal Challenge

    • It is similar! It appears that they aren’t using a circular reveal there — I see a shrinking rounded rect fading in. It happens very fast in the video, so it is hard to see exactly what is going on.

  1. In your post you use the following code:

    circleAnimator.setStartDelay(duration - ValueAnimator.getFrameDelay());

    You also say “If I tie the removal of view to the onAnimatorEnd(), then there may be a frame in which the animator completes and the view is revealed, showing a blink.”

    The part that worries me is the “may”… is this unpredictable and if so, why? I ask because delaying an animation by the device’s frame delay is something I’ve never seen done in application-level code before and just seemed like something that should already be handled by the framework.

  2. “Because we’re doing reveal and fade simultaneously on the solid Views, we really should make the system work a little more efficiently. Typically, we would want any fading View to be on its own hardware layer. However, we really shouldn’t do that here — the reveal would cause the layer to be redrawn on every frame (bad!). Having hasOverlappingRendering() return false will make it more efficient. ImageViews do that normally, but I didn’t think of that before making the project.”

    I assume you are referring to the NoOverlapView that you’ve introduced in your sample project. Are you saying that this simple wrapper is not actually necessary (since the ImageView#hasOverlappingRendering() will return false anyway?

  3. One last question… the sample project is written around Activity Transitions, but is writing something similar for Fragment Transitions possible as well? I ask because according to the SharedElementCallback documentation, the create/capture snapshot methods apparently won’t be called when fragment transitions take place.

    • With Fragment Transitions, you are guaranteed to know more about the Views than if you are in different applications. You can capture the image of the shared element during the onSharedElementStart and pass it directly to the onSharedElementEnd using a member field. Or, since you know the View, you could duplicate it exactly in your launched Fragment. You don’t need the complexity that Activity Transitions needs.

  4. In your post you mention “This is stolen right from the support library code (and simplified a bit).”

    I’m just wondering… where in the support library did you steal the code from? Do you have a link? Thanks. 🙂

  5. Hi George, I’ve been reading material online (including your blog posts) to implement scene transitions for a project I am working on. While experimenting, I encountered a heavy leak of sharedElementSnapshots that causes apps to crash very quickly. While trying to figure out a workaround and browsing the scene transition source code I later noticed that you implemented some of these APIs. I also found that this issue has been reported at different stages a few times but have not seen any workaround.

    https://code.google.com/p/android-developer-preview/issues/detail?id=1543
    https://code.google.com/p/android/issues/detail?id=190196
    https://code.google.com/p/android/issues/detail?id=170469

    Curious as to how apps have been using these APIs since the simplest xusage causes amounts of MBs to be leaked due to the bitmap snapshots. I created a sample project to demo the leak and attached it here, based on the RevealActivityTransitions project you posted:

    https://code.google.com/p/android/issues/detail?id=190196

    I was wondering if you could provide some input as to whether there is any way this leak can be avoided, without disabling shared element transitions themselves or nulling snapshots. Currently I am trying to reimplement onCaptureSharedElementSnapshot and onCreateSnapshotView to be able to null the bitmaps after using them.

    • Yes, it is unfortunate that this wasn’t caught and fixed sooner. The only way I can think to fix this locally is to override the onCaptureSharedElementSnapshot and either return null (I think this works) or place the parcel from super into a Bundle that you can later clear. Most don’t use the snapshot.

      If you need the snapshot, you’ll have to also override the onCreateSnapshotView to unpackage the parcel from the bundle before calling super (or handle the snapshot yourself in any way you want).

      Sadly, this will just slow the leak down, but at least it won’t be kilobytes on each call.

Leave a comment