效果:
可修改字體類型,字體顏色,背景顏色,背景圖案.等等
可配合服務(wù)端,提供在線下載皮膚功能,下載完成即時(shí)生效替換資源.
實(shí)現(xiàn)思路:
1.采樣: 找到需要替換的所有view控件,記錄保存起來
2.替換皮膚資源: 利用AssetManager.加載皮膚資源,生成Resources,在給view設(shè)置資源屬性的時(shí)候,使用皮膚資源Resources來設(shè)置
實(shí)現(xiàn)原理:
皮膚包其實(shí)是一個(gè)apk,在更換皮膚的時(shí)候,其實(shí)是使用皮膚包里面的資源,來替換本地app的資源文件.
1.AssetManager加載皮膚包資源
AssetManager里面有一個(gè)hide的方法addAssetPath,通過反射調(diào)用這個(gè)方法可以給AssetManager設(shè)置我們皮膚資源的path,來加載皮膚資源
/**
* Add an additional set of assets to the asset manager. This can be
* either a directory or ZIP file. Not for use by applications. Returns
* the cookie of the added asset, or 0 on failure.
* {@hide}
*/
public final int addAssetPath(String path) {
return addAssetPathInternal(path, false);
}
AssetManager assetManager = AssetManager.class.newInstance();
Method method = assetManager.getClass().getMethod("addAssetPath", String.class);
method.setAccessible(true);
//調(diào)用addAssetPath方法,傳入皮膚資源路徑
method.invoke(assetManager,path);
//得到本app的application的resources
Resources resources = application.getResources();
//根據(jù)本app的resources的配置創(chuàng)建皮膚Resources
Resources skinResource = new Resources(assetManager, resources.getDisplayMetrics(),
resources.getConfiguration());
//獲取外部Apk(皮膚包) 包信息
PackageManager mPm = application.getPackageManager();
PackageInfo info = mPm.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
String packageName = info.packageName;
2.利用LayoutInflater采樣,找到需要換膚的view
查看系統(tǒng)setContentView(int resId)源碼發(fā)現(xiàn),view的創(chuàng)建是通過LayoutInflater來創(chuàng)建的,而LayoutInflater在創(chuàng)建view的過程中,我們可以通過給LayoutInflater.setFactory2(),來設(shè)置我們自己的Factory2,然后拿到需要替換皮膚的View
mLayoutInflater.inflate(layoutResID, mContentParent);
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
//得到xml解析器
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
//...省略
//調(diào)用createViewFromTag來創(chuàng)建xml對(duì)應(yīng)的View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
......
try {
View view;
//如果有mFactory2 ,就調(diào)用該工廠來創(chuàng)建view
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
//省略部分代碼...
return view;
}
LayoutInflater.setFactory2(),回調(diào)到Factory的onCreateView()方法中,模仿系統(tǒng)源碼,實(shí)現(xiàn)創(chuàng)建view,并且拿到這個(gè)view,來給它設(shè)置資源,實(shí)現(xiàn)換皮膚效果
仿寫android系統(tǒng)源碼,利用classloader來創(chuàng)建view
//name 傳入view的全路徑,如果是android SDK提供的view,需要我們拼接路徑處理
//如果是自定義控件,或者控件在xml使用的時(shí)候帶 '.'的,就直接傳入該完整路徑
private View createView(String name, Context context, AttributeSet attrs) {
Constructor<? extends View> constructor = sConstructorMap.get(name);
if(constructor == null) {
try {
//加載類的全路徑,得到class
Class<? extends View> aClass = context.getClassLoader().loadClass(name).asSubclass(View.class);
//得到構(gòu)造方法,參數(shù):mConstructorSignature是方法的參數(shù)類型.class
constructor = aClass.getConstructor(mConstructorSignature);
sConstructorMap.put(name, constructor);
} catch (Exception e) {
e.printStackTrace();
}
}
if(null != constructor){
try {
return constructor.newInstance(context,attrs);
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
3.過濾需要換皮膚的view,并且設(shè)置屬性
拿到上面創(chuàng)建的view,按照我們定義的條件,找到符合條件,需要替換皮膚的控件,并且設(shè)置屬性值
private static final List<String> mAttributes = new ArrayList<>();
static {
//過濾的條件,有以下屬性的view才考慮換膚
mAttributes.add("background");
mAttributes.add("src");
mAttributes.add("textColor");
mAttributes.add("tabTextColor");
mAttributes.add("drawableLeft");
mAttributes.add("drawableTop");
mAttributes.add("drawableRight");
mAttributes.add("drawableBottom");
}
switch (skinPair.attributeName) {
//設(shè)置background
case "background":
Object background = SkinResources.getInstance().getBackground(skinPair.resId);
//Color
if (background instanceof Integer) {
view.setBackgroundColor((Integer) background);
} else {
ViewCompat.setBackground(view, (Drawable) background);
}
break;
//設(shè)置textColor
case "textColor":
((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList
(skinPair.resId));
break;
case "drawableLeft":
left = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
case "drawableTop":
top = SkinResources.getInstance().getDrawable(skinPair.resId);
......
//按照我們需要設(shè)置的屬性,并且賦值..
default:
break;
}
if (null != left || null != right || null != top || null != bottom) {
((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right,
bottom);
}
4.還原
使用APP默認(rèn)的Resources來加載資源,并且給換皮膚的view設(shè)置回APP默認(rèn)的Resources下的資源即可
注意:
1.皮膚包其實(shí)是一個(gè)只有資源文件的空殼apk
2.app的資源文件的名字,必須和apk的資源文件的名字一樣