4

Decided to try the new DrawableCompat class. Following instructions from a reliable source, I'm calling:

Button b = (Button) findViewById(R.id.button);
Drawable d = b.getBackground();
d = DrawableCompat.wrap(d);
DrawableCompat.setTintList(d, getResources().getColorStateList(...));

Surprisingly, this does not work: my button background gets the color I define for the un-pressed, un-focused state, but it doesn't change on press / on focus.

I was able to succeed in a totally different way,

Button b = (Button) findViewById(R.id.button);
AppCompatButton b2 = (AppCompatButton) b; //direct casting to AppCompatButton throws annoying warning
b2.setSupportBackgroundTintList(getResources().getColorStateList(...));

which works and is even more compact, but however I wanted to use DrawableCompat instead. Could you tell me why is it?

natario
  • 24,954
  • 17
  • 88
  • 158

1 Answers1

4

d = DrawableCompat.wrap(d); creates a new instance if it's not already DrawableWrapper so you tint this new instance but the original which is stored in the button remains the same.

The whole code would look something like this

Button b = (Button) findViewById(R.id.button);
Drawable d = b.getBackground();
d = DrawableCompat.wrap(d);
DrawableCompat.setTintList(d, getResources().getColorStateList(...));
b.setBackground(d); // or setBackgroundDrawable on older platforms

So yeah, I'd go with the second approach you described because it abstracts the hard work from you.

EDIT:

Just took a dive into appcompat code and found out that the AppCompatButton tints iself and not the drawable unlike Lollipop native (but only if the background is on the whitelist, e.g. default appcompat button drawable). So you have to clear tint from the button itself first.

Button b = (Button) findViewById(R.id.button);

if (b instanceof AppCompatButton) {
    ((AppCompatButton)b).setSupportBackgroundTintList(null);
}

Drawable d = b.getBackground();
d = DrawableCompat.wrap(d);
DrawableCompat.setTintList(d, getResources().getColorStateList(...));
b.setBackground(d); // or setBackgroundDrawable on older platforms

EDIT 2:

The above code will throw a NullPointerException when you try to reset the button's tint list. I'm currently filing a bug report.

In the meantime I suggest you inflate the button with a custom background (non-whitelisted for tinting by appcompat) directly or with @null background and resolving the default button background by

TypedArray ta = context.obtainStyledAttributes(null, new int[]{android.R.attr.background}, R.attr.buttonStyle, R.style.Widget_AppCompat_Button);
Drawable d = ta.getDrawable(0);
ta.recycle();

Final solution

So as all this looks pretty fugly, the easiest (and only working and foolproof, yet hidious as well) solution for you now is this:

Button b = (Button) findViewById(R.id.button);
ColorStateList c = getResources().getColorStateList(...);
Drawable d = b.getBackground();
if (b instanceof AppCompatButton) {
    // appcompat button replaces tint of its drawable background
    ((AppCompatButton)b).setSupportBackgroundTintList(c);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    // Lollipop button replaces tint of its drawable background
    // however it is not equal to d.setTintList(c)
    b.setBackgroundTintList(c);
} else {
    // this should only happen if 
    // * manually creating a Button instead of AppCompatButton
    // * LayoutInflater did not translate a Button to AppCompatButton
    d = DrawableCompat.wrap(d);
    DrawableCompat.setTintList(d, c);
    b.setBackgroundDrawable(d);
}

You should put this monstrosity away in a utility class.

Eugen Pechanec
  • 37,669
  • 7
  • 103
  • 124
  • 1
    Does not work here! Nor does `b.setBackground(DrawableCompat.unwrap(d))`, fairly. – natario May 16 '15 at 16:01
  • Also, with my second approach, casting to `AppCompatButton` would dramatically fail on API21+, while `DrawableCompat` should not. – natario May 16 '15 at 16:04
  • @mvai I found some more errors but I managed to put together a solution that should work reliably. – Eugen Pechanec May 16 '15 at 17:42
  • Waited for some official information about why `DrawableCompat` doesn't work, but got none. However, from my tests I can say that casting to `AppCompatButton` does *not* (weird!) crash on API21+. If you can confirm that, the first check is unnecessary. Nice catch on `AppCompatButton` being created programmatically, but the last three lines do not work here. My advice for now would be to always use my approach (`setSupportBackgroundTintList()`). – natario May 21 '15 at 11:31
  • @mvai Thanks, i didn't realize that! Yes, the appcompat layout inflater will inflate `AppCompatButton` instead of `Button` on all platforms. The code after that is just in case the layout inflater did not work for some reason and a regular `Button` was inflated. To make it really foolproof, you see? – Eugen Pechanec May 21 '15 at 11:42