Reveal Activity Transitions

Lots has been going on recently in planning for the M release, so I haven’t had as much of a chance to work on this as I had hoped when I made my last post. But, finally! I have a moment to write on using the Reveal Transition we saw in the previous post with Lollipop Activity Transitions.

Remember that Activity Transitions using Transitions was introduced in Lollipop, so this doesn’t work in Kit-Kat and earlier versions.

I’m sure you can imagine many uses for the RevealTransition. In my case, I have a button in the first Activity that I want to transition to a second Activity. However, the button isn’t in the second Activity. Instead, an image is in the launched activity. I’m going to use the circular reveal to remove the button and then show the image.

This is what I want to achieve:

Now, let’s start with an Android Studio project and set up the starting scene in the launching Activity. Instead of a “Hello World” TextView, I’ve made it a button:

 <Button
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:layout_alignParentBottom="true"
   android:layout_centerHorizontal="true"
   android:background="@android:color/holo_green_dark"
   android:textAllCaps="false"
   android:textSize="20sp"
   android:padding="20dp"
   android:text="@string/hello_world"
   android:textColor="@android:color/white"
   android:onClick="launch"
   android:transitionName="hello" />

I know that you would never hard-code the values in your application, right? But I can be lazy in my demo. The only really interesting thing here is the

 android:transitionName="hello"

This is the name that I’ve given to this element. When sharing an element using Activity Transitions, views should be given a unique transitionName. In this example, I only have one potential shared element. In cases where you have several potential shared elements, each should be given a different transitionName. For example, if you have list of contacts, each contact image may have a transitionName with String.valueOf(contactId). For example, in an Adapter

