Custom Drawable
Drawing actions without creating an extra layer in layout XMLs.
I love our new designs! Recently I’ve been working on user interactions. One of them is presented on the GIF above. I wanted to create a custom Drawable, so I can set a background on any view I want without wrapping it with some custom ViewGroup. An extra layer means extra calculations.
I couldn’t find any guide nor article that would help me create a Drawable object that I would be able to set as a background via XML. Like a Ripple Drawable. Unfortunately if someone counts that in this article I solved that mistery I’ll heads up a little — I didn’t. I looked though how RippleDrawable is defined and I prepared my Drawable classes based on what I saw.
Divide and Counquer
All advanced animations look complicated until we find a way to disassemble them. What animations are present on this GIF?
There are three:
- Rounded Rectangle Bounce
- Alpha Mask Appearance
- Long-Click Filling Color
How a Ripple knows when animate? That was my first question. When I think about a View it comes to my mind onClickListener or onTouchEvent. Drawable doesn’t have such methods. It has a method named onStateChanged like this:
Rounded Rectangle Bounce
That was a starting point. I reflected this method in my class, removing the irrelevant states:
So first step is to override a method isStateful. Without that, the code from onStateChanged will never be triggered.
Next, I wanted to start the animation on finger released. Otherwise long click would start with a bouncing Drawable. The condition to start was: is enabled and was pressed but is not pressed anymore.
setClickActive(enabled && wasPressed && !pressed)
To start an animation I used an ObjectAnimator and took advantage of float type by starting a value from 0.99 and finishing on 1. This cheats an eye enough to see a fluent movement and I get an end-value different from start-value which helps me differentiate if the anim is in run without extra flags.
Alpha Mask Appearance
So the rectangle shrinks, but the animation also has a transparent mask showing on top of. The mask must be fully transparent at first and fade-in and out within the same time as previous animation.
This code is not a final touch but the POC that I went out of. Anyone may feel free to do the same thing.
Also, keep in mind that an ObjectAnimator uses reflection to trigger set methods, so always add a @Keep annotation, to prevent ProGuard from minifying them.
Using Custom Drawable
As I mentioned in the beginning of the article I wasn’t able to add a background drawable in a layout resource file, so I had to do it programmatically like this:
Cascade usage of drawables was possible only because of deriving from LayerDrawable class which is a well known <layer-list> node in drawable XMLs.
Long-Click Filling Color
I have a nice bouncing animation now that I can add to any of my Views. There’s only one more animation left to be done. Filling a color on long click.
I know how to react to states of the Drawable. The only difference is the start of the animation. I wouldn’t like the color to start filling right after I press a View, because I would loose that experience that, these are in fact, two different actions. There’s an easy way though. Object animator has a property startDelay that I experimented with to get that natural distinction between a simple click and a long click. To me 100ms is enough:
Conclusion
It’s possible and not that hard to create a custom Drawable. But is it worth it? Is it better than creating a custom ViewGroup? Every solution in programming has its pros and cons. It’s always our own decision which way we choose.
A custom Drawable:
- Saves calculations of positioning
- Doesn’t require to change all XMLs by adding a surrounding ViewGroup
- Supports only limited number of states, that are already recognized by a View and passed to a Drawable
A custom ViewGroup:
- Custom ViewGroup on the other hand can be used in an XML and a background Drawable can be added there rather than from the code
- With custom ViewGroup you have more control by overriding onTouchEvent whereas custom Drawable only reacts to states
There must be a reason why Google doesn’t let us create a custom Drawable easily. There are no tutorials, no option to create a custom node like <ripple> has. Maybe it’s because it’s not the best choice? Or maybe soon Google give us a tool to do just that?