Styling Colors & Drawables w/ Theme Attributes
You’ve probably noticed that when you write something like:
context.getResources().getColor(R.color.some_color_resource_id);
Android Studio will give you a lint message warning you that the
Resources#getColor(int)
method was deprecated in Marshmallow in favor of the
new, Theme
-aware Resources#getColor(int, Theme)
method. You also
probably know by now that the easy alternative to avoiding this lint warning
these days is to call:
ContextCompat.getColor(context, R.color.some_color_resource_id);
which under-the-hood is essentially just a shorthand way of writing:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return context.getResources().getColor(id, context.getTheme());
} else {
return context.getResources().getColor(id);
}
Easy enough. But what is actually going on here? Why were these methods
deprecated in the first place and what do the new Theme
-aware methods have to
offer that didn’t exist before?
The problem with Resources#getColor(int)
& Resources#getColorStateList(int)
First, let’s be clear on what these old, deprecated methods actually do:
-
Resources#getColor(int)
returns the color associated with the passed in color resource ID. If the resource ID points to aColorStateList
, the method will return theColorStateList
’s default color. -
Resources#getColorStateList(int)
returns theColorStateList
associated with the passed in resource ID.
“When will these two methods break my code?”
To understand why these methods were deprecated in the first place, consider the
ColorStateList
declared in XML below. When this ColorStateList
is applied to a
TextView
, its disabled and enabled text colors should take on the colors pointed to by the
R.attr.colorAccent
and R.attr.colorPrimary
theme attributes respectively:
<!-- res/colors/button_text_csl.xml -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorAccent" android:state_enabled="false"/>
<item android:color="?attr/colorPrimary"/>
</selector>
Now let’s say you want to obtain an instance of this ColorStateList
programatically:
ColorStateList csl = context.getResources().getColorStateList(R.color.button_text_csl);
Perhaps surprisingly, the result of the above call is undefined! You’ll see a stack trace in your logcat output similar to the one below:
W/Resources: ColorStateList color/button_text_csl has unresolved theme attributes!
Consider using Resources.getColorStateList(int, Theme)
or Context.getColorStateList(int)
at android.content.res.Resources.getColorStateList(Resources.java:1011)
...
“What went wrong?”
The problem is that Resources
objects are not intrinsically linked to a specific
Theme
in your app, and as a result, they will be unable to resolve the values pointed to by
theme attributes such as R.attr.colorAccent
and R.attr.colorPrimary
on their own. In fact,
specifying theme attributes in ColorStateList
XML files was not supported until API 23,
which introduced two new methods for extracting ColorStateList
s from XML:
-
Resources#getColor(int, Theme)
returns the color associated with the passed in resource ID. If the resource ID points to aColorStateList
, the method will return theColorStateList
’s default color. Any theme attributes specified in theColorStateList
will be resolved using the passed inTheme
argument. -
Resources#getColorStateList(int, Theme)
returns theColorStateList
associated with the passed in resource ID. Any theme attributes specified in theColorStateList
will be resolved using the passed inTheme
argument.
Additional convenience methods were also added to Context
and to the support
library’s ResourcesCompat
and ContextCompat
classes as well.
“That stinks! How can I workaround these problems?”
As of v24.0 of the AppCompat support library, you can now workaround all of these
problems using the new AppCompatResources
class! To extract a themed ColorStateList
from XML, just use:
ColorStateList csl = AppCompatResources.getColorStateList(context, R.color.button_text_csl);
On API 23+, AppCompat will delegate the call to the corresponding framework method,
and on earlier platforms it will manually parse the XML itself, resolving any
theme attributes it encounters along the way. If that isn’t enough, it also
backports the ColorStateList
’s new
android:alpha
attribute as well (which was previously only available to devices running API 23 and above)!
The problem with Resources#getDrawable(int)
You guessed it! The recently deprecated Resources#getDrawable(int)
method shares
pretty much the exact same problem as the Resources#getColor(int)
and
Resources#getColorStateList(int)
methods discussed above.
As a result, theme attributes in
drawable XML files will not resolve properly prior to API 21, so if
your app supports pre-Lollipop devices, either avoid theme attributes entirely
or resolve them in your Java code and construct the Drawable
programatically instead.
“I don’t believe you! Are there really no exceptions?”
Of course there is an exception, isn’t there always? :)
It turns out that similar to the AppCompatResources
class, the VectorDrawableCompat
and AnimatedVectorDrawableCompat
classes were able to workaround
these issues and are actually smart enough to resolve the theme
attributes it detects in XML across all platform versions as well. For example,
if you want to color your VectorDrawableCompat
the standard shade of grey,
you can reliably tint the drawable with ?attr/colorControlNormal
while still maintaining backwards compatibility with older platform versions:
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:pathData="..."
android:fillColor="@android:color/white"/>
</vector>
(If you’re curious how this is implemented under-the-hood, the short answer is that
the support library does their own custom XML parsing and uses the
Theme#obtainStyledAttributes(AttributeSet, int[], int, int)
method to resolve the theme attributes it encounters. Pretty cool!)
Pop quiz!
Let’s test our knowledge with a short example. Consider the following ColorStateList
:
<!-- res/colors/button_text_csl.xml -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorAccent" android:state_enabled="false"/>
<item android:color="?attr/colorPrimary"/>
</selector>
And assume you’re writing an app that declares the following themes:
<!-- res/values/themes.xml -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="colorPrimary">@color/vanillared500</item>
<item name="colorPrimaryDark">@color/vanillared700</item>
<item name="colorAccent">@color/googgreen500</item>
</style>
<style name="CustomButtonTheme" parent="ThemeOverlay.AppCompat.Light">
<item name="colorPrimary">@color/brown500</item>
<item name="colorAccent">@color/yellow900</item>
</style>
And finally, assume that you have the following helper methods to resolve theme
attributes and construct ColorStateList
s programatically:
@ColorInt
private static int getThemeAttrColor(Context context, @AttrRes int colorAttr) {
TypedArray array = context.obtainStyledAttributes(null, new int[]{colorAttr});
try {
return array.getColor(0, 0);
} finally {
array.recycle();
}
}
private static ColorStateList createColorStateList(Context context) {
return new ColorStateList(
new int[][]{
new int[]{-android.R.attr.state_enabled}, // Disabled state.
StateSet.WILD_CARD, // Enabled state.
},
new int[]{
getThemeAttrColor(context, R.attr.colorAccent), // Disabled state.
getThemeAttrColor(context, R.attr.colorPrimary), // Enabled state.
});
}
Try to see if you can predict the enabled and disabled appearance of a button on
both an API 19 and API 23 device in each of the following scenarios (for
scenarios #5 and #8, assume that the button has been given a custom theme in XML
using android:theme="@style/CustomButtonTheme"
):
Resources res = ctx.getResources();
// (1)
int deprecatedTextColor = res.getColor(R.color.button_text_csl);
button1.setTextColor(deprecatedTextColor);
// (2)
ColorStateList deprecatedTextCsl = res.getColorStateList(R.color.button_text_csl);
button2.setTextColor(deprecatedTextCsl);
// (3)
int textColorXml =
AppCompatResources.getColorStateList(ctx, R.color.button_text_csl).getDefaultColor();
button3.setTextColor(textColorXml);
// (4)
ColorStateList textCslXml = AppCompatResources.getColorStateList(ctx, R.color.button_text_csl);
button4.setTextColor(textCslXml);
// (5)
Context themedCtx = button5.getContext();
ColorStateList textCslXmlWithCustomTheme =
AppCompatResources.getColorStateList(themedCtx, R.color.button_text_csl);
button5.setTextColor(textCslXmlWithCustomTheme);
// (6)
int textColorJava = getThemeAttrColor(ctx, R.attr.colorPrimary);
button6.setTextColor(textColorJava);
// (7)
ColorStateList textCslJava = createColorStateList(ctx);
button7.setTextColor(textCslJava);
// (8)
Context themedCtx = button8.getContext();
ColorStateList textCslJavaWithCustomTheme = createColorStateList(themedCtx);
button8.setTextColor(textCslJavaWithCustomTheme);
Solutions
Here are the screenshots of what the buttons look like on API 19 vs. API 23 devices:
Note that there isn’t anything special about the weird pink color in the two
screenshots. That’s just the “undefined behavior” that results when you try to
resolve a theme attribute without a corresponding Theme
. :)
As always, thanks for reading! Feel free to leave a comment if you have any questions, and don’t forget to +1 and/or share this blog post if you found it helpful! And check out the source code for these examples on GitHub as well!