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!

Advertisements

What are all these dang Transitions?

I’ve seen a few questions about the Transitions used in Activity Transitions and Fragment Transitions and I thought I’d clear up their meanings.

In Activity Transitions, some Transitions are executed in the calling Activity and some are executed in the called Activity. Let’s see the simplest case and describe what is going on.

I’ve slowed down all animations on my device by 5x so that you can see what’s going on easier. There are two Activities, one with “Hello world” and one showing “Goodbye, cruel world!” One of the “Hello world” TextViews is shared to the Goodbye Activity. This app uses the default Material Theme — what is going on?

In the calling Activity, no Transition is being run. What?!? That can’t be. The Views from hello are clearly being faded out. What is happening is that the goodbye Activity is being brought on top of the hello Activity. Its background is being faded in on top of hello Activity and it appears as if the hello Activity is being faded out. You can adjust the duration of this fade by calling:

getWindow().setTransitionBackgroundFadeDuration(millis);

or in the window style:

<item name="windowTransitionBackgroundFadeDuration">500</item>

These should be called on the called (goodbye) Activity’s Window.

In the called Activity (goodbye), the “Goodbye, cruel world!” Views are faded in. This Transition is the Enter Transition and Material defaults this to Fade.

The shared element TextView “Hello, world!” is being moved using the shared element enter transition. In Material themes, this defaults to a combination of ChangeBounds, ChangeTransform, ChangeImageTransform, and ChangeClipBounds. These are generally a good starting point for most shared elements, moving and resizing the shared element.

Now, when going back, we want to play the transitions backwards. The enter transition is replaced with the return transition. The shared element enter transition is replaced with the shared element return transition. By default, these will just use the enter versions. In many cases, this is fine, but you may need to adjust the timing between entering and returning — either offsetting start delays or changing the interpolator to accelerate instead of decelerate or similar.

When going back, the background of the goodbye Activity fades out while the “Goodbye, cruel world!” Views Fade, following the return transition. The “Hello, world!” View moves to the position in the hello Activity following the shared element return transition.

So far, we’ve covered:

  • shared element enter
  • enter
  • shared element return
  • return

Let’s work on exit/reenter Transitions next:

Here, you can see that the “Hello, world” TextViews as well as the Action Bar move out of the hello Activity with an Explode Transition. The epicenter of the explosion is the shared element, the top-left “Hello, world” text. On the way back, I chose to use a different transition, Slide, to bring the Views back into the Activity. In my theme, I made these two additions:

<style name="AppTheme" parent="android:Theme.Material.Light">
  <item name="android:windowExitTransition">@android:transition/explode</item>
  <item name="android:windowReenterTransition">@android:transition/slide_bottom</item>
</style>

The exit transition is used to transition Views out of the calling Activity during the transition. The return transition is used to transition Views back into the Activity. These are used in the calling Activity.

The final transitions, shared element exit/return, are a little different. These are used in the calling Activity to execute a Transition before the shared element is transferred. Let’s make the TextView lift up off the hello Activity, before moving to the new goodbye Activity. Then the goodbye Activity will drop it when it reaches its destination.

I’ve made the background of the shared element “Hello, world” white so that you can see it a little better and distinguish the shadow. Setting a background on a View means that I get a shadow when I change the translation Z.

I’ve updated my transition definitions in the theme to use some custom transitions:

<style name="AppTheme" parent="android:Theme.Material.Light">
 <item name="android:windowExitTransition">@android:transition/explode</item>
 <item name="android:windowSharedElementExitTransition">@transition/shared_element_exit</item>
 <item name="android:windowSharedElementEnterTransition">@transition/shared_element_enter</item>
 <item name="android:windowSharedElementReenterTransition">@transition/shared_element_enter</item>
 <item name="android:windowSharedElementReturnTransition">@transition/shared_element_exit</item>
 <item name="android:windowReenterTransition">@android:transition/slide_bottom</item>
</style>

And I now have two new transitions, shared_element_enter:

<?xml version="1.0" encoding="utf-8"?>
<transitionSet
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:transitionOrdering="sequential">
 <changeBounds/>
 <transition class="com.sample.simpletransitions.ShadowTransition"
             android:duration="150"/>
</transitionSet>

and shared_element_exit:

<?xml version="1.0" encoding="utf-8"?>
<transitionSet
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:transitionOrdering="sequential">
 <transition class="com.sample.simpletransitions.ShadowTransition"
             android:duration="150"/>
 <changeBounds/>
</transitionSet>

Note the timing. I want the lift to happen first, then move, then drop.

I have a very simple custom Transition for translation z. You’ll see it in the project.

Now, when I launch the goodbye Activity, I have to call setTranslationZ to trigger the shared element exit Transition:

startActivity(intent, options.toBundle());
hello.setTranslationZ(16);

Now, on the way back, there is nothing telling the system to put the shared element back to zero elevation, so I need to force that. I chose to use the SharedElementCallback. You could also use a TransitionListener on the share element reenter Transition.

setExitSharedElementCallback(new SharedElementCallback() {
  @Override
  public void onSharedElementEnd(List<String> sharedElementNames,
      List<View> sharedElements, List<View> sharedElementSnapshots) {
    findViewById(R.id.hello).setTranslationZ(0);
  }
});

Now we have an active shared element exit transition.

In summary, these are called in the calling Activity (e.g. hello):

  • exit transition – removes Views from the calling Activity (e.g. explode)
  • reenter transition – moves Views back into the calling Activity when returning (e.g. slide). Defaults to exit transition.
  • shared element exit transition – additional Transition to execute before transferring the shared element to the called Activity. Does not have to be on the shared element! In my example, this lifts the shared element off the hello Activity.
  • shared element reenter transition – when coming back, this is used after the shared element has been transferred back to the calling Activity. In my example, this drops the shared element back into the hello Activity. Defaults to shared element exit transition.

In the called Activity (e.g. goodbye):

  • enter transition – moves Views into the called Activity (e.g. fade)
  • return transition – moves Views out of the called Activity when going back. Defaults to the enter transition.
  • shared element enter transition – moves shared elements from the location/size in the calling Activity to the final location/size.
  • shared element return transition – when going back, moves the shared elements from the location/size in the called Activity to the location/size in the calling Activity. Defaults to shared element enter transition.

All of these transitions are the same in Fragment Transitions except that there are no shared element exit and shared element return transitions. Fragment Transitions work using the FragmentTransaction. You remove a Fragment, then add a Fragment, and the Transitions are activated. If you remove a Fragment, it sure is difficult to do some manipulation on it like you would in the Activity Transition. Instead, you’re going to have to do your manipulations before starting the FragmentTransaction.

Here is the code I used in the examples.

 

 

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.