Friday, September 16, 2011

Old Views Don't Die; They Just Fade Away

One of the app developers here on Android asked me about the best way to animate adding and removing items from a UI. Specifically, he wanted to fade items in and out as they became visible/invisible.

So I wrote up a sample activity that used ViewPropertyAnimator, showing how to set the visibility at the right time (making it visible before fading it in, listening for the onAnimationEnd() to set it invisible after fading it out). Pretty straightforward, but if you haven't played around a lot with the new animation classes yet (WHY HAVEN'T YOU?!?!), it's probably not obvious:

To make it invisible:
invisibleButton.animate().alpha(0f).setListener(new AnimatorListenerAdapter() {
                    public void onAnimationEnd(Animator animation) {
                        invisibleButton.setVisibility(View.INVISIBLE);
                        invisibleButton.setAlpha(1f);
                        invisibleButton.animate().setListener(null);
                    }
                });
To make it visible again:
invisibleButton.setAlpha(0);
                    invisibleButton.setVisibility(View.VISIBLE);
                    invisibleButton.animate().alpha(1);

I sent the sample application along to the developer.

Then I thought I'd add to that sample and show how to also add/remove views, or set them to View.GONE as well as View.INVISIBLE.

I sent that updated sample to the developer as well.

Then I thought I might as well show how you'd do the same thing with ObjectAnimator. It's a little more code than ViewPropertyAnimator, but still pretty straightforward. For example, fading the object out and making it invisible looks like this:
                ObjectAnimator anim = ObjectAnimator.ofFloat(invisibleButton1, "alpha",0);
                anim.addListener(new AnimatorListenerAdapter() {
                    public void onAnimationEnd(Animator animation) {
                        invisibleButton1.setAlpha(1);
                        invisibleButton1.setVisibility(View.INVISIBLE);
                    }
                });
                anim.start();

I sent this further updated sample to the developer.

Then I thought I'd poke at a utility class that's been on my mind for a while. We have all of these new animation capabilities as of the Honeycomb release, but I'd still like it to be simpler to run these kinds of animations, especially ones that involve several actions like this: fade this view out, then remove it. So I wrote up a Fade class that has utility methods in it for fading/adding/removing/etc. I enhanced the sample to use the new Fade utilities. Now making a view invisible is just one step:
                fade.hide(invisibleButton2, View.INVISIBLE);
Similarly, making that view visible again is a single call:
                fade.show(invisibleButton2);

I sent this latest version of the sample to the developer. He was getting pretty tired of hearing from me by this time.

Then I tweaked the Fade class to have a duration property.

I was going to send this final (ha!) update to the developer, but I didn't want him to call security on me. I think he got what he needed the first time around. So rather than continue to bury him in yet more ways to accomplish this simple task, I thought I'd publish it here.

Check out the sample code for FaderActivity, which shows all of these things: ViewPropertyAnimator, ObjectAnimator, and this new Fade utility class. I hope that something like the Fade class and other higher-level animation classes will make it into the SDK eventually, but in the meantime, Fade should simplify fading tasks.

There are a couple of things to note about fading animations. One thing is that there is an abrupt 'pop' when an item is removed from or added to a layout that is affected by that change. For example, the LinearLayout used in the example expands or contracts when the first button is removed or added or when the last button is set to VISIBLE or GONE (although you can't see that change since it's the last item in that layout). There's nothing to be done about this problem right now, although you might play with the LayoutTransition class available in 3.0, which animates the layout changes as well.

It's also worth noting that the Fade class is great at fading things out from their current alpha and then back to an alpha value of 1 (fully opaque). It does not compensate for in-between alpha values that your views might want to persist between fades. That logic could be added, but there's some tedious logic around knowing when an in-between value is coming from the view itself vs. some other fade animation that happens to be running when you start the new one (for example, you fade an item out and then, halfway through, you fade it back in). The Fade class is great for the common case where views are typically just opaque (alpha == 1). But it seemed worth mentioning.

You can grab a zipped version of the Eclipse project with the source for the example activity and the utility Fade class here.

Enjoy.