Animator — Android animation tool

What is Animator?

Let’s start with some history. The View Animation framework existed from the moment the Android platform was launched. As its name might suggest, it was used for creating animation. However, devices from the 00’s had severe performance issues and complex animation was out of the question. As a result, this framework was inconvenient and inflexible, with only four animation types (TranslateAnimation, AlphaAnimation, ScaleAnimation, RotateAnimation) and a class that allowed to combine them (AnimationSet). Also, it could only work with classes it inherited from View.

Android 3.0 introduced a much more flexible framework called Property Animation. It can alter any of the available properties as well as work with any class. Its main tool is Animator.

Animator is a class type aimed at altering the values of a specific object in a timespan. Generally speaking, it is a tool that controls a stream of set duration that changes a specific property from its initial value to its final value. Transparency is a good example of a gradually changing property in animation.

Ideologically, the Animator and View Animation classes are different because View Animation changes the “presentation” of the object without altering it (with the exception of setFillAfter(true), but this flag changes the object at the end of the animation), while Animator is designed to alter the properties of the object itself.

​Classes inherited from Animator​

ValueAnimator (inherited from Animator)

In the simplest scenario we set the type of changing property for this class, its initial and final values, and launch it. As a result, events will take place at the start, end, repetition and cancellation of the animation, and on two extra events that are specified separately for pause and change of value. The change of value event is crucial, as it will contain the altered value, using which we will change the object properties.

Have a look at how we use it to change alpha:

ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
   @Override
   public void onAnimationUpdate(ValueAnimator animation) {
       view.setAlpha((Float) animation.getAnimatedValue());
   }
});
animator.start();

ObjectAnimator, inherited from ValueAnimator

This class makes working with ValueAnimator easier. With it, you no longer need to manually change a value on the event of change; instead, you just refer an object to the Animator and specify the field you would like to change, for example, scaleX. You can use Java Reflection to find the setter for this field (in this case, setScaleX). After this, Animator will alter the value of this field on its own.

Changing alpha using ObjectAnimator would look like this:

ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).start();

The View class has several properties designated for creating animations using Animator:

  • alpha (View.ALPHA);
  • scale (View.SCALE_X, View.SCALE_Y);
  • rotation (View.ROTATION, View.ROTATION_X, View.ROTATION_Y);
  • position (View.X, View.Y, View.Z);
  • position of the displayed fragment (View.TRANSLATION_X, View.TRANSLATION_Y, View.TRANSLATION_Z).

AnimatorSet (inherited from Animator)

This class allows to combine animations in several ways by launching them simultaneously or subsequently, adding delay and so on.

ViewPropertyAnimator

This is a separate class. It is not inherited from Animator, but its logic is similar to ObjectAnimator for View, and it is used for easily animating a View without too much hassle:

This is how you can change alpha with it:

view.animate().alphaBy(0).alpha(1).start();

How we started using Animator

About a year ago I had to create an animation that would be displayed after clicking an element, like this:

355820622fb24674b809518a2004ca4e.gif


It’s not that I have never made animations before, but animations are rarely needed in outsourcing, and so I typed “Animation Android” into Google. The first five links described how to make an animation in detail, and I got to work. The first result looked like this:

355820622fb24674b809518a2004ca4e.gif


