Mobile Object Animation in Android
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.
I had the Random Button example update every 10ms, and running Android Device Monitor - I quickly discovered the problem:
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:
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