SureshJoshi.com ▼

Object Animation in Android


2016-01-19

Object animation is something that seems to be very common in Android apps, however, I’ve noticed that a lot of the examples of it on StackOverflow and elsewhere on the internet seem to skip out on some crucial information.

In an app I wrote a while ago, I needed to animate an image to look as though it was hovering. For the demo, the requirement was as simple as getting the image to bob up and down for 5 seconds. Not having done much animation in the past, I took to trusty StackOverflow and used some demo code I found there. Worked just fine and looked (similar) to what I’ve written below (two ObjectAnimators put into an AnimatorSet):

ObjectAnimator animator1 = ObjectAnimator.ofFloat(mAnimationButton, "translationY", 0, translationDistancePixels);
animator1.setRepeatCount(0);
animator1.setDuration(kAnimationDuration);

ObjectAnimator animator2 = ObjectAnimator.ofFloat(mAnimationButton, "translationY", translationDistancePixels, -translationDistancePixels);
animator2.setRepeatCount(Animation.INFINITE);
animator2.setDuration(kAnimationDuration);
animator2.setRepeatMode(Animation.REVERSE);

// Create an animation set to play multiple animations
// Use .before to ensure the first animation finishes before the second starts
AnimatorSet set = new AnimatorSet();
set.play(animator1).before(animator2);
set.start();

This played for a few seconds, and the app moved on.

Moving to Prod

Fast-forward a few months, and the client comes back to me with working hardware, and now I need to make that image emulate what the sensor on the hardware was reading. The hardware sent back sensor readings every 50ms (which were a bit jittery), so I decided to slow those readings down to animations representing 100ms of movement (smoothes everything out).

I removed one of the ObjectAnimators from the above code and set the repeatCount to 0 - so that I could just play an ObjectAnimator that moved from the previous location to the new location (new location determined by mapping the hardware sensor to pixels).

I left the rest of the code the same, and it basically looked like this (except “Random” represented a reading coming from the hardware sensor, mapped to pixels):

Random random = new Random();
float translationDistancePixels = Utils.convertDpToPixel(TRANSLATION_DISTANCE_DP, MainActivity.this);

// Pick a random number and scale it between translationDistancePixels up or down
Float nextLocation = random.nextFloat() * 2 * translationDistancePixels - translationDistancePixels;
ObjectAnimator animator = ObjectAnimator.ofFloat(mRandomButton, "translationY", mLastRandomLocation, nextLocation);
mLastRandomLocation = nextLocation;

animator.setRepeatCount(0); // 0 because this is a one-time value
animator.setDuration(ANIMATION_DURATION);

mAnimatorSet.play(animator);
mAnimatorSet.start();

where mAnimatorSet was a member-variable AnimatorSet instance.

It looks pretty simple, and seemed to work fine during my testing, however, I noticed that by the end of the animation, occasionally, it looked like it was slowing down or was a bit jerky. A few times it even crashed!

Isolating the Problem

Instead of trying to debug in-app, with the hardware constraints, I decided to create a simple throwaway app to test a few ways of animating a moving element. Literally 3 buttons which either: translated up/down forever, translated up/down then eventually stopped somewhere, and had a stream of random translations as an input.

ObjectAnimationExample

I had the Random Button example update every 10ms, and running Android Device Monitor - I quickly discovered the problem:

Animator Object
Collection

After only about 10 seconds, I had accumulated thousands and thousands of objects in an ArrayList that never cleared itself.

Fixing the Problem

Looking into this, I should have probably either re-created the AnimatorSet each time I ran the location update handler, or maybe called .cancel() on the AnimatorSet - which might have cleared out that array.

In reality, the solution was much, much simpler… Just get rid of the AnimationSet entirely.

Random random = new Random();
float translationDistancePixels = Utils.convertDpToPixel(TRANSLATION_DISTANCE_DP, MainActivity.this);

// Pick a random number and scale it between translationDistancePixels up or down
Float nextLocation = random.nextFloat() * 2 * translationDistancePixels - translationDistancePixels;
ObjectAnimator animator = ObjectAnimator.ofFloat(mRandomButton, "translationY", mLastRandomLocation, nextLocation);
mLastRandomLocation = nextLocation;

animator.setRepeatCount(0); // 0 because this is a one-time value
animator.setDuration(ANIMATION_DURATION);
animator.start();

Running that code resulted in an extremely smooth animation, never got choppy, no more crashes. Here is the same allocation table:

Animation Object Collection
Reduced

If you want to try to reproduce this experiment, or see how you can perform object animation using random inputs, I’ve made my code used in this post available at https://github.com/sureshjoshi/android-object-animator-example.

Feature Photo credit: BRICK 101 / Foter / CC BY-NC-SA