[译]使用Android Theme属性进行个性化

时间:2023-02-05 11:22:39

原文地址——Styling Colors & Drawables w/ Theme Attributes

你也许注意到

context.getResources().getColor(R.color.some_color_resource_id);

AndroidStudio会提示Resources#getColor(int)方法在Marshmallow 版本已经过时了,可以使用 Resources#getColor(int, Theme)来代替。
你也许知道最简单的处理方法是调用:

ContextCompat.getColor(context, R.color.some_color_resource_id);

这个方法其实是下面方法的简单写法

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return context.getResources().getColor(id, context.getTheme());
} else {
return context.getResources().getColor(id);
}

很简单,但它的内部原理是什么,为什么这个方法会过时,为什么Theme参数之前不需要?

Resources#getColor(int) & Resources#getColorStateList(int) 的问题

首先,让我们来研究老方法内部做了什么。
- Resources#getColor(int)
返回一个于颜色ID对应的颜色值。如果这个ID指向一个ColorStateList,方法会返回一个默认色值;
- Resources#getColorStateList(int)
返回一个ID指向的ColorStateList

代码什么时候会报错呢

为了明白方法过时的原因,考虑一下ColorStateList是使用以下xml文件来声明的。当这个xml应用到TextView上时,就使字体颜色指向了R.attr.colorAccentR.attr.colorPrimary的主题色。

<!-- 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>

假设现在在代码中获取ColorStateList

ColorStateList csl = context.getResources().getColorStateList(R.color.button_text_csl);

出人意料的是,错误出现了!

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)
...
哪里出错?

问题在于Resource并不与应用中特定的Theme绑定,所以像R.attr.colorAccentR.attr.colorPrimary就无法从Theme中获取到指定的颜色。事实上,直到API 23,才支持在ColorStateList中指定主题属性,使用下面两个方法可以实现:
- Resources#getColor(int, Theme)
返回ID指定的色值,如果ID指向的是ColorStateList,方法会返回一个默认色值,任何主题属性都会从主题参数中获取;
- Resources#getColorStateList(int, Theme)
返回一个ID指向的ColorStateList,任何主题属性都会从主题参数中获取;

更方便的方法可以通过support library中的ResourcesCompat
和 ContextCompat
来获取。

如何更优雅地解决问题

v24.0AppCompt Support Library,可以通过使用 AppCompatResources
 来解决这个问题

ColorStateList csl = AppCompatResources.getColorStateList(context, R.color.button_text_csl);

Api23+AppCompat 会委托对应的框架来实现,更早的版本会手动解析xml文件,解析所有的主题属性。如果还不够,ColorStateList中的 android:alpha
已经可以由低版本使用了(之前只能在API 23+上使用)

 Resources#getDrawable(int)的问题

你猜对了。已过时的方法Resources#getDrawable(int)
Resources#getColor(int)Resources#getColorStateList(int)一样有同样的问题——直到API 21+xml文件中才支持主题属性。所以,如果你想支持Lolipop之前的版本,需要避免主题属性或者动态构造一个Drawable对象。

我不相信你,真的没有例外?

总是有例外的。
AppCompatResources类似, VectorDrawableCompat
 和AnimatedVectorDrawableCompat
可以解析主题属性,例如你想把VectorDrawableCompat变成标准的灰色,可以使用android:tint=?attr/colorControlNormal,在老版本上也可以使用。

<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>

原理在于,在support库中解析xml中的主题属性是使用Theme#obtainStyledAttributes(AttributeSet, int[], int, int)
,很酷吧。

突击考试

我们用上面的知识做个简单的测试,有如下的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>

假设声明主题如下:

<!-- 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>

最后假设你使用上述方法解析主题属性动态构造ColorStateList

@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.
});
}

试试预测在API 23API 19下按钮enabledisable的外观(例如,在#5和#8中,按钮使用了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);
答案

[译]使用Android Theme属性进行个性化

[译]使用Android Theme属性进行个性化

注意在截屏中的粉色并不奇怪,而是当未指定主题时,默认加载默认主题的效果。

与往常一样,感谢阅读。如果有任何问题请评论,Github源码地址