There is a nice micro-animation library at useanimations.com which contains some useful animations which are particularly well suited for animated icons. These are all free to use and downloadable at Lottie animations. For those that already use Lottie they can use these animations as-is. However using them is apps which don’t use Lottie, or in cases where they may need tweaking and you don’t have a designer available that can perform the necessary tweaks in After Effects, it may not be possible to do this. In this occasional series we’ll look at how to create some of these animations as Animated Vector Drawables which will show some useful AVD techniques.

The animation that we’re going to look at this time is a maximum / minimum state transition, or possibly an expanded / collapsed transition.

It consists of four ‘corner’ segments of a which each all point outwards for form the overall outline of a square for the maximum state; the segments transition to point inwards for the minimum state.

Let’s begin by looking at the two static vector states beginning with the max state:

res/drawable/max.xml <?xml version="1.0" encoding="utf-8"?> <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="64dp" android:height="64dp" android:viewportWidth="64" android:viewportHeight="64"> <path android:name="top_left" android:strokeColor="@android:color/white" android:strokeWidth="4" android:pathData="M24,8 L8,8 L8,24" android:strokeLineJoin="round" android:strokeLineCap="round" /> <path android:name="top_right" android:strokeColor="@android:color/white" android:strokeWidth="4" android:pathData="M40,8 L56,8 L56,24" android:strokeLineJoin="round" android:strokeLineCap="round" /> <path android:name="bottom_left" android:strokeColor="@android:color/white" android:strokeWidth="4" android:pathData="M24,56 L8,56 L8,40" android:strokeLineJoin="round" android:strokeLineCap="round" /> <path android:name="bottom_right" android:strokeColor="@android:color/white" android:strokeWidth="4" android:pathData="M40,56 L56,56 L56,40" android:strokeLineJoin="round" android:strokeLineCap="round" /> </vector> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <? xml version = "1.0" encoding = "utf-8" ?> < vector xmlns : android = "http://schemas.android.com/apk/res/android" android : width = "64dp" android : height = "64dp" android : viewportWidth = "64" android : viewportHeight = "64" > < path android : name = "top_left" android : strokeColor = "@android:color/white" android : strokeWidth = "4" android : pathData = "M24,8 L8,8 L8,24" android : strokeLineJoin = "round" android : strokeLineCap = "round" / > < path android : name = "top_right" android : strokeColor = "@android:color/white" android : strokeWidth = "4" android : pathData = "M40,8 L56,8 L56,24" android : strokeLineJoin = "round" android : strokeLineCap = "round" / > < path android : name = "bottom_left" android : strokeColor = "@android:color/white" android : strokeWidth = "4" android : pathData = "M24,56 L8,56 L8,40" android : strokeLineJoin = "round" android : strokeLineCap = "round" / > < path android : name = "bottom_right" android : strokeColor = "@android:color/white" android : strokeWidth = "4" android : pathData = "M40,56 L56,56 L56,40" android : strokeLineJoin = "round" android : strokeLineCap = "round" / > < / vector >

There are four paths here each representing the four distinct segments. As is often the case with these kinds of symmetrical animations we apply the same technique multiple times, so for the sake of simplicity we’ll focus on one of these segments throughout the remainder of this article. The path named top_left as one might expect is the segment in the top left corner. The path consists of three commands:

The M24,8 moves the current point to coordinates 24,8 without rendering anything, which is the right most point of the horizontal line section.

The L8,8 draws a horizontal line to coordinates 8,8. Wile we could achieve the same effect with an H8 command, I chose to use an explicit lineto command here because it will be more flexible later on when we come to animate things. This draws the horizontal line section

The final L8,24 command draws a vertical line to coordinates 8,24. Once again we could have used V24 here, but the explicit lineto is important when it comes to animating.

The android:strokeLineCap="round" attribute rounds the two line ends, and android:strokeLineJoin="round" rounds the corner where the two lines join, and this renders as:

For the min state, the static vector is:

res/drawable/min.xml <?xml version="1.0" encoding="utf-8"?> <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="64dp" android:height="64dp" android:viewportWidth="64" android:viewportHeight="64"> <path android:name="top_left" android:strokeColor="@android:color/white" android:strokeWidth="4" android:pathData="M24,8 L24,24 L8,24" android:strokeLineJoin="round" android:strokeLineCap="round" /> . . . </vector> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <? xml version = "1.0" encoding = "utf-8" ?> < vector xmlns : android = "http://schemas.android.com/apk/res/android" android : width = "64dp" android : height = "64dp" android : viewportWidth = "64" android : viewportHeight = "64" > < path android : name = "top_left" android : strokeColor = "@android:color/white" android : strokeWidth = "4" android : pathData = "M24,8 L24,24 L8,24" android : strokeLineJoin = "round" android : strokeLineCap = "round" / > . . . < / vector >

The first and last pathData commands are actually identical to those in the max state, with only the middle L24,24 being different. So instead of first drawing a horizontal line to 8,8 followed by a vertical line to 8,24, this min path first draws a vertical line to 24,24, followed by a horizontal line to 24,8:

