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.

8 comments:

hazam said...

nice! sharing those sweet utility classes is of great help.

Have you thought about making a version of the same class that falls back to old View animations if we are not on Honeycomb (97% of the field AFAIK)?

How do you feel about animations built around a recurring self-scheduling handler? Is a reasonable hack or is just a mess?

And the biggest of all, will these animation work also on ICS?

Chet Haase said...

@hazam: No plans to port it back to the old animation system - it's just something quick I hacked together to show how it's done with the new stuff, not something I want to continually build on. I understand that 3.0 is prevalent out there yet, but that will change over time and the new animation system is the one to use when possible.

I don't know what you're talking about with the self-scheduling handler....

As far as ICS, that's the whole idea with a forward-compatible library like the Android SDK; we add APIs that we continue to support in future versions.

hazam said...

@chet thank you for your answer!

Let me just explain myself on the self-scheduling handler animation:

class Animator {

Handler handler = new Handler() {
handleMessage() {
View viewTarget;
//get view target
view.setTranslatX(/*update position*/);
}
};

startAnim() {
handler.postDelayed(r, FRAMERATE);
}}

do you endorse making animations like this or there are some sort of drawbacks, performance, side-effects etc??

I see that this is the way some of the older components are animated, like SlidingDrawer.

This way you can animate pretty much everything, right?
Thank you!

Chet Haase said...

@hazam: It depends on what release you're targeting. If this is for pre-3.0, then yes, using a Handler is one of the ways to animate properties that don't happen to be covered by the old Animation classes. (The other way is the trick of using the values generated by an Animation, as seen in the use of AlphaAnimation to animate the ProgressBar values - see ProgressBar for that neat hack).

But if you're on 3.0 and later (which you'd have to be if you're calling setTranslationX(), since that method only exists in 3.0+), then you're basically duplicating the work of the new Animator classes; this is exactly what that system does. It creates a handler, posts messages, and updates all running Animators every frame. This is less overhead that you doing it because there's only one Handler/message for the whole set of animators in the system. And it's a lot easier than you writing your own Handler loop.

hazam said...

Thank you for the great explanation Chet!

cat said...

Chet: Nice article! Is there a way to use the newer capabilities of ObjectAnimators (pause and resume which were introduced in API 19) in API levels below 19? I want to achieve the pause and resume ability on a ObjectAnimator set for "rotation" on a view.

I attempted to capture the getCurrentPlayTime() from the animator before cancelling the animator (on my pause animator function) and cloned/re-created the animator , set the saved currentPlayTime, set the target and started the animator (in my resume animator function).

The code snippet I used:

private void stopAnimation(){
currentTime = mFrameAnimator.getCurrentPlayTime();
mFrameAnimator.cancel();
}

private void startAnimation(Context context, View view, float startAngle) {
ObjectAnimator frameAnimatorClone = (ObjectAnimator)AnimatorInflater.loadAnimator(context, R.animator.rotate_frame);
frameAnimatorClone.setTarget(mFrameLayout);
frameAnimatorClone.setCurrentPlayTime(currentTime);
frameAnimatorClone.start();
}

But this did not work! Any pointers would help.

Unknown said...

Hey,
Chet Hasse,
Sorry i can find any other way to contact you. In View class can you please tell the difference between animate and animateBy?

Also is there any way we can animate the bitmap instead of view? Yes i know there is no setAlpha exposed in the api of bitmap and also the bitmap is immutable class i.e we can't change the properties once draw, Is it possible to use reflection to achieve that?

I thought this would have work, but its not :(

BitmapDrawable bmpDrawable = new BitmapDrawable(context.getResources(), bitmap);
bmpDrawable.setAlpha(0);

ObjectAnimator fadeOut = ObjectAnimator.ofFloat(bmpDrawable, "alpha", 1);
fadeOut.setDuration(3000);
fadeOut.start();

Your's fan
Another Android Guy

Unknown said...

Also is this the right way to fadeIn a view using ViewPropertyAnimator!

v.setAlpha(0);
v.animate().alpha(1).setDuration(300);

And if the animation framework is supposed to support only views then why should we use ObjectAnimator (Hacky) instead of ViewProperyAnimation (Native)?

Thanks