Loading...

"The solution often turns out more beautiful than the puzzle."

Arushi Pant

Always eager to learn. Determined to make the best of the time I've got on Earth.

Custom loading icon using AnimatedVectorDrawable

Posted on June 25, 2018

In Project 2 of my Google-Udacity Android Developer Nanodegree[1], I wanted a custom animated loader for my app. So, I loaded a gif image using Glide in the loader ImageView.

 

But, usage of gif is discouraged in Android. This is what I got when I ran the Lint tool against my code - 

Android Lint: Usability

Using .gif format for bitmaps is discouraged. 

 

This may be because it may cause issues with older devices, or some unforeseen performance issues because a gif is usually heavy. 

So, I decided to use AnimatedVectorDrawable for my animated loader. This blog post is about how I created the custom loading icon step-by-step.

 

Let's start with what I wanted to achieve. There are two images below. The first one is that of the gif I was using. The second one is the newer vector version that I created.

gif loader                     animated vector drawable loader

 

Starting with the basics:

 

What is an AnimatedVectorDrawable?

A VectorDrawable is a type of drawable that can scale without getting pixelated or blurry. That is why, whenever possible, it is better to use vector drawables instead of adding png/jpeg drawables for each screen density. It makes creating responsive UI more convenient and efficient.

An AnimatedVectorDrawable is a class that lets us animate the properties of a vector drawable, such as changing its colour, or changing the path data to morph it into another image. For backward compatibility, we can use AnimatedVectorDrawableCompat class instead.

An AnimatedVectorDrawable can be defined as 3 separate xml files, or merged into one. 

When we define it as 3 separate XML files, they will be stored as - 

  1. A VectorDrawable XML file ( in res/drawable )
  2. An AnimatedVectorDrawable XML file ( in res/drawable )
  3. An Animator XML file

Or, we can merge all these related XML files into a single XML file. 

 

You can read more about the basics in the official documentation.

 

Let's begin with our AnimatedVectorDrawable. We will go step-by-step through the process.

 

1. Create a Vector Drawable XML file

 

Vector Drawable

This icon has 9 squares - 8 blue, and 1 pink one ( overlapping one of the blue squares ). 

In the XML, you can see there are two paths defined. We do not define a separate path for each of the blue squares. There is no need. All the squares are of the same colour, and do not require any specific animations that are different for each square. So, the 2 paths are -

  •  A path that defines the shape that looks like a square split up into smaller blue squares.
  • Another path that defines a smaller pink square. This square will be moved clockwise on top of the blue squares using animation.

 

The 2 pathData commands we use here are -

M (X,Y)+  - The absolute moveto command that will move the cursor to the position with (X,Y) coordinates.

l (X,Y) - The relative lineto command that will draw a line from current position to the position with (X,Y) coordinates.

 

In pathData commands, Uppercase (eg. M) is absolute, and lowercase (eg. l) is relative.

 

Another thing to remember here is that the point at the top left of the canvas is (0,0). From there, x increases to the right, and y increases to the bottom. As we have set android:viewportHeight="57.0" and android:viewportWidth="57.0" , the bottom-right of our canvas will be the point (57,57).

 

We make a sketch of the shape we want to make before writing the code. This will help us determine the coordinates of the different squares. We take the size of each square to be 17 and start from (0,0). You change the square size, and the coordinates, according to your taste.

Now, that we know the coordinates, we will start with the path.

The first path begins with 

M 0,0 l 17,0 l 0,17 l -17,0 l 0,-17

This draws the first blue square (just under the pink one in the image). Splitting it into pathData commands, we get the following instructions

M 0,0 - Move to (0,0)

l 17,0 - Draw a line to (17,0)

l 0,17 - Draw a line to (17,17). Remember, this is the relative command. If we had given the absolute command, it would have been L 17,17

l -17,0 - Draw a line to (0,17)

l 0,-17 - Draw a line to (0,0)

When you use the same command multiple times in a row, you can eliminate the command letter on subsequent commands. So our final commands look like this

M 20,0 l 17,0 0,17 -17,0 0,-17

 

In the Preview panel, we will be able to see our little blue square.

 

