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.

 

Reveal Transition

Android Lollipop introduced a really great new Animator, the circular reveal animator. To use it, we simply provide the center point of the circle as well as its start and end radii:

int w = view.getWidth();
int h = view.getHeight();
float maxRadius = (float) Math.sqrt(w * w / 4 + h * h / 4);
Animator reveal = ViewAnimationUtils.createCircularReveal(view,
    w / 2, h / 2, 0, maxRadius);
reveal.start();

Now you get a cool reveal in which the view is exposed from the center of the View to the edge.

That’s fun, but many of us are using Transitions to automate this kind of reveal. Normally, we use the Fade Transition, but perhaps we want some of our views to come in with a reveal animation instead. That doesn’t sound too hard! But there are a a couple of tricks to watch out for.

First, let’s create the basic RevealTransition as a subclass of Visibility. This gives us all the niceness that Visibility does for us, especially keeping a View in the Overlay when it has been removed from the layout:

public class RevealTransition extends Visibility { 
    public RevealTransition() {
    }

    public RevealTransition(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public Animator onAppear(ViewGroup sceneRoot, final View view,
            TransitionValues startValues,
            TransitionValues endValues){
        float radius = calculateMaxRadius(view);
        final float originalAlpha = view.getAlpha();
        view.setAlpha(0f);

        Animator reveal = createAnimator(view, 0, radius);
        reveal.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                view.setAlpha(originalAlpha);
            }
        });
        return reveal;
    }

    @Override
    public Animator onDisappear(ViewGroup sceneRoot, View view,
            TransitionValues startValues,
            TransitionValues endValues) {
        float radius = calculateMaxRadius(view);
        return createAnimator(view, radius, 0);
    }

    private Animator createAnimator(View view, float startRadius,
            float endRadius) {
        int centerX = view.getWidth() / 2;
        int centerY = view.getHeight() / 2;

        Animator reveal = ViewAnimationUtils.createCircularReveal(
            view, centerX, centerY, startRadius, endRadius);
        return reveal;
    }

    static float calculateMaxRadius(View view) {
        float widthSquared = view.getWidth() * view.getWidth();
        float heightSquared = view.getHeight() * view.getHeight();
        float radius = (float) Math.sqrt(widthSquared +
            heightSquared) / 2;
        return radius;
    }
}

This is a great start at a RevealTransition. You’ll notice a few tricks. I’ve added two constructors. The nullary constructor helps you create a RevealTransition in code. The constructor taking a Context and an AttributeSet allow you to load the transition from XML:

<transition class="my.sample.RevealTransition"
  android:duration="500"
  android:transitionVisibilityMode="mode_out"/>

The second trick is that onAppear hides the view at the start. If you have the RevealTransition follow another transition, its initial state should be invisible. It can use Visibility, but alpha has fewer side-effects, such as focus changes. onDisappear does it for you. Hmmm… should have probably had onAppear do it for you also, eh?

That works great, but if your RevealTransition is interrupted, the animation will always start at the beginning. Sadly, there is no way to capture the current state in the RevealAnimator. It runs on the render thread, so anything you do would be at best a guess. I expect this to be fixed in a future release, but for now, you get a jump cut when you interrupt the transition.

If you play with this and try the transition interruption, you’ll see an OperationNotSupportedException when the transition tries to pause the animator. Yikes! We can fix this by wrapping the Animator:

private static class NoPauseAnimator extends Animator {
    private final Animator mAnimator;
    private final ArrayMap<AnimatorListener, AnimatorListener>
        mListeners = new ArrayMap<AnimatorListener,
            AnimatorListener>();

    public NoPauseAnimator(Animator animator) {
       mAnimator = animator;
    }

    @Override
    public void addListener(AnimatorListener listener) {
        AnimatorListener wrapper = new AnimatorListenerWrapper(this,
            listener);
        if (!mListeners.containsKey(listener)) {
            mListeners.put(listener, wrapper);
            mAnimator.addListener(wrapper);
        }
    }

    @Override
    public void cancel() {
       mAnimator.cancel();
    }

    @Override
    public void end() {
        mAnimator.end();
    }

    @Override
    public long getDuration() {
        return mAnimator.getDuration();
    }

    @Override
    public TimeInterpolator getInterpolator() {
       return mAnimator.getInterpolator();
    }

    @Override
    public ArrayList<AnimatorListener> getListeners() {
        return new ArrayList<AnimatorListener>(mListeners.keySet());
    }

    @Override
    public long getStartDelay() {
        return mAnimator.getStartDelay();
    }

    @Override
    public boolean isPaused() {
        return mAnimator.isPaused();
    }

    @Override
    public boolean isRunning() {
        return mAnimator.isRunning();
    }

