Two-Way Data Binding Conversions

I’ve seen a few questions regarding using Android Data Binding with EditText, but with a different type. For example, you may want the EditText to bind to an integer value:

<EditText android:text="@={user.age}" ../>

or float value:

<EditText android:text="@={user.favoriteRationalNumber}" ../>

or maybe even a custom object.

With Android Studio 2.2’s gradle plugin, you can use a simplified conversion form:

<EditText android:text="@={`` + user.age}" ../>

This converts to and from a String from any primitive type.

However, this doesn’t work on Android Studio 2.1, nor does it work for any custom type, so you must implement it yourself. Here’s how you can do it for a float value:

@BindingAdapter("android:text")
public static void setText(TextView view, float value) {
    boolean setValue = view.getText().length() == 0;
    try {
        if (!setValue) {
            setValue = Float.parseFloat(view.getText().toString()) != value;
        }
    } catch (NumberFormatException e) {
    }
    if (setValue) {
        view.setText(String.valueOf(value));
    }
}

@InverseBindingAdapter(attribute = "android:text")
public static float getText(TextView view) {
    try {
        return Float.parseFloat(view.getText().toString());
    } catch (NumberFormatException e) {
        return 0;
    }
}

In this, you’ll have erroneous float format for text return 0. You may want to keep track of the old value and return it if there is an error. If you do, remember to use a weak reference, such as a WeakHashMap so that you don’t end up leaking View references everywhere.

2-way Data Binding on Android!

Released with Android Studio 2.1 Preview 3, Android Data Binding now has 2-way data binding.

Data Binding Quick Recap

For a short recap on data binding, you can now add expressions to the layout files to reference variables in your data model. For example:

<layout ...>
  <data>
    <variable type="com.example.myapp.User" name="user"/>
  </data>
  <RelativeLayout ...>
    <TextView android:text="@{user.firstName}" .../>
  </RelativeLayout>
</layout>

The above binds the user’s first name to the TextView’s text field. The UI is updated whenever the user’s data has changed.

Two-Way Data Binding

Android isn’t immune to typical data entry and it is often important to reflect changes from the user’s input back into the model. For example, if the above data were in a contact form, it would be nice to have the edited text pushed back into the model without having to pull the data from the EditText. Here’s how you do it:

<layout ...>
  <data>
    <variable type="com.example.myapp.User" name="user"/>
  </data>
  <RelativeLayout ...>
    <EditText android:text="@={user.firstName}" .../>
  </RelativeLayout>
</layout>

Pretty nifty, eh? The only difference here is that the expression is marked with “@={}” instead of “@{}”. It is expected that most data binding will continue to be one-way and we don’t want to have all those listeners created and watching for changes that will never happen.

Implicit Attribute Listeners

You can also reference attributes on other Views:

<layout ...>
  <data>
    <import type="android.view.View"/>
  </data>
  <RelativeLayout ...>
    <CheckBox android:id="@+id/seeAds" .../>
    <ImageView android:visibility="@{seeAds.checked ? View.VISIBLE : View.GONE}" .../>
  </RelativeLayout>
</layout>

In the above, whenever the checked state of CheckBox changes, the ImageView’s visibility will change. No need to attach a listener on your own! This kind of expression only works with attributes that support 2-way data binding and those that have binding expressions.

Enabling Two-Way Data Binding

So, what else do you need? First, you must be sure to be using the 2.1-alpha3 or above version of the android gradle plugin in your project’s build.gradle:

classpath 'com.android.tools.build:gradle:2.1.0-alpha3'

And, of course, you need to enable data binding in your module’s android section of the build.gradle:

android {
    ...
    dataBinding.enabled = true
}

Catch?

So, what’s the catch? Well, the main one is that only a few attributes are supported. This is because there aren’t listeners to allow the data binding framework to know when something has changed. The good news is that these are probably the attributes you care most about:

  • AbsListView android:selectedItemPosition
  • CalendarView android:date
  • CompoundButton android:checked
  • DatePicker android:year, android:month, android:day (yes, these are synthetic, but we had a listener, so we thought you’d want to use them)
  • NumberPicker android:value
  • RadioGroup android:checkedButton
  • RatingBar android:rating
  • SeekBar android:progress
  • TabHost android:currentTab (you probably don’t care, but we had the listener)
  • TextView android:text
  • TimePicker android:hour, android:minute (again, synthetic, but we had the listener)