Animation code:

 public static void likeAnimation(@DrawableRes int icon,
                                     final ImageView imageView) {
        imageView.setImageResource(icon);
        imageView.setVisibility(View.VISIBLE);
        AlphaAnimation showAlphaAnimation = new AlphaAnimation(0.0f, 1.0f);
        showAlphaAnimation.setDuration(SHOW_DURATION);
        ScaleAnimation showScaleAnimation = new ScaleAnimation(0.2f, 1.4f, 0.2f, 1.4f,
                android.view.animation.Animation.RELATIVE_TO_SELF, 0.5f,
                android.view.animation.Animation.RELATIVE_TO_SELF, 0.5f);
        showScaleAnimation.setDuration(SHOW_DURATION);
        AnimationSet showAnimationSet = new AnimationSet(false);
        showAnimationSet.addAnimation(showAlphaAnimation);
        showAnimationSet.addAnimation(showScaleAnimation);
        showAnimationSet.setAnimationListener(new OnEndAnimationListener() {
            @Override
            public void onAnimationEnd(android.view.animation.Animation animation) {
                ScaleAnimation toNormalScaleAnimation = new ScaleAnimation(1.4f, 1.0f, 1.4f, 1.0f,
                        android.view.animation.Animation.RELATIVE_TO_SELF, 0.5f,
                        android.view.animation.Animation.RELATIVE_TO_SELF, 0.5f);
                toNormalScaleAnimation.setDuration(TO_NORMAL_DURATION);
                toNormalScaleAnimation.setAnimationListener(new OnEndAnimationListener() {
                    @Override
                    public void onAnimationEnd(android.view.animation.Animation animation) {
                        AlphaAnimation hideAlphaAnimation = new AlphaAnimation(1.0f, 0.0f);
                        hideAlphaAnimation.setDuration(HIDE_DURATION);
                        ScaleAnimation hideScaleAnimation = new ScaleAnimation(1.0f, 0.2f, 1.0f, 0.2f,
                                android.view.animation.Animation.RELATIVE_TO_SELF, 0.5f,
                                android.view.animation.Animation.RELATIVE_TO_SELF, 0.5f);
                        hideScaleAnimation.setDuration(HIDE_DURATION);
                        AnimationSet hideAnimationSet = new AnimationSet(false);
                        hideAnimationSet.setStartOffset(HIDE_DELAY);
                        hideAnimationSet.addAnimation(hideAlphaAnimation);
                        hideAnimationSet.addAnimation(hideScaleAnimation);
                        hideAnimationSet.setAnimationListener(new OnEndAnimationListener() {
                            @Override
                            public void onAnimationEnd(android.view.animation.Animation animation) {
                                imageView.setVisibility(View.GONE);
                            }
                        });
                        imageView.startAnimation(hideAnimationSet);
                    }
                });
                imageView.startAnimation(toNormalScaleAnimation);
            }
        });
        imageView.startAnimation(showAnimationSet);
    }

Code link

The code turned out to be obscure, and so I searched for a different approach to creating an animation sequence. I found the solution on StackOverflow. The idea was to put each subsequent animation in the sequence into AnimationSet with a shift equal to the sum of durations of the previous animations. It turned out to be much better:

935d70fd173c465a9b1876a159bbe983.gif


AnimationSet:

 public static void likeAnimation(@DrawableRes int icon,
                                     final ImageView imageView) {
        imageView.setImageResource(icon);
        imageView.setVisibility(View.VISIBLE);
        AnimationSet animationSet = new AnimationSet(false);
        animationSet.addAnimation(showAlphaAnimation());
        animationSet.addAnimation(showScaleAnimation());
        animationSet.addAnimation(toNormalScaleAnimation());
        animationSet.addAnimation(hideAlphaAnimation());
        animationSet.addAnimation(hideScaleAnimation());
        animationSet.setAnimationListener(new OnEndAnimationListener() {
            @Override
            public void onAnimationEnd(Animation animation) {
                imageView.setVisibility(View.GONE);
            }
        });
        imageView.startAnimation(animationSet);
    }

    private static Animation showAlphaAnimation() {
        AlphaAnimation showAlphaAnimation = new AlphaAnimation(0.0f, 1.0f);
        showAlphaAnimation.setDuration(SHOW_DURATION);
        return showAlphaAnimation;
    }

    private static Animation showScaleAnimation() {
        ScaleAnimation showScaleAnimation = new ScaleAnimation(
                0.2f, 1.4f, 0.2f, 1.4f,
                Animation.RELATIVE_TO_SELF, 0.5f,
                Animation.RELATIVE_TO_SELF, 0.5f);
        showScaleAnimation.setDuration(SHOW_DURATION);
        return showScaleAnimation;
    }

    private static Animation toNormalScaleAnimation() {
        ScaleAnimation toNormalScaleAnimation = new ScaleAnimation(
                1.4f, 1.0f, 1.4f, 1.0f,
                Animation.RELATIVE_TO_SELF, 0.5f,
                Animation.RELATIVE_TO_SELF, 0.5f);
        toNormalScaleAnimation.setDuration(TO_NORMAL_DURATION);
        toNormalScaleAnimation.setStartOffset(SHOW_DURATION);
        return toNormalScaleAnimation;
    }

    private static Animation hideAlphaAnimation() {
        AlphaAnimation hideAlphaAnimation = new AlphaAnimation(1.0f, 0.0f);
        hideAlphaAnimation.setDuration(HIDE_DURATION);
        hideAlphaAnimation.setStartOffset(SHOW_DURATION + TO_NORMAL_DURATION + HIDE_DELAY);
        return hideAlphaAnimation;
    }

    private static Animation hideScaleAnimation() {
        ScaleAnimation hideScaleAnimation = new ScaleAnimation(
                1.0f, 0.2f, 1.0f, 0.2f,
                Animation.RELATIVE_TO_SELF, 0.5f,
                Animation.RELATIVE_TO_SELF, 0.5f);
        hideScaleAnimation.setDuration(HIDE_DURATION);
        hideScaleAnimation.setStartOffset(SHOW_DURATION + TO_NORMAL_DURATION + HIDE_DELAY);
        return hideScaleAnimation;
    }

