1、Android换肤的背景
移动客户端在不重启应用的情况下,实现动态换肤的效果。换肤这块做的比较好的,有网易云音乐,qq等,给用户带来了多样的界面选择和个性化定制。本文介绍Android客户端换肤/主题切换的原理和实践。
2、Android换肤的实现原理和实践方法
2.1 Android 实现应用内换肤的常用方式(两种)
- 通过Theme切换主题,即静态方法。
- 通过AssetManager切换主题,可实现动态切换。
2.2 动态换肤方案的优点分析
- 动态换肤可以满足日常产品和运营需求,满足用户个性化界面定制的需求等等。
- 动态换肤,相比于静态皮肤,可以减小apk大小。
- 皮肤模块独立便于维护。
- 由服务器下发,不需要发版即可实现动态更新。
2.3 通过Theme切换主题
Android 通过在 Activity 中使用 setTheme() 函数来设置背景样式,通过加载styles.xml里的样式来设置Android 应用的主题。需要在 setContentView(R.layout.activity_main);之前调用setTheme()。
在开始制作主题之前我们先看下这张图
通过这张图我们可以了解到不同的字段代表的是哪一块的颜色,例如:
- colorPrimary 代表的是 App Bar 的颜色。
- colorPrimaryDark 代表的是状态栏的背景色。
我们也可以自己定制布局控件的颜色:
2.3.1 在values文件夹下创建attr.xml ,在attr.xml写入属性名
<resources>
<attr name="mainColor" format="color" />
<attr name="view1color" format="color" />
<attr name="view2color" format="color" />
<attr name="view3color" format="color" />
<attr name="button1color" format="color" />
</resources>
2.3.2 在colors.xml 填入需要用到的颜色
<resources>
<color name="colorPrimary">#008577</color>
<color name="colorAccent">#D81B60</color>
<color name="colorPrimaryDark">#00574B</color>
<color name="blue2">#006c93</color>
<color name="blue1">#b4e1f1</color>
<color name="blue3">#003CFF</color>
<color name="blace">#000000</color>
<color name="white">#FFFFFF</color>
<color name="red">#fd0000</color>
<color name="red2">#f96363</color>
<color name="green">#04fd00</color>
<color name="yellow">#D9B300</color>
<color name="gray">#cecece</color>
<color name="pink">#ff3542</color>
</resources>
2.3.3 设置控件的颜色样式,注意红框圈起来的部分
2.3.4 在styles.xml文件下自定义主题样式,
<style name="AppTheme" parent="Base.Theme.AppCompat.Light.DarkActionBar" >
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>// App Bar 颜色
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>//状态栏颜色
<item name="colorAccent">@color/colorAccent</item>//控件选中状态下的颜色
<item name="android:windowBackground">@drawable/white</item>//窗口背景颜色
<item name="view1color">@color/blue1</item>//textview1的颜色
<item name="view2color">@color/red</item>//textview2的颜色
<item name="view3color">@color/yellow</item>//textview3的颜色
<item name="button1color">@color/blue1</item>//button的颜色
</style>
2.3.5 实现读取配置文件设置主题
private void setBaseTheme() {
SharedPreferences sharedPreferences = getSharedPreferences(
"com.example.test_preferences", MODE_PRIVATE);
String themeType = sharedPreferences.getString("theme_type", "蓝色主题");
int themeId;
switch (themeType) {
case "蓝色主题":
themeId = R.style.blueTheme;
break;
case "粉色主题":
themeId = R.style.pinkTheme;
break;
case "彩色主题":
themeId = R.style.AppTheme;
break;
default:
themeId = R.style.blueTheme;
}
setTheme(themeId);
}
2.4 动态切换主题
2.4.1 原理
动态主题切换、换肤功能,是点击换肤按钮后,瞬间发生改变,系统在换肤时直接触发 View 的 setColor,setBackgroudColor,setDrawable 等方法实现,然而一个个方法进行调用显然是不现实的,对于这种公用的功能需求一般都会抽取成接口,让 View 继承实现自己的逻辑。
实现思路:
- 定义换肤功能接口,让需要换肤的 View 实现自己的换肤逻辑。
- 给 LayoutInflater 设置自定义的 Factory2,将 XML 中的 View 改为实现换肤接口的 View,并且将 View 记录下来。
- 制作皮肤包,这里让皮肤包和 App 本身资源名相同,值不同,这样换肤时,根据 View 设置的资源名去皮肤包中找同名资源。
如图所示,当打包出 Apk 后,保持资源名称相同,值不同:
- 换肤时,循环记录下来的换肤 View,调用其换肤方法即可。
2.4.2 动态换肤、主题切换实现流程
1、制作皮肤包的方法和过程
1). 使用Android studio新建工程project。
2). 将换肤的资源文件添加到res文件下,无java文件。
3). 直接运行build.gradle,生成apk文件。运行时Run/Redebug configurations 中Launch Options选择launch nothing,否则build 会报 no default Activty的错误。
4). 将apk文件重命名如themeXXX.apk,重命名为 themeXXX.skin 防止用户点击安装。
构建之后的产物提供给服务端,用于皮肤资源下载。
2、下载并加载皮肤包
1) 将皮肤包上传到服务器后台。
2) 客户端根据接口数据下载皮肤包,在客户端进行加载及客户端换肤操作。
3、拿到皮肤包Resource对象
// 构建PackageManager
PackageManager pm = context.getPackageManager();
PackageInfo info = pm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
skinPackageName = info.packageName;
// 反射方式把资源包注入AssetManager
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, skinPkgPath);
// 返回Resources
Resources superRes = context.getResources();
Resources skinResource = new
Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
superRes为当前app的Resource对象,而skinResource即为加载后的皮肤包的Resource对象。
皮肤包的资源即可通过
skinResource.getIdentifier(resName,"color",skinPackageName);
这种方式拿到了。
4、标记需要换肤的View
通过skin:enbale="true"这种方式,对布局中需要换肤的View进行标记
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:skin="http://schemas.android.com/android/skin"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/hall_back_color"
skin:enable="true"
>
<code.solution.widget.CustomActivityBar
android:id="@+id/custom_activity_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/widget_action_bar_height"
app:common_activity_title="@string/app_name"
app:common_activity_title_gravity="center"
app:common_activity_title_icon="@drawable/ic_win_cp"
/>
</LinearLayout>
在SKinInflaterFactory的onCreateView 方法中,实际是对xml中映射的每个View 进行过滤。
- 如果 skin:enbale 不为 true 则直接返回null交给系统默认去创建。
- 如果为true,则自己去创建这个View,并将这个VIew的所有属性比如id, width height,textColor,background等与支持换肤的属性进行对比。比如我们支持换background textColor listSelector等, android:background="@color/hall_back_color" 这个属性,在进行换肤的时候,如果皮肤包里存在hall_back_color这个值的设置,就将这个颜色值替换为皮肤包里的颜色值,以完成换肤的需求。同时,也会将这个需要换肤的View保存起来。
如果在切换换肤之后,进入一个新的页面,就在进入这个页面Activity的 InlfaterFacory的onCreateView里根据skin:enable="true" 这个标记,进行判断。为true则进行换肤操作。而对于切换换肤操作时,已经存在的页面,就对这几个存在页面保存好的需要换肤的View进行换肤操作。
2)在代码中动态添加的View
上述是针对在布局中设置skin:ebable="true"的View进行换肤,那么如果我们的View不是通过布局文件,而是通过在代码种创建的View,怎样换肤呢?
public void dynamicAddSkinEnableView(Context context, View view, List<DynamicAttr> pDAttrs) {
List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();
SkinItem skinItem = new SkinItem();
skinItem.view = view;
for (DynamicAttr dAttr : pDAttrs) {
int id = dAttr.refResId;
String entryName = context.getResources().getResourceEntryName(id);
String typeName = context.getResources().getResourceTypeName(id);
SkinAttr mSkinAttr = AttrFactory.get(dAttr.attrName, id, entryName, typeName);
viewAttrs.add(mSkinAttr);
}
skinItem.attrs = viewAttrs;
skinItem.apply();
addSkinView(skinItem);
}
public void dynamicAddSkinEnableView(Context context, View view, String attrName, int attrValueResId) {
int id = attrValueResId;
String entryName = context.getResources().getResourceEntryName(id);
String typeName = context.getResources().getResourceTypeName(id);
SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
SkinItem skinItem = new SkinItem();
skinItem.view = view;
List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();
viewAttrs.add(mSkinAttr);
skinItem.attrs = viewAttrs;
skinItem.apply();
addSkinView(skinItem);
}
即在Activity中通过比如
dynamicAddSkinEnableView(context, mTextView,"textColor",R.color.main_text_color)即可完成对动态创建的View的换肤操作。
本文研究是基于github开源项目Android-Skin-Loader进行的。这个框架主要是动态加载皮肤包,在不需要重启应用的前提下,实现对页面布局等动态换肤的操作。皮肤包独立制作和维护,不和主工程产生耦合。同时由后台服务器下发,可即时在线更新不依赖客户端版本。
5、切换时即时刷新页面
1、SkinBaseApplication:
public class SkinApplication extends BaseApplication {
@Override
public void onCreate() {
super.onCreate();
SkinManager.getInstance().init(this);
SkinManager.getInstance().load();
}
}
主要是进行一些初始化的操作。
2、SkinBaseActivity:
public abstract class BaseActivity extends
code.solution.base.BaseActivity implements ISkinUpdate, IDynamicNewView {
private SkinInflaterFactory mSkinInflaterFactory;
@Override
protected void onCreate(Bundle savedInstanceState) {
mSkinInflaterFactory = new SkinInflaterFactory();
LayoutInflaterCompat.setFactory(getLayoutInflater(), mSkinInflaterFactory);
super.onCreate(savedInstanceState);
changeStatusColor();
}
/**
* dynamic add a skin view
*
* @param view
* @param attrName
* @param attrValueResId
*/
protected void dynamicAddSkinEnableView(View view, String attrName, int attrValueResId){
mSkinInflaterFactory.dynamicAddSkinEnableView(this, view, attrName, attrValueResId);
}
@Override
public void onThemeUpdate() {
if(!isResponseOnSkinChanging){
return;
}
mSkinInflaterFactory.applySkin();
changeStatusColor();
}
在这里使用了之前自定义的SkinInflaterFactory,来替换默认的Factory,以达到截获创建View,获取View的属性,与支持换肤的属性进行对比,进行View换肤操作以及保存这些需要换肤的View到List中,在下次换肤切换时对这些View进行换肤的目的。
其中换肤操作执行时,会调用SKinManager.notifySKinUpdate方法
@Override
public void notifySkinUpdate() {
if(skinObservers == null) return;
for(ISkinUpdate observer : skinObservers){
observer.onThemeUpdate();
}
}
而这里的observer.onThemeUpdate里面主要是执行这个Activity的下述方法:
public void onThemeUpdate() {
if(!isResponseOnSkinChanging){
return;
}
mSkinInflaterFactory.applySkin();
changeStatusColor();
}
mSkinInflaterFactory.applySkin();即为SKinInflaterFactory的applySkin方法,
public void applySkin() {
if (ListUtils.isEmpty(mSkinItems)) {
return;
}
for (SkinItem si : mSkinItems) {
if (si.view == null) {
continue;
}
si.apply();
}
}
其中 mSKinItems即为当前Acitivty通过xml 文件中skin:enbale进行标记的 及动态dynamicAddSkinEnableView(…)添加的需要换肤的View的集合,这样整个换肤的过程就完成了。