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:

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?

I need a bitmap image of the start state so that I can do a cross-fade. Reveal in reverse the shared element start state and a uniform color. Cross-fade in the color over the start state image. Move a circle View from the start position to the final position. Reveal the end state view and the solid color 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!