Code link

The code became more readable and transparent, but at a cost: tracking this shift for every animation was rather inconvenient even in a simple sequence like this. Adding a few more steps would make this task almost impossible. Another drawback of this approach was the odd behavior of the animation, as the size of the animated object (for unknown reasons) was larger than in a regular animation sequence. I was unable to figure out why and decided not to dig in deeper as I didn’t like this approach anyway. However, I still wanted to develop this idea, and tried to split each step into a separate AnimatorSet. Here’s the result:

3a3bc5a293044c5684722d05bf552be1.gif


AnimatorSet in AnimatorSet:

public static void likeAnimation(@DrawableRes int icon,
                                     final ImageView imageView) {
        imageView.setImageResource(icon);
        imageView.setVisibility(View.VISIBLE);
        AnimationSet animationSet = new AnimationSet(false);
        animationSet.addAnimation(showAnimationSet());
        animationSet.addAnimation(toNormalAnimationSet());
        animationSet.addAnimation(hideAnimationSet());
        animationSet.setAnimationListener(new OnEndAnimationListener() {
            @Override
            public void onAnimationEnd(Animation animation) {
                imageView.setVisibility(View.GONE);
            }
        });
        imageView.startAnimation(animationSet);
    }

    private static AnimationSet showAnimationSet() {
        AlphaAnimation showAlphaAnimation = new AlphaAnimation(0.0f, 1.0f);
        ScaleAnimation showScaleAnimation = new ScaleAnimation(
                0.2f, 1.4f, 0.2f, 1.4f,
                Animation.RELATIVE_TO_SELF, 0.5f,
                Animation.RELATIVE_TO_SELF, 0.5f);
        AnimationSet set = new AnimationSet(false);
        set.addAnimation(showAlphaAnimation);
        set.addAnimation(showScaleAnimation);
        set.setDuration(SHOW_DURATION);
        return set;
    }

    private static AnimationSet toNormalAnimationSet() {
        ScaleAnimation toNormalScaleAnimation = new ScaleAnimation(
                1.4f, 1.0f, 1.4f, 1.0f,
                Animation.RELATIVE_TO_SELF, 0.5f,
                Animation.RELATIVE_TO_SELF, 0.5f);
        AnimationSet set = new AnimationSet(false);
        set.addAnimation(toNormalScaleAnimation);
        set.setDuration(TO_NORMAL_DURATION);
        set.setStartOffset(SHOW_DURATION);
        return set;
    }

    private static AnimationSet hideAnimationSet() {
        AlphaAnimation hideAlphaAnimation = new AlphaAnimation(1.0f, 0.0f);
        ScaleAnimation hideScaleAnimation = new ScaleAnimation(
                1.0f, 0.2f, 1.0f, 0.2f,
                Animation.RELATIVE_TO_SELF, 0.5f,
                Animation.RELATIVE_TO_SELF, 0.5f);
        AnimationSet set = new AnimationSet(false);
        set.setDuration(HIDE_DURATION);
        set.addAnimation(hideAlphaAnimation);
        set.addAnimation(hideScaleAnimation);
        set.setStartOffset(SHOW_DURATION + TO_NORMAL_DURATION + HIDE_DELAY);
        return set;
    }

Code link

The animation was glitchy, the approach was terrible, everything went wrong. I googled for help again and found out that Animation had become Legacy code already: obsolete and no longer supported, even if still used.

I understood that animations should be made in a different way altogether. I stumbled upon Animator on Android Developers, and my next attempt to make an animation with the help of Animator looked like this:

d67ae74f22d34d0f8da4037229c95cf5.gif


Animator:

