Update: This can now be accomplished with DrawableCompat that is now available in the Support Library. Use the DrawableCompat#setTintList() method to accomplish the same thing as shown below.
One of the things that used to drive me (and my designer co-workers) crazy was that we needed to create a full stack of various PNG’s to create different states on our drawables. That means if I wanted a button with a white default state, orange pressed state and grey disabled state I’d have to create three PNG’s (for each density) … which … as you know is a huge number of PNG’s and a real pain to update when the time comes.
One Png To Rule Them All
I’m not the first to come up with this solution, but I figured its a good time to share it so its out there. This is not a one size fits all solution (see the conclusion at the end for more info), but it does give you a good jumping point and if you’re building state list drawables. You can see how this would help you simplify some of your icons by only having to create one.*
With this solution we have one PNG and then we use some Java code to create a StateListDrawable at runtime. This allows us to have one default PNG and then change the colors at runtime with just code.
Place the code below in a file called DrawableUtil.java and put it somewhere in your project.
public class DrawableUtil { public static StateListDrawable getStateListDrawable(Context context, @DrawableRes int imageResource, @ColorRes int desiredColor, @IntRange(from = 0, to = 255) int disableAlpha) { // Create the colorized image (pressed state) Bitmap one = BitmapFactory.decodeResource(context.getResources(), imageResource); Bitmap oneCopy = Bitmap.createBitmap(one.getWidth(), one.getHeight(), Bitmap.Config.ARGB_8888); Canvas c = new Canvas(oneCopy); Paint p = new Paint(); int color = context.getResources().getColor(desiredColor); p.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); c.drawBitmap(one, 0, 0, p); // Create the disabled bitmap for the disabled state Bitmap disabled = BitmapFactory.decodeResource(context.getResources(),imageResource); Bitmap disabledCopy = Bitmap.createBitmap(disabled.getWidth(), disabled.getHeight(), Bitmap.Config.ARGB_8888); Canvas disabledCanvas = new Canvas(disabledCopy);; Paint alphaPaint = new Paint(); alphaPaint.setAlpha(disableAlpha); disabledCanvas.drawBitmap(disabled, 0, 0, alphaPaint); StateListDrawable stateListDrawable = new StateListDrawable(); // Pressed State stateListDrawable.addState(new int[]{android.R.attr.state_pressed}, new BitmapDrawable(oneCopy)); // Disabled State stateListDrawable.addState(new int[]{-android.R.attr.state_enabled}, new BitmapDrawable(disabledCopy) ); // - symbol means opposite, in this case "disabled" // Default State stateListDrawable.addState(new int[]{}, context.getResources().getDrawable(imageResource)); return stateListDrawable; } }
The code above creatse a StateListDrawable that will return a Drawable that has three states:
- Default – The default image (imageResource)
- Pressed – The default image, colored with the color param (desiredColor)
- Disabled – The disabled state image is the default image (above) but has the opacity set to the value that is passed in (disableAlpha)
How to Use It
Simply call the static method with the required parameters and it will return a StateListDrawable that you can use to set the background of any image that can have state (like a Button, ImageView, etc).
myImageView.setBackground(DrawableUtil.getStateListDrawable(context, R.drawable.ic_user_dark, R.color.white, 127)); // 127 = 50% in 0…255 alpha
Now, if for some reason you want to change the color of the pressed state, simply change the color value that is passed in – say changing R.color.white to R.color.red and have the new selected image be red. All done with a simple code change.
If you want to get advanced you could use the Pallete lirbary to help get your colors and colorize your icons based upon the theme of the image that is composing the screen, the PocketCasts team does a great job of this in their player.
Here’s what it looks like if we use a user icon, set the pressed color state to red and the disabled to 90.
Why Did I Use This
There are a couple of other support library implementations that we could have used (shown below). The reason why this implmentation was used over the others is because I wanted to keep the default state of the PNG intact. What do I mean? I wanted to use a PNG that looked like this as the default state (not pressed, nor disabled):
When pressed though, I wanted the image to look like this:
Other Implementations such as DrawableCompat.setTintList() would not allow me to keep the original drawable.
Other Implementations
On the /r/androiddev comments for this article it was brought up that you can use DrawableCompat to wrap and set the tint list on the drawable. This is correct, somewhat. A problem occurs when you want to perform what I set out to do above – keep the original drawable but tint the other states. If you don’t care about keeping the original color (or simply want to change it anyway) you can use the DrawableCompat with great success like this:
Drawable logoDrawable = getResources().getDrawable(R.drawable.ic_agilevent_logo); Drawable tintableDrawable = DrawableCompat.wrap(logoDrawable); DrawableCompat.setTintList(tintableDrawable, getResources().getColorStateList(R.color.logo_color_selector)); DrawableCompat.setTintMode(tintableDrawable, PorterDuff.Mode.SRC_IN); myImageView.setBackground(tintableDrawable);
The R.color.logo_color_selector looks like this:
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true" android:color="@color/red" /> <item android:state_enabled="false" android:color="@color/green" /> <item android:color="@color/blue" /> </selector>
Conclusion*
The new material design support libs out there you can do some of the same things I’m showing above, but some have still opted for this solution as they’re not 100% material (or not going material at all).
This is one of many solutions to a very common problem. It does not mean that you have to use this, in fact if you’re using v21+ you can use android:tint attribute to colorize android pngs. You can also use the android colorFilter to do the same thing we’re doing above. Like I said, there are a few ways to do this, but this is one way where you create advanced state list drawable in code.
Lastly, there is also a TintImageView that is present in the android.support.v7.internal.widget.TintImageView that has very similar settings. The code to use it looks like this:
<android.support.v7.internal.widget.TintImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/ic_icon" android:backgroundTint="@color/green" android:backgroundTintMode="src_over"/>
However, I would advise against using this as this is inside of the internal package inside of android.support.v7. Traditionally anything inside of an internal package name indicates that it is not a public API that should be consumed or relied upon. Choose to use it at your own risk. 🙂
Gonçalo Ferreira says
Hi Donn,
Nice, i did something similar using the colorFilter you specifiy in the conclusion applying a black/white filter but only on pressed states.
Here it is for anyone who finds it useful:
https://gist.github.com/monxalo/3ef606a54207c8eb7429
walmyrcarvalho says
Very handy tip, thanks for sharing it! Just a question: Using this implementation, do I need to use an asset for each density? I mean, could I make this to resize the biggest asset according to smaller density?
Thanks!
Ian Lake says
One thing to note is that there is now a public AppCompatImageView which was added in version 23.1 (although if you use ImageView in your XML files and AppCompatActivity, they’ll get replaced with this class automatically): http://developer.android.com/reference/android/support/v7/widget/AppCompatImageView.html