The orientations of these lines changing is the reason that we avoided using the more specific H and V commands earlier – by using the more generic L command it enables us to animate between these two states. When we come to animate pathData one of the rules is that we need to match both the number and types of commands at any given position. We’ll look at this in more detail shortly.

The next thing that we need is the AnimatedStateList which manages the state transitions:

res/drawable/max_min.xml <?xml version="1.0" encoding="utf-8"?> <animated-selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/checked" android:drawable="@drawable/min" android:state_checked="true" /> <item android:id="@+id/unchecked" android:drawable="@drawable/max" /> <transition android:drawable="@drawable/max_to_min" android:fromId="@id/unchecked" android:toId="@id/checked" /> <transition android:drawable="@drawable/min_to_max" android:fromId="@id/checked" android:toId="@id/unchecked" /> </animated-selector> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <? xml version = "1.0" encoding = "utf-8" ?> < animated - selector xmlns : android = "http://schemas.android.com/apk/res/android" > < item android : id = "@+id/checked" android : drawable = "@drawable/min" android : state_checked = "true" / > < item android : id = "@+id/unchecked" android : drawable = "@drawable/max" / > < transition android : drawable = "@drawable/max_to_min" android : fromId = "@id/unchecked" android : toId = "@id/checked" / > < transition android : drawable = "@drawable/min_to_max" android : fromId = "@id/checked" android : toId = "@id/unchecked" / > < / animated - selector >

The two item s define the max and min static states, and the two transition s map animated vector transitions two the transitions between these two states. @drawable/max_to_min is an AnimatedVectorDrawable which maps the animators to the specific path elements in the static drawable:

res/drawable/max_to_min.xml <?xml version="1.0" encoding="utf-8"?> <animated-vector xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@drawable/max"> <target android:animation="@animator/max_min_top_left" android:name="top_left" /> . . . </animated-vector> 1 2 3 4 5 6 7 8 9 10 11 <? xml version = "1.0" encoding = "utf-8" ?> < animated - vector xmlns : android = "http://schemas.android.com/apk/res/android" android : drawable = "@drawable/max" > < target android : animation = "@animator/max_min_top_left" android : name = "top_left" / > . . . < / animated - vector >

This maps an animator named max_min_top_left to the path element we’ve been looking at:

res/animator/max_min_top_left.xml <?xml version="1.0" encoding="utf-8"?> <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" android:duration="@android:integer/config_shortAnimTime" android:interpolator="@android:interpolator/decelerate_quad" android:propertyName="pathData" android:valueFrom="M24,8 L8,8 L8,24" android:valueTo="M24,8 L24,24 L8,24" android:valueType="pathType" /> 1 2 3 4 5 6 7 8 <? xml version = "1.0" encoding = "utf-8" ?> < objectAnimator xmlns : android = "http://schemas.android.com/apk/res/android" android : duration = "@android:integer/config_shortAnimTime" android : interpolator = "@android:interpolator/decelerate_quad" android : propertyName = "pathData" android : valueFrom = "M24,8 L8,8 L8,24" android : valueTo = "M24,8 L24,24 L8,24" android : valueType = "pathType" / >

This performs a path animation between two distinct paths. It is this particular animator that requires the types of each command in the valueFrom attribute to match those in the same position in the valueTo attribute. The animator is then able to interpolate the values of each path command. So the first and last command will not change and those parts of the path rendered throughout the animation will not change. But the change of the centre point means that the centre point of the path will move from coordinates 8,8 to 24,24 when this animator is run. If you now focus on the top left corner in the animated gif, you will see that the ends of the path segment remain static, and the point where they meet moves.

Hopefully this make it clear why we need to use L commands in these paths. While these lines are either horizontal or vertical in the static vector states, they are actually diagonal during the animation. So using V or H commands here would make it impossible to animate them.

The reverse of this animator is used for the min to max animation:

res/animator/min_max_top_left.xml <?xml version="1.0" encoding="utf-8"?> <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" android:duration="@android:integer/config_shortAnimTime" android:interpolator="@android:interpolator/decelerate_quad" android:propertyName="pathData" android:valueFrom="M24,8 L24,24 L8,24" android:valueTo="M24,8 L8,8 L8,24" android:valueType="pathType" /> 1 2 3 4 5 6 7 8 <? xml version = "1.0" encoding = "utf-8" ?> < objectAnimator xmlns : android = "http://schemas.android.com/apk/res/android" android : duration = "@android:integer/config_shortAnimTime" android : interpolator = "@android:interpolator/decelerate_quad" android : propertyName = "pathData" android : valueFrom = "M24,8 L24,24 L8,24" android : valueTo = "M24,8 L8,8 L8,24" android : valueType = "pathType" / >

This is identical to the previous animator except that the valueFrom and valueTo paths have been swapped.

The remaining corners are all done using the exact same techniques, only the precise paths used for the max / min states for each will change.

That gives us the following transition when the view stat transitions from normal to checked and back again:

The source code for this article is available here.

© 2019, Mark Allison. All rights reserved.

Related

Copyright © 2019 Styling Android. All Rights Reserved.

Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.