    @Override
    public boolean isStarted() {
        return mAnimator.isStarted();
    }

    @Override
    public void removeAllListeners() {
        super.removeAllListeners();
        mListeners.clear();
        mAnimator.removeAllListeners();
    }

    @Override
    public void removeListener(AnimatorListener listener) {
        AnimatorListener wrapper = mListeners.get(listener);
        if (wrapper != null) {
            mListeners.remove(listener);
            mAnimator.removeListener(wrapper);
        }
    }

    /* We don't want to override pause or resume methods
     * because we don't want them to affect mAnimator.
    public void pause();
    public void resume();
    public void addPauseListener(AnimatorPauseListener listener);
    public void removePauseListener(AnimatorPauseListener listener);
     */

    @Override
    public Animator setDuration(long durationMS) {
        mAnimator.setDuration(durationMS);
        return this;
    }

    @Override
    public void setInterpolator(TimeInterpolator timeInterpolator) {
        mAnimator.setInterpolator(timeInterpolator);
    }

    @Override
    public void setStartDelay(long delayMS) {
        mAnimator.setStartDelay(delayMS);
    }

    @Override
    public void setTarget(Object target) {
        mAnimator.setTarget(target);
    }

    @Override
    public void setupEndValues() {
        mAnimator.setupEndValues();
    }

    @Override
    public void setupStartValues() {
        mAnimator.setupStartValues();
    }

    @Override
    public void start() {
        mAnimator.start();
    }
 }

 private static class AnimatorListenerWrapper
        implements Animator.AnimatorListener {
    private final Animator mAnimator;
    private final Animator.AnimatorListener mListener;

    public AnimatorListenerWrapper(Animator animator,
            Animator.AnimatorListener listener) {
        mAnimator = animator;
        mListener = listener;
    }

    @Override
    public void onAnimationStart(Animator animator) {
        mListener.onAnimationStart(mAnimator);
    }

    @Override
    public void onAnimationEnd(Animator animator) {
        mListener.onAnimationEnd(mAnimator);
    }

    @Override
    public void onAnimationCancel(Animator animator) {
        mListener.onAnimationCancel(mAnimator);
    }

    @Override
    public void onAnimationRepeat(Animator animator) {
        mListener.onAnimationRepeat(mAnimator);
    }
}

Wow! That’s complicated. We not only need to keep the pause/resume from doing anything with the animator, but we also need to redirect the Animator parameters in the listener. Specifically, the TransitionManager has a list of currently-running animators and when the animator ends, it removes it from its internal list using the parameter in the onAnimationEnd(). Since the wrapped animator was the one that it added, the parameter that it receives must be the same as the one that it receives in the parameter.

That’s it! Enjoy your new RevealTransition and next time, I’ll talk about how to use it with shared elements in Activity Transitions.

Click here for the entire RevealTransition code

Event Planning

I recently had an interesting challenge and I thought I’d share my solution. I’d love to hear what you all think of it and how you’ve solved similar puzzles.

I work on Android and I have to write a notification system that will potentially notify many times each second. This seems like a simple thing to do: write a listener interface, add notifications to a list and then notify when an event happens:

public interface SomethingListener {
  void onSomething(SomethingNotifier sender, Object arg);
}

private ArrayList<SomethingListener> mListeners
    = new ArrayList<SomethingListener>();

public void addListener(SomethingListener listener) {
    if (!mListeners.contains(listener)) {
        mListeners.add(listener);
    }
}

public void removeListener(SomethingListener listener) {
    mListeners.remove(listener);
}

public void somethingHappened(Object arg) {
    for (SomethingListener listener : mListeners) {
        listener.onSomething(this, arg);
    }
}

easy! Let’s write our first listener:

SomethingListener listener = new SomethingListener() {
    @Override
    public void onSomething(SomethingNotifier source, Object arg) {
        source.removeListener(this);
    }
}

Oh no! This is a common pattern: removing a listener in the listener itself. During somethingHappened(), the listener is called, removes itself, which modifies mListeners. Unfortunately, somethingHappened() is in the middle of iterating over mListeners and an Exception will be thrown.

You may consider using the ArrayList index, but that just means that the following listener will be skipped — remember i++ in your for loop! Hacky solutions involving the size of the list are just begging for hard-to-track-down bugs. The first listener that removes a one listener and adds another will break assumptions.

On Android, we have a nice system for handling this type of problem:

private CopyOnWriteArrayList<SomethingListener> mListeners
    = new CopyOnWriteArrayList<SomethingListener>();

public void somethingHappened(Object arg) {
    for (SomethingListener listener : mListeners) {
        listener.onSomething(this, arg);
    }
}

