This post explains how you can create nice animated buttons using existing button widgets, and XML files.
This started out of a fairly simple desire to have some nice animated buttons for a personal project of mine (which I’ll describe in another post). I’m writing this because I ran into enough caveats that I feel it's worthwhile documenting it for the next person.
I’m also somewhat allergic to writing new code unless absolutely necessary. Although it's generally possible to create custom UI elements by extending existing classes in Android, I hate the idea of creating lots of barely extended classes just to tweak your UI.
I ended up stumbling upon some very cool (but not much discussed) functionality that was introduced in Lollipop (Android 5.0, SDK 21):
- VectorDrawable / <vector>
- AnimatedVectorDrawable / <animated-vector>
- AnimatedStateListDrawable / <animated-selector>
- RippleDrawable / <ripple>
These classes can be used to easily animate buttons (and other UI elements). Fortunately for me and my spartan aesthetic, using them requires no code changes whatsoever.
Before getting into the grizzly details, I suggest taking a quick look at Mark Allison’s four part series on Vector Drawables (https://blog.stylingandroid.com/vectordrawables-part-1/) which does a great job of introducing how to use the VectorsDrawable and AnimatedVectorDrawable classes.
Ok, so assuming you now know a little about Vectors and Animated Vector, we can go about creating an animated button.
For simplicity, we're going to use a ToggleButton, since it automatically changes the checked state when pressed, and lets us specify different text for when it’s ON (checked) or OFF (not checked).
We could also use other types of buttons (or UI components) here. Ironically, although already animated and very similar to ToggleButton, we could not have used the Switch widget. The Switch widget has its drawables and animations hardcoded, so we can’t override its background drawable to any great effect. In fact, it seems to be a very good example of how NOT to animate a button (code here) if you want it to be stylable and extensible. It is probably this way for legacy reasons.
Here's the layout for our application:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" tools:context="net.jpuderer.animatedbutton.MainActivity"> <ToggleButton android:layout_width="200dp" android:layout_height="200dp" android:textAllCaps="true" android:textStyle="bold" android:textSize="40sp" android:background="@drawable/circle_button" android:textOn="On" android:textOff="Off"/> </LinearLayout>
The important element above is the background drawable, which defines the way our button looks and animates. Without it, the ToggleButton would look like a normal (albeit large) ToggleButton.
<?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item> <ripple android:color="?android:attr/colorControlHighlight”/> </item> <item> <animated-selector> <item android:id="@+id/off" android:drawable="@drawable/circle_button_off" android:state_checked="false" /> <item android:id="@+id/on" android:drawable="@drawable/circle_button_on" android:state_checked="true" /> <!-- Need to put each animated-vector in its own file, since inlining them exposes a bug in the XML parsing. Boo!!! https://code.google.com/p/android/issues/detail?id=164361 --> <transition android:drawable="@drawable/circle_button_to_on" android:fromId="@id/off" android:toId="@id/on" android:reversible="false"/> <transition android:drawable="@drawable/circle_button_to_off" android:fromId="@id/on" android:toId="@id/off" android:reversible="false"/> </animated-selector> </item> </layer-list>
The top level of our circle_button drawable is <layer-list>. This allows us to layer multiple drawables that respond to state.
- The first layer containing the <ripple> element (which corresponding to the RippleDrawable class), responds to changes of android:state_pressed. It’s the same effect that gets used to animate screen presses for other widgets in Lollipop.
- The second layer contains our <animated-selector> element that contains the drawables for the checked and unchecked states, as well as the animations for transitioning between them.
- We could have put either of these elements at the top level (without using <layer-list>), but then we wouldn’t have an easy way to handle both sets of state changes independently.
Below are the links to the two vector drawables representing the checked and unchecked states. You’ll notice (if you look carefully) that they differ by only a single attribute: android:trimPathEnd
I find it somewhat annoying that there does not appear to be a away to inherit from another drawable. There’s really no good reason that I have to repeat the path information in both circle_button_on.xml and circle_button_off.xml, when they differ by only a single attribute (android:trimPathEnd).
The drawables specified in the <transition> elements are simple enough. The <animated-drawable> element defines which properties we animate, and the animation(s) to apply to those properties:
<?xml version="1.0" encoding="utf-8"?> <animated-vector xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@drawable/circle_button_on"> <target android:name="circle_green" android:animation="@animator/circle_on" /> <!-- I would LOVE to inline the animation here directly (like drawables), instead of creating yet another file that gets referenced in only one place once. I wish I understood how Google determines when you can inline definitions (hint: should be always), or where the rules are documented. --> </animated-vector>
And here is the definition of the animation that gets applied:
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <objectAnimator android:propertyName="trimPathEnd" android:valueFrom="0" android:valueTo="1" android:duration="500" android:valueType="floatType" android:interpolator="@android:interpolator/decelerate_cubic"> </objectAnimator> </set>
The transition in the opposite direction is much the same, so I won’t repeat it here. You can find links to these files here:
Observant readers might be asking themselves at this point, why the repetition? Why can’t Android simply reverse the transition. Well, in theory I think it can, but it seems to be broken, which is why I set android:reversible="false" in the transitions. I think it’s supposed to allow the transition to be played backwards, but when set to true, it causes a number of issues (not the least of which are strange artifacts when rotating the screen). If anyone can figure this out (or point me to the relevant bug number), it would be much appreciated.
You can find the complete code for the above example here: https://github.com/jpuderer/AnimatedButton
Which looks like this:
Which looks like this:
You can also find the code for the more complicated animation (featured at the top of the page) here:
It’s almost identical to the one above (same git project, just a different branch), except that the path and animations have a few more components to them.
For completeness sake I'll mention that you might also have been able to do some of this using a regular StateListDrawable with ObjectPropertyAnimators to animate states, as described here. However, these don't define individual transition between states, which is the benefit of using AnimatedStateListDrawable