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 quite a simple transition between a play and pause state for a media player of some kind. The actual animation technique that we’ll use is not anything that we haven’t used before in this series, but there are a couple of subtleties at work which may not be immediately obvious but are worthy of discussion nonetheless.

Let’s begin by looking at the actual animation. Those that have read the previous posts in this series may be able to guess that we’re performing a path transformation here. The only path that is actually changing is the right hand vertical bar of the “pause” symbol, and this is transitioning in to two of the sides of the triangle which represented the “play” symbol. The key to this is that what appears to be a simple, vertical line inn the pause symbol is actually two line segments:

res/drawable/play_pause_circle_pause_vector.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="circle" android:pathData="M32,8 A24,24 0 1 1 31.99,8 z" android:strokeWidth="5" android:strokeColor="@android:color/white" android:strokeLineCap="round" /> <path android:name="pause_left" android:pathData="M26,23 V41" android:strokeWidth="5" android:strokeColor="@android:color/white" android:strokeLineCap="round" /> <path android:name="pause_right" android:pathData="M37,23 L37,32 L37,41" android:strokeWidth="5" android:strokeColor="@android:color/white" android:strokeLineCap="round" android:strokeLineJoin="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 <? 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 = "circle" android : pathData = "M32,8 A24,24 0 1 1 31.99,8 z" android : strokeWidth = "5" android : strokeColor = "@android:color/white" android : strokeLineCap = "round" / > < path android : name = "pause_left" android : pathData = "M26,23 V41" android : strokeWidth = "5" android : strokeColor = "@android:color/white" android : strokeLineCap = "round" / > < path android : name = "pause_right" android : pathData = "M37,23 L37,32 L37,41" android : strokeWidth = "5" android : strokeColor = "@android:color/white" android : strokeLineCap = "round" android : strokeLineJoin = "round" / > < / vector >

The two line segments for pause_right are drawn using the L (line) command rather than the simpler V (vertical line) command used for pause_left . This is to allow us to transform this path to the one used for the play symbol:

res/drawable/play_pause_circle_play_vector.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="circle" android:pathData="M32,8 A24,24 0 1 1 31.99,8 z" android:strokeWidth="5" android:strokeColor="@android:color/white" android:strokeLineCap="round" /> <path android:name="pause_left" android:pathData="M26,23 V41" android:strokeWidth="5" android:strokeColor="@android:color/white" android:strokeLineCap="round" /> <path android:name="pause_right" android:pathData="M26,23 L41,32 L27,41" android:strokeWidth="5" android:strokeColor="@android:color/white" android:strokeLineCap="round" android:strokeLineJoin="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 <? 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 = "circle" android : pathData = "M32,8 A24,24 0 1 1 31.99,8 z" android : strokeWidth = "5" android : strokeColor = "@android:color/white" android : strokeLineCap = "round" / > < path android : name = "pause_left" android : pathData = "M26,23 V41" android : strokeWidth = "5" android : strokeColor = "@android:color/white" android : strokeLineCap = "round" / > < path android : name = "pause_right" android : pathData = "M26,23 L41,32 L27,41" android : strokeWidth = "5" android : strokeColor = "@android:color/white" android : strokeLineCap = "round" android : strokeLineJoin = "round" / > < / vector >

We tie the animation to the checked state of the drawable using an animated selector:

res/drawable/play_pause_circle.xml <?xml version="1.0" encoding="utf-8"?> <animated-selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/play" android:drawable="@drawable/play_pause_circle_play_vector" android:state_checked="true" /> <item android:id="@+id/paused" android:drawable="@drawable/play_pause_circle_pause_vector" android:state_checked="false" /> <transition android:drawable="@drawable/pause_to_play" android:fromId="@id/paused" android:toId="@id/play" /> <transition android:drawable="@drawable/play_to_pause" android:fromId="@id/play" android:toId="@id/paused" /> </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/play" android : drawable = "@drawable/play_pause_circle_play_vector" android : state_checked = "true" / > < item android : id = "@+id/paused" android : drawable = "@drawable/play_pause_circle_pause_vector" android : state_checked = "false" / > < transition android : drawable = "@drawable/pause_to_play" android : fromId = "@id/paused" android : toId = "@id/play" / > < transition android : drawable = "@drawable/play_to_pause" android : fromId = "@id/play" android : toId = "@id/paused" / > < / animated - selector >