This works great! Each time somethingHappened is called, the iterator from mListeners keeps a local copy list. Only in this case where a reentrant call is made to remove a listener will the list of listeners be modified. You may be notified on a listener that was removed, but that is considered safe.

However, there is a problem with this: memory allocation. In my case, notifications can be made potentially hundreds of times each second. This pattern uses a newly-generated Iterator object on every notification and, if the listeners are modified, can allocate a second list of listeners. I need to guarantee close to zero allocations on notification.

Think… think… thunk.

private ArrayList<SomethingListener> mListeners
    = new ArrayList<SomethingListener>();
private BitSet mRemoved = new BitSet();
private int mNotificationLevel;

public void add(SomethingListener listener) {
    int index = mListeners.lastIndexOf(listener);
    if (index < 0 || mRemoved.get(index)) {
        mListeners.add(listener);
    }
}

public void remove(SomethingListener listener) {
    if (mNotificationLevel == 0) {
        mListeners.remove(listener);
    } else {
        int index = mListeners.lastIndexOf(listener);
        if (index >= 0) {
            mRemoved.set(index);
        }
    }
}

public void somethingHappened(Object arg) {
    mNotificationLevel++;
    int size = mListeners.size();
    for (int i = 0; i < size; i++) {
        if (!mRemoved.get(i)) {
            mListeners.get(i).onSomething(this, arg);
        }
    }
    mNotificationLevel--;
    if (mNotificationLevel == 0) {
        // Now that notifications have been complete,
        // remove listeners.
        int numRemoved = 0;
        int i = -1;
        // previousSetBit is introduced in JDK 1.7
        while ((i = mRemoved.nextSetBit(i + 1)) >= 0) {
            mListeners.remove(i - numRemoved);
            numRemoved++;
        }
        mRemoved.clear();
    }
}

This is quite a bit more complex. Let’s break this up and see how it works.

The main difference is that this keeps track of removed listeners via a BitSet, mRemoved:

private BitSet mRemoved = new BitSet();

Along with this is an integer tracking the recursion of notifications. Recursion is common during notifications where a notification can lead to another notification. mNotificationLevel tracks the depth of the recursion:

private int mNotificationLevel;

When adding, we still need to check to see if the current listener exists before adding it to prevent duplicates:

public void add(SomethingListener listener) {
    int index = mListeners.lastIndexOf(listener);
    if (index < 0 || mRemoved.get(index)) {
        mListeners.add(listener);
    }
}

But now we must check the mRemoved BitSet as well. Here, we’re counting on the ordering in mListeners — the last listener will always be the active one. If there are multiple, the earlier ones will have been marked removed:

public void remove(SomethingListener listener) {
    if (mNotificationLevel == 0) {
        mListeners.remove(listener);
    } else {
        int index = mListeners.lastIndexOf(listener);
        if (index >= 0) {
            mRemoved.set(index);
        }
    }
}

In the remove, we must check the recursion and remove straight from the list if not currently notifying. If a notification is currently occurring, we must mark the listener as removed instead. It will be removed later in somethingHappened().

Now it is time to look at the meat in somethingHappened():

    mNotificationLevel++;
    int size = mListeners.size();
    for (int i = 0; i < size; i++) {
        if (!mRemoved.get(i)) {
            mListeners.get(i).onSomething(this, arg);
        }
    }
    mNotificationLevel--;

This is very similar to our first notification system. There are still a few differences. First, we’re tracking the recursion with mNotificationLevel. Second, we’re capturing the size of the listeners instead of capturing the list of listeners itself. This avoids the iterator allocation, but still captures which listeners are active at the time of notification. It doesn’t notify removed listeners, but I consider that an improvement.

If you were paying attention to the removeListener code, you saw that it doesn’t modify mListeners when notifications are happening. We must do that now:

    if (mNotificationLevel == 0) {
        // Now that notifications have been complete,
        // remove listeners.
        int numRemoved = 0;
        int i = -1;
        // previousSetBit is introduced in JDK 1.7
        while ((i = mRemoved.nextSetBit(i + 1)) >= 0) {
            mListeners.remove(i - numRemoved);
            numRemoved++;
        }
        mRemoved.clear();
    }

Only when all notifications are complete does it remove the listeners from the list. I would have preferred iterating backward and removing listeners from the tail end, but previousBitSet() isn’t available on Android yet — it is in JDK 1.7. C’est la vie.

All-in-all, I’m pretty happy with this solution. We’ve solved the problem of reentrant listeners and there are no new allocations on notification.

There are several improvements that can be made, including not pre-allocating the mRemoved BitSet and making the calls thread safe. This may be sufficient for your use or you may just slap a few “synchronized” on the methods to fix the thread safety. I hope you find it helpful.