You’re also going to start getting warnings now if your variable names collide with the View id’s in your layout. How do we know whether you mean the View or the variable in your expressions?

Rolling Your Own

Let’s imagine that the attribute you care about isn’t one of those listed above. How do you go about making your own? Let’s imagine that you have a color picker View where you want to have two-way binding for the color choice.

public class ColorPicker extends View {
    public void setColor(int color) { /* ... */ }
    public int getColor() { /* ... */ }
    public void setOnColorChangeListener(OnColorChangeListener listener) { 
        /*...*/
    }
    public interface OnColorChangeListener {
        void onColorChange(ColorPicker view, int color);
    }
}

The important aspect of the above View is that it has a listener that the data binding framework can listen for. Now we need to tell data binding about the attribute. The simplest way is using an InverseBindingMethod attribute on any class:

@InverseBindingMethods({
  @InverseBindingMethod(type = ColorPicker.class, attribute = "color"),
})

In this case, the name of the getter matches the name of the attribute “getColor” for “app:color.” If the name is something different, you can supply a method attribute to correct that. This creates a synthetic attribute to be used when binding the event using the name of the attribute with the suffix “AttrChanged.” Let’s see how this is used to set up the listener:

@BindingAdapter("colorAttrChanged")
public static void setColorListener(ColorPicker view,
        final InverseBindingListener colorChange) {
    if (colorChange == null) {
        view.setOnColorChangeListener(null);
    } else {
        view.setOnColorChangeListener(new OnColorChangeListener() {
            @Override
            public void onColorChange(ColorView view, int color) {
                colorChange.onChange();
            }
        });
    }
}

That’s the simplest kind of BindingAdapter — it converts between the color change listener type and the one used by the data binding framework, InverseBindingListener. However, you’re often going to want to also support binding to the colorChange event as well, right? You’re going to have to combine listeners:

@BindingAdapter(value = {"onColorChange", "colorAttrChanged"}, 
                requireAll = false)
public static void setColorListener(ColorPicker view,
        final OnColorChangeListener listener,
        final InverseBindingListener colorChange) {
    if (colorChange == null) {
        view.setOnColorChangeListener(listener);
    } else {
        view.setOnColorChangeListener(new OnColorChangeListener() {
            @Override
            public void onColorChange(ColorView view, int color) {
                if (listener != null) {
                    listener.onColorChange(view, color);
                }
                colorChange.onChange();
            }
        });
    }
}

Now you can use two-way data binding and bind to the onColorChange event, even in the same View.

InverseBindingAdapters

Similar to BindingAdapters and setters, there are times when you have to do something other than just calling a getter to retrieve a value. For example, if our ColorPicker has an enumeration of only a few colors, we may need to convert to int:

@InverseBindingAdapter(attribute = "color")
public static int getColorInt(ColorPicker view) {
    return ConvertColorEnumToInt(view.getColor());
}

Preventing Loops

One problem we encounter with two-way data binding is a sort of infinite loop. When making a change to a value raises an event, the listener will set the value on the target object. This can raise another event and it may trigger a change to the View again and we’re off on our loop. This kind of loop is normally caught either in the View’s setter or the data value’s setter, but this can’t be guaranteed if you don’t have control over the View or the data value. We’ve decided to prevent loops through BindingAdapters. For our ColorPicker example:

@BindingAdapter("color")
public static void setColor(ColorPicker view, int color) {
    if (color != view.getColor()) {
        view.setColor(color);
    }
}

There! No more loop.

I hope you get some use out of two-way data binding and let me know if you have any issues.

Next time I’ll talk about event lambda expressions that we released as well.

 

DatePicker Data Binding

We’re very close to releasing the version 1.0 of data binding (we’re up to 1.0-rc4). There have been very few bug trickling in, so we know it is getting to be rock solid. I thought I’d talk about some of the features and how to implement some enhancements you might want to add to data binding for your application.