For the second square, we will move to its start position before drawing further. So, the next command to be added will be M 20,0.

Now that we are at (20,0), we can follow all the relative commands for drawing the square because the coordinates will be calculated w.r.t. the start position (20,0).

 

This time our instructions will be -

M 20,0 - Move to (20,0)

l 17,0 - Draw a line to (37,0)

l 0,17 - Draw a line to (37,17)

l -17,0 - Draw a line to (20,17)

l 0,-17 - Draw a line to (20,0)

 

We will draw the rest of the squares similarly, adding more commands to this same path. Only the coordinates of the moveto command change, as the rest is relative.

 

This is what we have achieved so far.

 

 

We will store this path as a string resource in the res/values/strings.xml .

 

The path for the pink square will be the same as that of the first blue square as we want them to overlap.


You can read more about creating vector drawables - Creating Simple Vector Drawables, Understanding Vector Drawable pathData commands, What do pivotx & pivoty mean.

 

2. Create an Animator XML

 

Here's a great article on animation techniques that you could read before you continue ahead - Introduction to icon animation techniques.

 

First, we create a new XML file in the res/animator folder. We will define an Animator Set in this file that will later be mapped to the pink square path in our vector drawable.

We want the pink square to show up over each blue square one by one. For this, we need it to move, and we update the pathData to redraw it over each blue square one-by-one. ( I tried translationX and x properties to move the square but it didn't work for me )

Our pink square is over the first blue square initially. Now, we want to move it to the second blue square location. 

 

Here is how the code will start off…

<?xml version="1.0" encoding="utf-8"?>
<set
    xmlns:android="http://schemas.android.com/apk/res/android" >

    <!-- Move square 1 to square 2 -->
    <objectAnimator
        android:propertyName="fillAlpha"
        android:valueFrom="1"
        android:valueTo="0"
        android:duration="0"
        android:startOffset="100"/>

    <objectAnimator
        android:duration="0"
        android:propertyName="pathData"
        android:valueFrom="M 0,0 l 17,0 0,17 -17,0 0,-17"
        android:valueTo="M 20,0 l 17,0 0,17 -17,0 0,-17"
        android:valueType="pathType"
        android:startOffset="100" />

    <objectAnimator
        android:propertyName="fillAlpha"
        android:valueFrom="0"
        android:valueTo="1"
        android:duration="0"
        android:startOffset="150" />
.
.
.
</set>

 

We have to first hide the pink square being shown on the 1st position. For this, we change the property name "fillAlpha" value from 1 to 0. 

Then, we redraw the square at a different position ( the 2nd square ). We update the pathData accordingly.

When the square is at 2nd position, we show it again by changing "fillAlpha" value from 0 to 1.

 

To ensure a seamless experience of movement, we calculate and assign values to duration and startOffset. The duration is assigned value 0 as we want the property change to be immediate. The startOffset is the time after which we want the animation to start from the beginning of the animator set.

 

Our calculations for startOffset will look somewhat like this:

-- display for = 100 -- Hide -- immediate -- Move -- time btwn hide & show = 50 -- Show --


   Square 1 to 2:       100                  100                                   150

   Square 2 to 3:       250                  250                                   300

.

.

.

   Square 8 to 1:       1150                 1150                                 1200

 

Update all the values and the final code will look like this - Github link

 

3. Create an AnimatedVectorDrawable XML file

 

This XML file in the res/drawable folder will map our vector drawable (created in step 1) to the animator (created in step 2)

<animated-vector
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/av_icon" >

    <target
        android:name="square_progress"
        android:animation="@animator/move" />
</animated-vector>

 

Our AnimatedVectorDrawable is now ready for use.

 

4. Add ImageView for loader to layout

 

I am omitting positional code from the view code. You can add according to your layout.

We set our AndroidVectorDrawable (the one created in step 3) as the background for the ImageView.

<ImageView

   android:id="@+id/pb"

   android:layout_width="70dp"

   android:layout_height="70dp"

   android:background="@drawable/avd_animated_icon"

   android:contentDescription="@string/desc_loader"

/>

 

5. Show animated loader in Activity

 

We start our animation in onStart(), not in onCreate(). This is because onCreate() is not always called - it is called only when the activity is created. On the other hand, onStart() will be called every time the activity is visible to the user.

import android.graphics.drawable.AnimatedVectorDrawable;
import android.widget.ImageView;

.
.
.

@Override
protected void onStart() {
   super.onStart();

   /* Loader - Squares */
   ImageView pb = findViewById(R.id.pb);
   ((AnimatedVectorDrawable) pb.getBackground()).start();
}

 

Now we run our application. This is what we get.

 

Oh!! What happened?

The loader is shown, but the animation is shown only once. This is because we need to tell it to repeat in order to make it run more than once.

 

6. Make loader animation repeat infinitely 

 

Got the solution to this problem from StackOverflow. We will have to change our code a little to make this work as expected.

We will now use AnimatedVectorDrawableCompat instead of AnimatedVectorDrawable.

 

AnimatedVectorDrawable is available for use from API 21+ , but the registerAnimationCallback function that we will use to loop animation was added in API 23+. This forces us to use AnimatedVectorDrawableCompat.

 

First, add we the below line in our defaultConfig  in the app build.gradle.

vectorDrawables.useSupportLibrary = true

 

Now, we change the layout file.

Remove background from the ImageView. We will add it programmatically to make it work with AnimatedVectorDrawableCompat.

If we add the drawable as background in the layout file, the program automatically takes it to be an AnimatedVectorDrawable type, and will cause the following error -

java.lang.ClassCastException: android.graphics.drawable.AnimatedVectorDrawable 
cannot be cast to android.support.graphics.drawable.AnimatedVectorDrawableCompat

 

Our ImageView code (minus the positioning code) will now be as follows -

<ImageView
   android:id="@+id/pb"
   android:layout_width="70dp"
   android:layout_height="70dp"
   android:contentDescription="@string/desc_loader"
/>

 

 

In our activity onStart function, we create an AnimatedVectorDrawableCompat resource and set it as the background of the ImageView.

public class MainActivity extends AppCompatActivity {
   AnimatedVectorDrawableCompat mAnimatedVector;
   .
   .
   .
   @Override
   protected void onStart() {
   super.onStart();

              ImageView pb = findViewById(R.id.pb);
               mAnimatedVector = AnimatedVectorDrawableCompat.create(this, R.drawable.avd_animated_icon);
              pb.setImageDrawable(mAnimatedVector);
     .
     .
     .

 

Now, we need to register an Animation Callback so that we can restart our animation every time it ends. This will put our loading animation into an endless loop to give us the desired effect.

 

Our final code in the activity will be

import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.support.graphics.drawable.Animatable2Compat.AnimationCallback;
import android.support.graphics.drawable.AnimatedVectorDrawableCompat;
import android.widget.ImageView;

public class MainActivity extends AppCompatActivity {
   AnimatedVectorDrawableCompat mAnimatedVector;
   AnimationCallback mAnimationCallback;

   .
   .
   .
   @Override
   protected void onStart() {
   super.onStart();

              ImageView pb = findViewById(R.id.pb);
               mAnimatedVector = AnimatedVectorDrawableCompat.create(this, R.drawable.avd_animated_icon);
              pb.setImageDrawable(mAnimatedVector);
              final Handler mainHandler = new Handler(Looper.getMainLooper());

   if(mAnimatedVector != null) {
      mAnimationCallback = new AnimationCallback() {
          @Override
          public void onAnimationEnd(final Drawable drawable) {
              mainHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mAnimatedVector.start();
                  }
              });
          }
      };

                mAnimatedVector.registerAnimationCallback(mAnimationCallback);

                mAnimatedVector.start();
      }
}

 @Override
protected void onDestroy() {
   super.onDestroy();
   mAnimatedVector.unregisterAnimationCallback(mAnimationCallback);
}

 

 

You can now customise the code to hide and show progress with our custom Animated Vector Drawable.

 

Here's a video clip of our loader -

 

 

Time for a nice little break, and a pat on the back. You learnt something cool today. 🎉 🎊

 

[1] Referral link - Will give you a Rs. 1000 cashback on your first Nanodegree enrollment


Liked the post?

Show your appreciation, and share with others.