From Punched Cards to Prompts
AndroidIntroduction When computer programming was young, code was punched into cards. That is, holes were punched into a piece of cardboard in a format...
Animations add vivacity and personality to your apps. Let’s take a look at how to implement a subcategory of animations called “Frame Animations,” meaning that they’re drawn frame by frame.
In Google’s official Material Design spec, there’s an entire page dedicated to Delightful Details, which has wonderful examples of Frame Animations in action.
Nifty animations! Unfortunately, nothing on the page links to resources for actually creating those delightful details, so here I am to help! Specifically, we’re going to walk through making an empty heart animate into a filled-up heart, then vice versa. It’ll look something like this:
…beautiful, I know.
The idea behind a frame animation is simple: We’ll be cycling through a series of images very quickly, just like an old movie reel. The “frame” refers to a single image. Thus, the first step in creating a custom frame animation is to create a sequence of images.
We have two options here: we can use XML drawables (such as shape drawables) or actual image files. For the sake of simplicity, we’re going to use the following series of PNG images:
In a production application, we would also make sure to have images sized appropriately for different screen densities. For now, shove those images into the res/drawable-mdpi
folder and call it a day. I would also recommend naming them in a self-descriptive way, such as ic_heart_0.png
, ic_heart_1.png
and so on. This way, we know the order of the images without having to view them.
I chose to name my heart images by their respective filled-up percentage, because I’m a nerd.
Now that we have our images to cycle through, the next step is to define an XML Drawable for our animation. Once again, we are faced with two possibilities: the Animation-list and the Animated-selector.
Animation-list is the default Frame Animation of choice, as it was introduced in API 1. It works everywhere, and it’s simple. It just cycles through a sequence of provided images in a given order with given durations.
Here’s an example of an Animation-list for my heart filling up, placed in res/drawable/animation_list_filling.xml
:
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="true">
<item
android:duration="500"
android:drawable="@drawable/ic_heart_0"/>
<item
android:duration="500"
android:drawable="@drawable/ic_heart_25"/>
<item
android:duration="500"
android:drawable="@drawable/ic_heart_50"/>
<item
android:duration="500"
android:drawable="@drawable/ic_heart_75"/>
<item
android:duration="500"
android:drawable="@drawable/ic_heart_100"/>
</animation-list>
Each item in the list is just pointing to one of the images in our sequence from earlier. All we have to do is place them in the correct order and then add an appropriate duration in milliseconds.
And here’s an example of an Animation-list for my heart emptying, placed in res/drawable/animation_list_emptying.xml
:
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="true">
<item
android:duration="500"
android:drawable="@drawable/ic_heart_100"/>
<item
android:duration="500"
android:drawable="@drawable/ic_heart_75"/>
<item
android:duration="500"
android:drawable="@drawable/ic_heart_50"/>
<item
android:duration="500"
android:drawable="@drawable/ic_heart_25"/>
<item
android:duration="500"
android:drawable="@drawable/ic_heart_0"/>
</animation-list>
You might notice the android:oneshot=”true”
in both of these code snippets, which is simply an attribute of the animation-list for playing the animation once and then stopping. If this is set to “false,” the animation will play on repeat.
In production, the 500ms duration is a long time, but I want to exaggerate the animations for demonstration purposes. Also note that five frames isn’t very many images for a smooth transition. The number of frames to use and how long to display them is a problem to solve on an individual-implementation basis. For a frame of reference, 15 frames at 15ms is very smooth.
Animated-selector is a bit more complex, as it’s state-based. Depending on the state of a View (such as selected or activated), the selector will animate to the correct state using provided Transitions. The Animated-selector is implemented only for Lollipop (and above), so we’re going to define our XML in the -v21
package.
Here is an example of the Animated-selector, placed in res/drawable-v21/selector.xml
:
<?xml version="1.0" encoding="utf-8"?>
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/on"
android:state_activated="true">
<bitmap
android:src="@drawable/ic_heart_100"/>
</item>
<item
android:id="@+id/off">
<bitmap
android:src="@drawable/ic_heart_0"/>
</item>
<transition
android:fromId="@+id/on"
android:toId="@+id/off"
android:drawable="@drawable/animation_list_emptying">
</transition>
<transition
android:fromId="@id/off"
android:toId="@id/on"
android:drawable="@drawable/animation_list_filling">
</transition>
</animated-selector>
Take note of how it’s actually referencing our Animation-lists from earlier as Transitions.
This animated-selector works well, but we need to account for the non-Lollipop devices. We’re going to define a non-animated selector, placed in res/drawable/selector.xml
:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_activated="true">
<bitmap android:src="@drawable/ic_heart_100"/>
</item>
<item
android:state_activated="false">
<bitmap android:src="@drawable/ic_heart_0"/>
</item>
</selector>
Now our selector will work on any device. If tried on a pre-Lollipop device, the animated-selector will just just skip the Transitions and go directly to the end state, since we’re just using a normal selector. And of course, a Lollipop device will have our Transition that we defined in the animated-selector.
In the above snippet, the animated-selector cares about the android:state_activated
attribute. Just like a normal selector, I have different items defined for the possible states. However, I also have transitions defined for how to animate between these different states. In this particular animation, I just pointed the transitions to the animation-list drawables that we defined earlier.
We now have four XML files: one for emptying the heart, one for filling the heart, and two selectors for transitioning between empty and full states.
It’s time to set up some ImageViews for us to play with. Specifically, we’re going to have three ImageViews, one for each XML Drawable that we defined previously. Put the following code in a Layout of your choice and throw it in an Activity:
<ImageView
android:id="@+id/imageview_animation_list_filling"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/animation_list_filling"
/>
<ImageView
android:id="@+id/imageview_animation_list_emptying"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/animation_list_emptying"
/>
<ImageView
android:id="@+id/imageview_animated_selector"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/selector"
/>
This is just a few ImageViews with unique ids and backgrounds pointing to our XML Drawables from earlier.
The behavior for starting the animations differs between the two implementations, so we’ll start with the animation-list.
In our Activity, we grab a reference to the ImageView and then start the animation, like so:
ImageView mImageViewFilling = (ImageView) findViewById(R.id.imageview_animation_list_filling);
((AnimationDrawable) mImageViewFilling.getBackground()).start();
Here’s what that looks like :
Now for its partner code (identical except for the id):
ImageView mImageViewEmptying = (ImageView) findViewById(R.id.imageview_animation_list_emptying);
((AnimationDrawable) mImageViewEmptying.getBackground()).start();
And here’s what that part looks like:
Those code snippets can be put in onCreate
(automatically begins when the Activity begins) or in an OnClickListener
(waits for user interaction). The choice is yours!
When using the Animated-selector, the animation will trigger whenever the state-based condition is met for the selector. In our simple sample, we’re going to add a click listener to our ImageView in the onCreate
method of our Activity:
mImageViewSelector.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mImageViewSelector.setActivated(!mImageViewSelector.isActivated());
}
});
When the user clicks on our heart, the heart will fill or empty, depending on the current state. Here’s a nice GIF of my heart looping back and forth forever (presumably with a user clicking at each full and empty state):
Be kind to our hearts, users!
Frame Animations have the power to surprise and delight users, plus it’s fun to add little personal touches to an app. Go forth and animate!
Introduction When computer programming was young, code was punched into cards. That is, holes were punched into a piece of cardboard in a format...
Jetpack Compose is a declarative framework for building native Android UI recommended by Google. To simplify and accelerate UI development, the framework turns the...
Big Nerd Ranch is chock-full of incredibly talented people. Today, we’re starting a series, Tell Our BNR Story, where folks within our industry share...