public static void likeAnimation(@DrawableRes int icon,
                                     final ImageView view) {
        if (view != null && !isAnimate) {
            AnimatorSet set = new AnimatorSet();
            set.playSequentially(
                    showAnimatorSet(view),
                    toNormalAnimatorSet(view),
                    hideAnimatorSet(view));
            set.addListener(getLikeEndListener(view, icon));
            set.start();
        }
        view.animate().alphaBy(0).alpha(1).start();
    }

    private static AnimatorListenerAdapter getLikeEndListener(final ImageView view, final int icon) {
        return new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
                isAnimate = true;
                view.setVisibility(View.VISIBLE);
                view.setImageResource(icon);
                view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                isAnimate = false;
                view.setVisibility(View.GONE);
                view.setImageDrawable(null);
                view.setLayerType(View.LAYER_TYPE_NONE, null);
            }
        };
    }

    private static AnimatorSet showAnimatorSet(View view) {
        AnimatorSet set = new AnimatorSet();
        set.setDuration(SHOW_DURATION).playTogether(
                ObjectAnimator.ofFloat(view, View.ALPHA, 0f, 1f),
                ObjectAnimator.ofFloat(view, View.SCALE_X, 0.2f, 1.4f),
                ObjectAnimator.ofFloat(view, View.SCALE_Y, 0.2f, 1.4f)
        );
        return set;
    }

    private static AnimatorSet toNormalAnimatorSet(View view) {
        AnimatorSet set = new AnimatorSet();
        set.setDuration(TO_NORMAL_DURATION).playTogether(
                ObjectAnimator.ofFloat(view, View.SCALE_X, 1.4f, 1f),
                ObjectAnimator.ofFloat(view, View.SCALE_Y, 1.4f, 1f)
        );
        return set;
    }

    private static AnimatorSet hideAnimatorSet(View view) {
        AnimatorSet set = new AnimatorSet();
        set.setDuration(HIDE_DURATION).playTogether(
                ObjectAnimator.ofFloat(view, View.ALPHA, 1f, 0f),
                ObjectAnimator.ofFloat(view, View.SCALE_X, 1f, 0.2f),
                ObjectAnimator.ofFloat(view, View.SCALE_Y, 1f, 0.2f)
        );
        set.setStartDelay(HIDE_DELAY);
        return set;
    }

Code link

The animation worked flawlessly, so my search was finally successful. The only thing you have to keep in mind when working with Animator is to make sure no other Animator is launched for the specific View, because otherwise the old one will continue to work as if nothing has happened.

Diving into Animator

Then I started to look for more interesting things to do using Animator, and my imagination led me to the following result:

a75a9b92a0e54275b1c4a92ba499f618.gif

Clicking on the button launches four Animators at once:

  • Simultaneous launch
 AnimatorSet showHideSet = new AnimatorSet();
      showHideSet.playTogether(
                ScrollAnimatorUtils.translationYAnimator(translationY, footerButtons),
                ScrollAnimatorUtils.translationYAnimator(translationY, footerText),
                ScrollAnimatorUtils.scrollAnimator(startScroll, endScroll, scrollView),
                ScrollAnimatorUtils.alphaAnimator(1, 0, recyclerView)
        );
        showHideSet.start();

1) the list footer is moved down;

2) the buttons are moved down;

  • translationYAnimator
 public static Animator translationYAnimator(final float start, int end, final View view, int duration) {
        ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, end);
        animator.setDuration(duration);
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                view.setTranslationY(start);
            }
        });
        return animator;
    }

3) ScrollView is scrolled to the bottom;

  • scrollAnimator
public static Animator scrollAnimator(int start, int end, final View view, int duration) {
        ValueAnimator scrollAnimator = ValueAnimator.ofInt(start, end);
        scrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                view.scrollTo(0, (int) valueAnimator.getAnimatedValue());
            }
        });
        scrollAnimator.setDuration(duration);
        scrollAnimator.addListener(getLayerTypeListener(view));
        return scrollAnimator;
    }

4. adds alpha effect to recyclerView.

  • alphaAnimator
public static Animator alphaAnimator(int start, int end, View view, int duration) {
        ValueAnimator alphaAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, start, end);
        alphaAnimator.setDuration(duration);
        alphaAnimator.addListener(getLayerTypeListener(view));
        return alphaAnimator;
    }

Full code link

Project link at Github

Animation
Mobile
Android