As you know, you can bind to almost all View attributes with an expression. For example, you can bind to a user’s name with the expression:

<TextView android:text="@{user.name}" .../>

You can even get callbacks for events you can set on the View. For example, if you want the user to be able to edit the name, you may use:

<EditText android:text="@{user.name}"
          android:afterTextChanged="@{handlers.nameChanged}" .../>

That said, DatePicker doesn’t have anything special associated with it. There’s no android:date or android:year property. You don’t even get them for free as automatic setters because there isn’t a setDate() or setYear() method, just an updateDate() method. Likewise, because there is no setter for the OnDateChanged listener, there is no android:onDateChanged support. It sure would be nice to have a date picker support this functionality.

Ideally, we could implement it in a single method:

@BindingAdapter({"android:year","android:month", "android:day",
                 "android:onDateChanged"})
public static void setDate(DatePicker view, int year, int month,
                           int day, OnDateChanged listener) {
    view.init(year, month, day, listener);
}

The above BindingAdapter will work when all of the attributes are supplied. If you want to have a BindingAdapter work when only some of the attributes are supplied, you should supply requireAll = false for the BindingAdapter annotation. When you do that, and the attribute doesn’t exist, the default value for type is supplied (0 for int, null for Objects, etc):

@BindingAdapter(value = {"android:year", "android:month",
                         "android:day", "android:onDateChanged"},
                required = false)
public static void setDate(DatePicker view, int year, int month,
                           int day, OnDateChanged listener) {
    if (year == 0) year = view.getYear();
    if (month == 0) month = view.getMonth();
    if (day == 0) day = view.getDayOfMonth();
    view.init(year, month, day, listener);
}

That doesn’t look hard! But wait a second, month = 0 is January! This won’t work at all. If the month expression is set January, the month will use the current value. We’re fortunate that year = 0 is not legal (in Gregorian Calendar, we traverse from 1BC to 1AD) and day = 0 is not legal as we count from the first day of the month.

At this point, I wish that I had implemented “these attributes are required, but not these” for data binding. But I didn’t, so we have to have several BindingAdapters to cover all cases:

  • year, month, day, onDateChanged
  • year, month, day
  • year, month, onDateChanged
  • year, month
  • month, day, onDateChanged
  • month, day
  • year, day, onDateChanged; requireAll = false

The final one is special. Since it doesn’t have a month, we can compare the values to the default values (0, null) and know whether to set them or use the default.

Take a look and see how DatePicker can be data bound with this BindingAdapter. Perhaps it will inspire you to add some cool BindingAdapters. I hope we’ll get lots of requests for BindingAdapters like these for the attributes that you use and would like to be built by default.

https://github.com/georgemount/DatePicker

Postponing Fragment Transitions

Last week at Google I/O, I finally got to talk about what I have been working on for the last 6 months. You should try out data binding:

http://developer.android.com/tools/data-binding/guide.html

It is a super-fast code-generating data binding system that optimized to work well with Android. We’re looking for feedback, so try it out and let us know what you think.

That said, I’ve had a couple of posts in the back of my mind that I haven’t been able to get to. One bit of feedback that I had heard about Fragment Transitions is that we don’t get the postponeEnterTransition() that we get with Activity Transitions. That is especially important when one or both fragments have a RecyclerView.

When I wrote Fragment Transitions, I ran across the problem that FragmentTransaction is a Transaction; one fragment should be removed and the other should be added in an atomic step. This left the developer to handle the postponeEnterTransition.

In the situation where Fragment A is replaced by B, my recommendation was this:

  1. Transaction 1
    • add B,
    • hide B
  2. Transition 2: when B is ready
    • show B
    • remove A
    • add shared elements
    • add to back stack

That works great when B needs to be postponed. If, for example, A has a recycler view, the popBackStack() will immediate remove B and add A. Since A has a recycler view, the contents aren’t ready and the shared elements won’t be ready; you’ll lose the shared element transition.

This can be alleviated by only hiding fragment A.

  1. Transaction 1:
    • add B
    • hide B
    • add to back stack
  2. Transaction 2: when B is ready
    • show B
    • hide A
    • add shared elements
    • add to back stack

