1). 换肤思路
在源应用APP中,下载皮肤包,使得对应的文件资源得以应用。使用DexClassLoader加载资源包,使用反射来获取资源ID.
2). 创建SkinChange工程
app是应用Module,spinone和spintwo是皮肤插件工程
3). app Module
- BaseActivity
/**
* Activity基类
* Created by mazaiting on 2018/6/27.
*/
public abstract class BaseActivity extends AppCompatActivity {
/**资源管理器*/
protected AssetManager mAssetManager;
/**资源*/
protected Resources mResources;
/**主题*/
protected Theme mTheme;
/**
* 加载资源
* @param dexPath dex路径
*/
protected void loadResources(String dexPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",String.class);
addAssetPath.invoke(assetManager, dexPath);
mAssetManager = assetManager;
} catch (Exception e) {
e.printStackTrace();
}
Resources superRes = super.getResources();
superRes.getDisplayMetrics();
superRes.getConfiguration();
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
mTheme = mResources.newTheme();
mTheme.setTo(super.getTheme());
}
@Override
public AssetManager getAssets() {
return mAssetManager == null ? super.getAssets() : mAssetManager;
}
@Override
public Resources getResources() {
return mResources == null ? super.getResources() : mResources;
}
@Override
public Theme getTheme() {
return mTheme == null ? super.getTheme() : mTheme;
}
}
- MainActivity
public class MainActivity extends BaseActivity {
private static final String TAG = "MainActivity";
private TextView mTextView;
private ImageView mImageView;
private ClassLoader mClassLoader;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = findViewById(R.id.tv_show);
mImageView = findViewById(R.id.iv_show);
}
/**
* 切换主题1
*/
public void changeThemeOne(View view) {
// 获取缓存路径
String fileDir = getCacheDir().getAbsolutePath();
// 获取文件路径
String filePath = fileDir + File.separator + "spinone-release.apk";
mClassLoader = new DexClassLoader(filePath, fileDir, null, getClassLoader());
loadResources(filePath);
setContentOne();
}
/**
* 切换主题2
*/
public void changeThemeTwo(View view) {
// 获取缓存路径
String fileDir = getCacheDir().getAbsolutePath();
// 获取文件路径
String filePath = fileDir + File.separator + "spintwo-release.apk";
mClassLoader = new DexClassLoader(filePath, fileDir, null, getClassLoader());
loadResources(filePath);
setContent();
}
/**
* 设置主题内容
*/
private void setContent() {
try {
Class clazz = mClassLoader.loadClass("com.mazaiting.UiUtil");
// 设置TextView内容
Method method = clazz.getMethod("getTextString", Context.class);
String string = (String) method.invoke(null, this);
mTextView.setText(string);
// 设置ImageView背景
method = clazz.getMethod("getImageDrawable",Context.class);
Drawable drawable = (Drawable) method.invoke(null,this);
mImageView.setBackground(drawable);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 设置主题1
*/
private void setContentOne() {
int stringId = getTextStringId();
int drawableId = getImgDrawableId();
Log.d(TAG, "stringId: " + stringId + ", drawableId: " + drawableId);
}
/**
* 获取图片ID
* @return
*/
private int getImgDrawableId() {
try {
// "com.mazaiting.spinone.R$color" -- spinone module 下的R.color.img
Class clazz = mClassLoader.loadClass("com.mazaiting.spinone.R$color");
Field field = clazz.getField("img");
int resId = (int) field.get(null);
return resId;
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
/**
* 获取字符串ID
* @return
*/
private int getTextStringId() {
try {
// "com.mazaiting.spinone.R$string" -- spinone module下的R.string.text
Class clazz = mClassLoader.loadClass("com.mazaiting.spinone.R$string");
Field field = clazz.getField("text");
int resId = (int) field.get(null);
return resId;
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
}
- activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.mazaiting.skinchange.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:onClick="changeThemeOne"
android:text="主题1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:onClick="changeThemeTwo"
android:text="主题2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<LinearLayout
android:paddingTop="50dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tv_show"
android:text="@string/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<ImageView
android:id="@+id/iv_show"
android:background="@color/img"
android:layout_width="200dp"
android:layout_height="200dp"/>
</LinearLayout>
</android.support.constraint.ConstraintLayout>
-
资源文件
4). 插件Module
spinone和spintwo中同样使用一个类,包的全路径名与MainActivity中setContent方法中的Class clazz = mClassLoader.loadClass("com.mazaiting.UiUtil");
中com.mazaiting.UiUtil
相同。并在插件Module中使用相同的资源文件名进行替换。
package com.mazaiting;
/**
* Created by mazaiting on 2018/6/27.
*/
public class UiUtil {
/**
* 获取字符串
*/
public static String getTextString(Context ctx) {
return ctx.getResources().getString(R.string.text);
}
/**
* 获取图片
*/
public static Drawable getImageDrawable(Context ctx) {
return ctx.getResources().getDrawable(R.color.img);
}
public static int getTextStringId(){
return R.string.text;
}
public static int getImageDrawableId(){
return R.color.img;
}
}
5). 运行
安装app Module 至手机,spinone和spintwo Module打包为apk文件,使用命令安装至手机,此处必须将spinone-release.apk文件push到/data/data/<包名>/cache/
目录下
adb push C:\Users\mazaiting\Desktop\release\s
pinone-release.apk /data/data/com.mazaiting.skinchange/cache
示例效果:
6). 残留的问题
- 每个插件包都包含一个UiUtil文件,造成代码冗余(主题2的加载)
- 主工程无法获取插件包的应用包名,从而实现动态加载。(主题1的加载)(能想到的解决办法:插件的文件名即包名)