一、概述
本换肤框架由第三方换肤框架Android-Skin-Support(点击查看)修改而来。
因为Android-Skin-Support框架主要面向的是第三方app的开发,不支持Android系统级的换肤,所以本换肤框架对其做了相应的修改,以满足系统级换肤的需求。
下面简述一下实现原理:
1、如何找到皮肤对应的资源id
例如当前有default和theme1两套皮肤,以给Button设置default皮肤的背景图片为**R,drawable.bg**为例,其换肤原理就是:
通过该资源id的值找到对应的资源名“bg”,然后加上后缀后组成theme1皮肤的资源名“bg_theme1”,再通过新的资源名 “bg_theme1” 来找到其对应的资源id。
2、如何通知各个应用切换皮肤
皮肤名记录在原生Settings数据库中(老版本通过系统数据库变化来监听皮肤变化,但实际项目中发现此变化通知的不及时)。切换皮肤时,系统会通过UiModeManager.SkinChangedCallback#onSkinChanged(String skinName)方法,通知各个有换肤需求的应用是通过监听该数据库字段的变化来切换到对应的皮肤的。
二、使用
应用Demo放在公共盘: tspcdsvr/public/9_Users/RaoBin/换肤框架Demo
本换肤框架直接编译进Android Framework,所以可直接通过定制SDK调用,不需要引用单独的换肤框架库。基本使用步骤如下:
1、继承SkinCompatApplication
有换肤需求的应用,需要创建Application类并继承skin.support.app.SkinCompatApplication。
2、定义各个皮肤的资源
例如现在有名为默认(default)和theme1的两种皮肤,default皮肤中有图片资源和颜色资源分别命名为为bg.jpeg和color_bg,那么theme1皮肤中对应的资源就应该分别命名为bg_theme1.jpeg和color_bg_theme1。
并建立res和res-theme1两个资源文件夹,default皮肤的资源放到res文件夹下,theme1的资源放到res-theme1文件夹下。如:
每个皮肤建立一个单独的资源文件夹:
上图中所示文件res/values/colors.xml中定义了default皮肤中的颜色: #FF0000,
上图中所示文件res-theme1/values/colors.xml中定义了theme1皮肤中的颜色: #0000FF。(注:为了更好的阅读性,可将这里的res-theme1/values/colors.xml文件命名为colors_theme1.xml。)
记得在mk文件中配置新建的资源文件夹路径:
在Android.mk中加入语句:
========================
LOCAL_RESOURCE_DIR := (LOCAL_PATH)/res-theme1
========================
3、资源引用
本换肤框架仅支持通过资源id设置的颜色、图片等资源的换肤。
例如现在有default皮肤的颜色资源为
#FF0000
有theme1皮肤的颜色资源为
#0000FF
那么:
直接设置drawable或颜色值不支持换肤
=========================
java代码中:
Drawable drawable = getResources().getDrawable(R.color.color_bg);
setBackgroundDrawable(drawable ) // 不支持换肤
xml中:
background="#FF0000"
=========================
通过设置资源id,才支持换肤
=========================
java代码中:
setBackgroundResource(R.drawable.color_bg) // 支持换肤
xml中:
background="@drawable/color_bg" <!--支持换肤→
4、监听皮肤变化
通常情况下,应用是不需要自己关注皮肤变化的回调的(即不需要自行注册如下监听),但如果有特殊需求,那么可采用如下方法监听皮肤变化。
可通过UiModeManager的addSkinChangedCallback(SkinChangedCallback callback)方法来注册监听回调:
==========================================
private UiModeManager.SkinChangedCallback mSkinChangeCallback = new UiModeManager.SkinChangedCallback() {
@Override
public void onSkinChanged(@NonNull String name) {
LogUtils.d(TAG, "onSkinChanged() called with: name = [" + name + "]");
//TODO
}
};
UiModeManager uiModeManager = (UiModeManager) getSystemService(Context.UI_MODE_SERVICE);
uiModeManager .addSkinChangedCallback(mSkinChangeCallback);
5、切换皮肤(Settings需要关注这一步,其它应用只需做上面三步)
对于普通应用,只需上面三步就可以支持换肤了。而有触发皮肤切换功能的应用(如Settings),需要通过如下语句来切换皮肤:
(1)切换到theme1皮肤:
==========================================
SkinCompatManager.getInstance().setSkinData(“theme1”, Settings.Global.SYSTEM_THEME_STRATEGY_BUILD_IN); // 其中皮肤名称theme1应作为常量定义到Settings.Global类中
==========================================
(2)恢复到默认皮肤:
==========================================
SkinCompatManager.getInstance().setSkinData(Settings.Global.SYSTEM_THEME_NAME_DEFAULT, Settings.Global.SYSTEM_THEME_STRATEGY_NONE);
==========================================
注意:Android-Skin-Support框架有自己的切换皮肤的接口,如SkinCompatManager.loadSkin、SkinCompatManager.restoreDefaultTheme等,不要使用这些方法,而应该使用本文档中介绍的setSkinData方法
因为暂时希望尽量保留Android-Skin-Support框架原有的方法,所以并没有屏蔽这些方法,待后期功能调试稳定后,会屏蔽这些方法。
三、自定义View换肤
1、直接继承自SkinCompatXXX控件
如直接继承SkinCompatTextView、SkinCompatLinearLayout等控件,这些控件是已经实现了换肤功能的。
2、继承自Android原生的控件
这种方式实现的自定义View,需要自己实现skin.support.widget.SkinCompatSupportable接口来实现换肤功能
注:推荐使用第一种方式实现自定义View换肤
四、Dialog换肤
1、SkinCompatDialog
需要实现换肤功能的Dialog,需要使自己的Dialog继承自SkinCompatDialog。SkinCompatDialog直接继承自Dialog。
注:
1、原生的Dialog本身就具有一定的换肤功能,SkinCompatDialog只是比原生Dialog多了窗口背景的换肤功能。因此,在不需要实现window背景换肤的情况下,可直接使用原生Dialog换肤。
2、暂未实现AlertDialog的换肤,有需求必须要使用AlertDialog的时候再考虑添加。
2、DialogFragment
使用DialogFragment实现的dialog,需要重写DialogFragment的onCreateDialog(Bundle savedInstanceState)方法,在该方法中返回一个派生自SkinCompatDialog的Dialog对象
五、注意事项(可能踩坑的地方)
1、字体颜色换肤
问题:
对于Button、TextView这类控件,在xml中通过android:textColor属性设置字体颜色的资源可实现字体颜色换肤。
但在java代码中却通过setTextColor(ColorStateList colors)设置字体颜色却不支持换肤,因为该接口不是通过颜色的资源id来设置的。
解决:
方法1(推荐)、在SkinCompatButton和SkinCompatTextView中加入了setTextColorResource(int resId)接口,通过该接口可实现字体颜色换肤
方法2、通过TextView原生的setTextAppearance(@StyleRes int resId)接口来设置字体颜色,可实现字体颜色换肤。但这种方式设置字体颜色后,字体的粗细与为使用该接口的TextView相比,有细微的不同。
2、在java代码中直接new出来的View
问题:
直接new出来的View,因为没有加入到皮肤切换的监听中,所以不能响应换肤
解决:
方法1(推荐)、将控件写到xml中,然后通过LayoutInflater.inflate方法将其解析出来,用这种方式代替直接new一个View的对象,可实现换肤。
方法2、将new出来的View的对象,通过SkinCompatManager的addObserver(SkinObserver o)方法将该View对象加入到皮肤切换的监听中。实例代码如下:
===========================
mCustomView = new CustomView(this); //注:这里new出的View必须要是实现了skin.support.observe.SkinObserver接口的。
SkinCompatManager.getInstance().addObserver(mCustomView);
===========================
3、关于在values/colors.xml中定义不同皮肤的颜色资源
先举例:
在res/values/colors.xml中定义颜色如下:
#FFFFFF
@android:color/wihte
在res-theme1/values/colors.xml中定义颜色如下:
@android:color/holo_red_light
@android:color/holo_red_light
问题:
在java中,使用color_bg_1和color_bg_2为View控件设置颜色,能成功换肤。
在xml中,引用color_bg_1和color_bg_2为Vuew控件设置颜色,却发现引用color_bg_1的地方换肤成功,引用color_bg_2的地方换肤无效。
问题原因:
在xml中为View控件设置颜色为color_bg_2,会将@android:color/wihte的id传给控件,
因此在切换皮肤的时候获取到的资源名为white,而不是color_bg_2,这时在组装theme1皮肤的资源名的时候,就成了white_theme1,因此会找不到这个资源而换肤失败
而color_bg_1就不存在这个问题,因此color_bg_1换肤成功,color_bg_2换肤失败。
解决:
方法1:使用color_bg_1的方式定义颜色
方法2:确实有需要以color_bg_2的方式定义颜色,且在xml中使用这个颜色的话。就需要将color_bg_2封装到selector或shape的xml中去再引用
还是以color_bg_2为例:
(1)可以将其封装到res/drawable/color_bg_2.xml中,然后在引用的地方通过R.drawable.color_bg_2或@drawable/color_bg_2的方式引用,color_bg_2.xml内容如下:
===========================
===========================
并将color_bg_2_theme1封装到res-theme1/drawable/color_bg_2_theme1.xml中:
===========================
===========================
(2)也可以将其封装到res/color/color_bg_2.xml中,然后在引用的地方通过R.color.color_bg_2或@color/color_bg_2的方式引用,color_bg_2.xml内容如下:
===========================
===========================
4、给Activity或SkinCompatDialog的Window设置windowBackground
问题:
在应用中使用了换肤框架后,默认情况下,在Activity或Dialog中使用下列方法设置窗口背景无效:
getWindow().setBackgroundDrawableResource(resId)或getWindow().setBackgroundDrawable(drawable)
问题原因:
AndroidSkinSupport框架中的SkinCompatManager中有一个setSkinWindowBackgroundEnable(boolean enable)方法,通过该方法可以控制窗口背景换肤功能的开关。
默认情况下,该窗口背景换肤功能是打开的。当该功能打开后,系统会在Activity的onResume方法和Dialog的onStart方法被调用时,去找到主题中的android:windowBackground属性所对应的资源id,并通过该资源id来设置当前皮肤对应的资源。
通常应用使用的主题中都是会配置android:windowBackground属性的(当前主题没有配置,但父主题中都是会配置的)。
所以,只要主题中android:windowBackground的属性不为null,那么这个换肤框架就总是会在Activity的onResume方法和Dialog的onStart方法被调用时(即Activity或Dialog显示的时候),将它们的window的窗口背景设置成主题中配置的背景
解决:
方法1:在style中通过android:windowBackground属性设置窗口背景,如:
......
@*android:drawable/bg
方法2:若不需要窗口背景换肤的功能,可在Application的onCreate方法中加上如下关闭此功能:
SkinCompatManager.getInstance().setSkinWindowBackgroundEnable(false);