In this scheme, there is an intermediate state that you add at which B is added and hidden, When you pop the back stack, you will end up in the intermediate state, so you’ll have to automatically pop the back stack to remove B when you get there. If you don’t want that intermediate state, you might be able to avoid it. I didn’t try it, but you would have to detect when you’ve popped back from Transaction 2 and just remove B instead of popping the back stack.

Now the hidden fragment, A, will not lose its view hierarchy when it is hidden. Thus, the view is ready when going back. However, if you do an orientation change, the view hierarchy will be rebuilt. This is fine if your application is ready, even when the view is gone, but if your view needs layout (like recycler view), this isn’t enough. You may be able to trigger the system into laying out the hidden fragment A, but I don’t know the solution to that right off. The other issue is, of course, memory usage. While A is hidden and won’t take part in layout, it does consume resources. If your back stack is particularly large, this could become a noticeable pressure on the heap.

So, to support getting fragment A ready when going back, we need another stage in the transactions:

  1. Transaction 1:
    • add B
    • hide B
    • add to back stack
  2. Transaction 2: when B is ready
    • show B
    • hide A
    • add shared elements
    • add to back stack
  3. Transaction 3:
    • show A
    • remove A
    • add to back stack

In this scheme, there are two intermediate states. The first one has Fragment B hidden and Fragment A showing. Fragment B can hang out until is ready to show. On the way back, that intermediate state is not really necessary and should be popped automatically. The second intermediate state shows B and hides A. When pushing up the stack, it can immediately proceed to the final state, but when popping, it needs to pause there until Fragment A is ready to show.

I made a utility to handle the 3 transitions as a unit and watch the back stack to detect when we’ve arrived in an intermediate state and proceed when the fragment is ready to transition (both ways). In my utility, I’ve made an interface for the Fragment to implement:

public interface DelayedTransitionFragment {
    boolean isTransitionDelayed();
}

If the fragment does not implement the interface, it won’t support delayed transitions. If isTransitionDelayed() returns true, then the fragment will be responsible for calling transitionReady() on the utility.

In my example, I had each fragment contain a RecyclerView that needed to be laid out prior to the transition continuing. Instead of being hidden (Visibility.GONE), the fragment View needed to be invisible. Once it was laid out, I continued the transition:

final RecyclerView recyclerView = (RecyclerView) getView();
recyclerView.setVisibility(View.INVISIBLE); // force a layout
recyclerView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
    @Override
    public boolean onPreDraw() {
        recyclerView.getViewTreeObserver()
            .removeOnPreDrawListener(this);
        FragmentTransitionUtil.getInstance(getFragmentManager())
            .transitionReady();
        return false;
    }
});

In trying this out, I found a few bugs in Fragment Transitions that I needed to work around. Obviously, I’ll fix those, but you’ll need to work around them until the bugs are fixed. In the fragment, I need to reset the enter and exit transitions onHiddenChanged and explicitly exclude the shared elements. My utility only provides for a single shared element, but you can expand it to support multiple. Again, this is only because of a bug; future versions shouldn’t require this hack.

@Override
public void onHiddenChanged(boolean hidden) {
    super.onHiddenChanged(hidden);
    resetTransitions();
}

private void resetTransitions() {
    // Work around bug in fragment transitions in 22.2.0
    // We need to reset the transition after the view is created
    String sharedElementName =
            FragmentTransitionUtil.getInstance(getFragmentManager())
                .getTransitionName();
    Slide enter = new Slide(Gravity.LEFT);
    Slide exit = new Slide(Gravity.RIGHT);
    if (sharedElementName != null) {
        enter.excludeTarget(sharedElementName, true);
        exit.excludeTarget(sharedElementName, true);
    }
    setEnterTransition(enter);
    setExitTransition(exit);
}

My sample project uses minimal data binding, so if you aren’t trying out the beta Android Studio, you’ll have to do just a little jiggering to get the sample to work.

Take a look at this project and let me know what you think:

https://drive.google.com/file/d/0BzzMYfCAYzn4bHpqWGhKbV81cU0/view?usp=sharing

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!

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.

 

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.