@Override
public View getView(int position, View convertView,
                    ViewGroup parent {
    ...
    view.setTransitionName(String.valueOf(contact.getId()));
    return view;
}

In my click handler, I need to launch a new Activity:

 public void launch(View view) {
     Intent intent = new Intent(this, Launched.class);
     ActivityOptions options = ActivityOptions
         .makeSceneTransitionAnimation(this, view, "hello");
     startActivity(intent, options.toBundle());
 }

The three parameter makeSceneTransitionAnimation is a shortcut to creating an Activity Transition with one shared element. The third parameter looks like the same String as in the transitionName, but that is just a coincidence. The third parameter is the name that the Activities agreed to associate with the shared element. When multiple shared elements are passed between Activities, each must have a unique name. Remember that Activity Transitions can work between applications and there doesn’t have to be any shared code, so this is the agreed API for the shared views. Also, since either or both sides could have different transitionNames for their shared elements, this is the only common link between the Activities.

Next, we have to set the target scene in the launched Activity. I’d like the button to move to the center of the ImageView. To do that, I have to have it in the same FrameLayout:

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

   <FrameLayout
       android:id="@+id/button"
       android:layout_gravity="center"
       android:transitionName="hello"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>
   </FrameLayout>
 </FrameLayout>

Now, you may be asking, “Why do you have both an ImageView and a FrameLayout for the button?” In my case, I want two different Views acting as the shared element. It would be possible to use one, but it takes a bunch of extra work.

As to the second part of the question: why use a FrameLayout for the button? That is a special case for the RevealTransition. Activity Transitions moves shared elements by setting the position at the start and again at the end and then uses the Transition system to animate between the two states. Our RevealTransition only animates from the center of the View. If we move the View, our circle stays in the same place as the View moves away from it! Therefore, if we want the reveal to work properly, it has to remain in the same position and so, we must move its parent instead. The FrameLayout is the button’s parent that will move from its start location to the center of the ImageView.

You can see that our FrameLayout has a transitionName “hello” and I made it match the name passed in as the shared element name in makeSceneTransitionAnimation. Because it matches, the framework will find it and map that FrameLayout to the shared element. If nothing matched, I would have to map it myself by setting the enter SharedElementCallback and overriding onMapSharedElement to the map. But I’ve made my life easy here.

In the launched Activity, I’m pretending as if I don’t know what the button looks like and am using the image snapshot. Otherwise, I’d just put it into the FrameLayout directly. To use the snapshot, set the shared element callback:

setEnterSharedElementCallback(new SharedElementCallback() {
  View mSnapshot;

  @Override
  public void onSharedElementStart(List<String> sharedElementNames,
      List<View> sharedElements, List<View> sharedElementSnapshots) {
    for (int i = 0; i < sharedElementNames.size(); i++) {
      if ("hello".equals(sharedElementNames.get(i))) {
        FrameLayout element = (FrameLayout) sharedElements.get(i);
        mSnapshot = sharedElementSnapshots.get(i);
        int width = mSnapshot.getWidth();
        int height = mSnapshot.getHeight();
        int widthSpec = View.MeasureSpec.makeMeasureSpec(width,
            View.MeasureSpec.EXACTLY);
        int heightSpec = View.MeasureSpec.makeMeasureSpec(height,
            View.MeasureSpec.EXACTLY);
        mSnapshot.measure(widthSpec, heightSpec);
        mSnapshot.layout(0, 0, width, height);
        mSnapshot.setTransitionName("snapshot");
        element.addView(mSnapshot);
        break;
      }
    }
    if (mSnapshot != null) {
      mSnapshot.setVisibility(View.VISIBLE);
    }
    findViewById(R.id.planter).setVisibility(View.INVISIBLE);
  }
});

I’ve set the planter image to be invisible at the start. I want to have that revealed at the end of the transition. I’ve also added the snapshot of the shared element “hello” to the View hierarchy. You can see that I’ve forced a layout on it so that it is positioned properly inside the FrameLayout. I’ve also given the snapshot a transitionName “snapshot” so that I can refer to it in my transition.

I must also set the final state for both the hello button, which should be invisible, and the planter image, which should be visible.

public void onSharedElementEnd(List<String> sharedElementNames,
    List<View> sharedElements, List<View> sharedElementSnapshots) {
  if (mSnapshot != null) {
    mSnapshot.setVisibility(View.INVISIBLE);
  }
  findViewById(R.id.planter).setVisibility(View.VISIBLE);
}

We also must hide the planter image from the framework so that it doesn’t treat it as an entering element. We could treat it as an entering element, but then it would be difficult to coordinate the removal of the button with the planter image. Any View that isn’t visible at the time the shared elements are mapped won’t be entering the scene, so we can hide it then.

@Override
public void onMapSharedElements(List<String> names,
    Map<String, View> sharedElements) {
  findViewById(R.id.planter).setVisibility(View.INVISIBLE);
}

Let’s set up the enter transition:

<transitionSet
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000">
  <changeBounds />
  <changeTransform />
  <changeImageTransform />
  <transitionSet android:transitionOrdering="sequential">
    <transition
        class="com.sample.revealactivitytransition.RevealTransition"
        android:transitionVisibilityMode="mode_out"/>
    <transition
        class="com.sample.revealactivitytransition.RevealTransition"
        android:transitionVisibilityMode="mode_in"/>
    <targets>
      <target android:targetId="@id/planter" />
      <target android:targetName="snapshot" />
    </targets>
  </transitionSet>
</transitionSet>

In this transition, I’ve setup the moving parts of the transition to work while the snapshot is being removed and after it is removed, the planter image is revealed. Ok, now we need to modify the application to use the transition. I like to use XML where possible, so I update the styles.xml. If you’re not paying attention, you may end up modifying the wrong styles.xml! I did this and it took me half a day to figure out what was going wrong. It turns out that Android Studio automatically creates a styles.xml in the values-v21 directory. That’s the one we want to modify, not the one in the values directory.

<style name="AppTheme" parent="android:Theme.Material.Light">
  <item name="android:windowSharedElementEnterTransition">@transition/shared_element_enter</item>
</style>

Now the shared elements will use my new transition to move into the scene. Note that the theme parent is Theme.Material.Light. If I had used Holo or one of the other older themes, Activity Transitions aren’t enabled by default. You must then add the following line to your style to enable them:

<item name="android:windowActivityTransitions">true</item>

Now the enter transition works. The snapshot of the button uses the circular reveal to be removed from the scene while it is being moved and then the planter image is revealed from the final location.

The reverse is not difficult to set up, but there are a few tricks. The first is that if you do an orientation or otherwise cause the Activity to be recreated, our snapshot has not been added to the scene. We have to add the snapshot in the onSharedElementEnd. Since it wasn’t there, we also have to force the FrameLayout’s size to be correct as well. The second is that the planter blinks when going back. The onMapSharedElement sets the planter to be INVISIBLE so that it isn’t treated as a leaving element, so I’ve found that if I just set it to visible in the finishAfterTransition, it is visible again before the transition starts.

Here is the complete Android Studio project.

 

Advertisements

26 thoughts on “Reveal Activity Transitions

  1. Great example, thank you George.

    Question — have you tried an expanding animation with a Google mapView? Animations haven’t historically worked with a mapView because it’s SurfaceView based, and I haven’t been able to get it to work in Lollipop either. Wondering if you’ve ever tried it?

    • I haven’t tried this with MapView. SurfaceView will almost certainly be a problem for animators and RevealAnimator specifically. You could work around the problem by a taking snapshot of the MapView into a Bitmap and then using an ImageView for your transition. Once the transition completes, you can replace the ImageView with the MapView.

    • Yes! The easiest way is to use finish() instead of finishAfterTransition(). You can also continue to do the return/reenter transition by removing the shared element in the onMapSharedElements callback. Then no shared elements will be transitioned, and all of the views will transition normally.

  2. Hello george
    Nice tutorial.

    My only problem is that I try some simple transition between activities (code way more simpler than yours because it is basically just:

    Intent intent = new Intent(this, Launched.class);
    ActivityOptions options = ActivityOptions
    .makeSceneTransitionAnimation(this, view, “hello”);
    startActivity(intent, options.toBundle());
    }

    and the xml definition.

    Problem is that all these transitions are not circle but rectangular.

    Where should I specify the system to use a circle?

    • The RevealTransition in this example does the circle animation. The simplest way to specify the transition is to update the theme XML’s android:windowSharedElementEnterTransition and android:windowSharedElementExitTransition. Look in the res/values-21/styles.xml folder in the example. The specific transitions are in the res/transition folder.

  3. You can correct me if I’m wrong… but calling “findViewById(R.id.planter).setVisibility(View.INVISIBLE);” inside onMapSharedElements() sounds strange to me. Nothing in the documentation implies that this callback method is a place where you should be modifying the state and/or appearance of the shared element views. For example, it seems like onSharedElementStart() or TransitionListener#onTransitionStart() instead would be the perfect place to set the planter’s initial visibility. Is there a “more correct” alternative or am I mistaken?

  4. I have a general question about “shared element snapshots”. It seems like you haven’t done any additional work to create the snapshot views in your sample project, so I assume that they are created by the framework by default? Is there a reason that this behavior is enabled by default? For example, does the framework ever make use of them without the developer knowing about it, or will the snapshots only ever be used if the developer explicitly chooses to do so?

  5. Another “shared element snapshot” question I have has to do with how they can be used inside the SharedElementCallback methods (such as onSharedElementStart() and onSharedElementEnd()).

    In your example project, you make use of the snapshot by setting the shared element to be a FrameLayout and then adding it to that initially empty FrameLayout in onSharedElementStart(). However, I am wondering if there is any other way to make use of this shared element snapshot View that doesn’t require adding an extra (and otherwise useless) FrameLayout into the view hierarchy.

    For example, let’s say you wanted to implement a standard shared element transition that transitions a low-resolution ImageView in the calling Activity to a high-resolution ImageView in the called Activity. The high-resolution ImageView might take a while to load, so you decide to transition a snapshot of the low-resolution ImageView instead of postponing the transition until the high-resolution ImageView has finished being processed. From what I can tell, this would require removing the high-resolution ImageView from the view hierarchy in the called Activity’s onSharedElementStart() method, and then replacing it with the corresponding snapshot View provided in the List sharedElementSnapshots argument. If this is correct, how then would you go about restoring the view hierarchy to its initial state once the Transition has completed?

  6. This is my last comment for today, I promise. 🙂

    The more I think about snapshots, the more I wonder whether snapshots really need to be represented as Views in the first place. In this blog post you make use of the ImageView snapshot by retrieving it from the sharedElementSnapshots list and directly adding it to the initially empty FrameLayout. However, in your “Reveal Challenge” blog post the snapshot View that is returned from onCreateSnapshotView() is unnecessary… all you really needed to remember was the parcelable bitmap passed as an argument. The extra View that needed to be created in this case was pointless… it’s only purpose was to satisfy the SharedElementCallback API which requires you to create and return a View instead.

    Do you have any thoughts on why this was implemented the way it was? Maybe I’m just missing something…

    • This is a bit of future-proofing. I’d like to make the snapshots more efficiently shared from the calling activity to the called activity. If I returned bitmaps always then the developers wouldn’t be able to take advantage of, say, a read-only display list copy.

    • I’m not sure I understand the question. In this example, the shared element is the item being revealed. Do you want to change the center of the circle? Are you trying to reveal the entire activity?

  7. You’re using ‘onSharedElementEnd’ as if it is something that executes after the shared element has moved to its end position, but in my own observation, it is not called at the end of the transition. It seems to be invoked via onPreDraw listener, which in my situation is happening apparently at the start of the transition. Is this expected?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s