The actual animation perform the pathData animation on pause_right between the two states:

res/drawable/pause_to_play.xml <?xml version="1.0" encoding="utf-8"?> <animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt" android:drawable="@drawable/play_pause_circle_pause_vector"> <target android:name="pause_right"> <aapt:attr name="android:animation"> <objectAnimator android:duration="100" android:interpolator="@android:interpolator/accelerate_decelerate" android:propertyName="pathData" android:valueFrom="M37,23 L37,32 L37,41" android:valueTo="M26,23 L41,32 L27,41" android:valueType="pathType" /> </aapt:attr> </target> </animated-vector> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <? xml version = "1.0" encoding = "utf-8" ?> < animated - vector xmlns : android = "http://schemas.android.com/apk/res/android" xmlns : aapt = "http://schemas.android.com/aapt" android : drawable = "@drawable/play_pause_circle_pause_vector" > < target android : name = "pause_right" > < aapt : attr name = "android:animation" > < objectAnimator android : duration = "100" android : interpolator = "@android:interpolator/accelerate_decelerate" android : propertyName = "pathData" android : valueFrom = "M37,23 L37,32 L37,41" android : valueTo = "M26,23 L41,32 L27,41" android : valueType = "pathType" / > < / aapt : attr > < / target > < / animated - vector >

I’ve only included on of the animators here – the other is identical except the valueFrom and valueTo strings are reversed but it’s available in the project source for those that want to see it.

That’s the animation working, but there are some things worth mentioning. The first is the animation duration. While this may seem obvious this can be an easy one to get wrong because this animation needs to run quickly. Take a look at the animator we just defined and you’ll see that I opted for a duration of 100ms. That may seem very short, but I feel it is absolutely necessary in this case because of the nature of what this transition represents. This is obviously toggling media playback between a play and paused state and the transition of the media playback will happen pretty instantly. (This may not be the case for streaming playback where buffering may be necessary before playback commences, but that would require a third “buffering” state on the icon for a good UX).

If the animation were to take too long to run, then the state of the visual indication will not be synchronised with the state of the media playback, and this will actually fell disconnected to the user. The UI state should match the playback state in order for the experience to feel right, and having a very quick animation will add fluidity to the transition. In this case 100ms is not a noticeable lag for the user, but having a longer animation duration may make it feel less in sync with the actual playback state.

The other little subtlety is with the mid point of the line defined in pause_right . While it is obvious that the top and bottom points of the line move horizontally during the transition, the mid point actually moves by a much smaller amount in the opposite direction. The reason for this is that the pause symbol is vertically symmetrical, the play symbol is not, and if we keep the mid point of the line static between the two states, the position of the triangle will actually appear to be slightly offset to the left within the circle. Mathematically this is not actually the case, but the narrower right side of the triangle will create the optical illusion that it is because the right side of the triangle has more white space around it inside the circle.

To counter this, the right-most vertex in the play shape is actually 4 units to the right of the centre point of the pause line. The left edge both the play and pause symbols remains constant, but the change to the right edge counters the optical illusion which occurs for the triangle, but not for the vertical line.

If you now go back and study the animation at the start of this article, it is possible to see this movement by focusing of the relevant point but it’s still pretty subtle. The larger movement of the top and bottom points actually helps hide the much smaller movement of the mid point. This can be quite a useful technique to understand because it might feel like doing something like this will appear unnatural, but masking the small movement with a larger movement is surprisingly deceptive.

The key thing here is that everything appears natural. If we didn’t include this small movement, the triangle would feel slightly off-centre, but with this in place it just feels much smoother and more natural, I think.

The source code for this article is available here.

© 2020, Mark Allison. All rights reserved.

Related

Copyright © 2020 Styling Android. All Rights Reserved.

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