Android中的theme和css有点像,界面容器和样式是解耦的。在Android中,theme如果声明在application中,是对整个application有效,声明在activity中则只对当前activity有效。

我使用theme的背景需求是:不同的接入方需要使用我们的SDK,然而他们都有各自的主题色,各自的选择框、progressbar等样式。

我的解决方案是将这些不同的需要设置的地方提取为attr,单独移到一个theme文件中。在SDK中提供一份默认theme,接入方如有需求,只需要按照自定义界面需求修改对应的一份同名theme。具体如下:

也许是设置theme的最佳姿势[1]

一、添加如下几个文件

<!--attr.xml-->
<resources>
<attr name="btn_positive_bg" format="reference" />
<attr name="btn_negative_bg" format="reference" />
<attr name="btn_positive_text_color" format="reference|color" />
<attr name="btn_negative_text_color" format="reference|color" />
</resources>
<!--theme.xml-->
<resources>
<style name="TESTAppTheme" parent="...">
<item name="btn_positive_bg">@drawable/dialog_positive_btn_bg</item>
<item name="btn_negative_bg">@drawable/dialog_negative_btn_bg</item>
<item name="btn_positive_text_color">@color/dialog_positive_text_color</item>
<item name="btn_negative_text_color">@color/dialog_negative_text_color</item>

<!--drawable/dialog_positive_btn_bg.xml-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/dialog_positive_btn_bg_pressed" android:state_enabled="true" android:state_pressed="true"></item>
<item android:drawable="@drawable/dialog_positive_btn_bg_normal" android:state_enabled="true" android:state_pressed="false"></item>
<item android:drawable="@drawable/dialog_positive_btn_bg_disabled" android:state_enabled="false"></item>
</selector>

使用方式

<Button
android:background="?attr/btn_positive_bg"
android:textColor="?attr/btn_positive_text_color"
/>

注意:并不能在dialog_positive_btn_bg.xml的drawable里面直接使用?attr/*,在Android sdk 21 之前这样做会弹异常[2]。所以只能在theme中用?attr/*引用一份drawable的res,也就是每一个theme都会对应有一份drawable文件。(会导致drawable文件大量重复,虽然只有color等不一样)

二、调用方式

2.1 在BaseAct 中设置

@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setTheme(R.style.TESTAppTheme)
setContentView(R.layout.base_ly_act);

Dialog这种attach到activity的, 直接在dialog的content layout xml文件中使用?attr/btn_positive_bg无效果。Dialog不能调用setTheme, 当其构造函数传入的context为baseAct的子类时能如下这样手动设置。当然其他能通过context取到theme的地方也可以这样调用。

TypedValue typedValue = new TypedValue();
boolean isAttrFound = context.getTheme().resolveAttribute(R.attr.btn_positive_bg, typedValue, true);
if (isAttrFound){
okBtn.setBackgroundResource(typedValue.resourceId);
}

在TextView中的某个代码片段

final Resources.Theme theme = context.getTheme();
TypedArray a = theme.obtainStyledAttributes(attrs, com.android.internal.R.styleable.TextViewAppearance, defStyle, 0);
int ap = a.getResourceId(com.android.internal.R.styleable.TextViewAppearance_textAppearance, -1);
TypedArray appearance = theme.obtainStyledAttributes(ap, com.android.internal.R.styleable.TextAppearance);
textColor = appearance.getColorStateList(attr);

2.2 在manifest 的application,activity中设置

<application
android:theme="@style/TESTAppTheme">

or

<activity
android:theme="@style/MyDialogTheme" />

这两者的作用是有顺序的,看代码便知:

//ActivityThread.java-performLaunchActivity()
int theme = r.activityInfo.getThemeResource();
if (theme != 0) {
activity.setTheme(theme);
}

三、Dialog、以及已经有了自定义的theme的activity

已经存在自定义theme的activity,及dialog的theme如何setting?
让自定义的theme继承于要设置的theme。然后对于activitity调用setTheme,对于dialog调用

public Dialog(Context context, int theme) {
//......
}

四、theme的调用顺序

  1. 对于在manifest.xml中定义的theme,写在application和activity中的会二选一,如果activity有设置theme,则application中设置的会忽略。
  2. 对于在onCreate中调用的setTheme,则相当于在manifest ActivityTheme / manifest ApplictionTheme 之后又调用了一遍setTheme 。这里有个坑:
<!-- application theme in manifest-->
<style name="AppTheme" parent="@android:style/Theme.Holo.Light">
<item name="android:windowNoTitle">true</item>
<item name="spbStyle">@style/SmoothProgressBar</item>
<item name="android:windowBackground">@color/app_background_color</item>
</style>

<!--activity theme in setTheme()-->
<style name="DialogActTheme" parent="android:Theme.Translucent.NoTitleBar">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
<item name="android:backgroundDimEnabled">true</item>
</style>

即:同时设置了application的theme,设置了windows的相关属性后,如窗体背景色,又在activity中调用setTheme()去覆盖windows的属性,如设置窗体透明,运行会发现setTheme设置的背景透明效果无效,窗体背景变成黑色的了。

结论:如果涉及到window的某些属性,建议写到xml中,不要在setTheme中修改,毕竟xml的优先级更高。重复设置window属性可能会有冲突(原因未知)。

五、theme的继承方式和顺序的区别

  1. parent关键字继承

    <style name="GreenText" parent="@android:style/TextAppearance">
    <item name="android:textColor">#00FF00</item>
    </style>
  2. .的方式继承

    <style name="CodeFont.Red">
    <item name="android:textColor">#FF0000</item>
    </style>

区别:

Note: This technique for inheritance by chaining together names only works for styles defined by your own resources. You can’t inherit Android built-in styles this way. To reference a built-in style, such as TextAppearance, you must use the parent attribute.

注意:这种将theme名字以点链接起来的方式,只适用于你自己定义的styles,对于Android内置的style,则无法work。要想继承内置的style,必须使用parent属性的方式继承。

(注意:在我的实际需求中,SDK被以aar的形式调用,自己本身在theme.xml中写了一份主题作为默认主题,调用方在自己的那份theme.xml中按照需要去覆盖要修改的attr,这里即使某些attr与我们默认提供的一样,也还是得声明一遍。如果使用点继承的方式去theme的话,你永远只能拿到默认的attr,而调用方设置的attr永远不会生效,但是转为parent继承这份theme后则无此问题,说明内部实现对两种继承方式的实现相当于全局变量和局部变量。)


  1. Support for referencing theme attributes in drawable XML

  2. Android Developers, we’ve been using themes all wrong