App換膚在很多大廠的應(yīng)用中都是比較常見的;比如:網(wǎng)易云音樂抄肖、QQ音樂咒锻、酷狗音樂等應(yīng)用中都是可以見到的府框。這些應(yīng)用是如何做到換膚的效果的,接下來就分析一下是如何實(shí)現(xiàn)的若厚。
1.控件采集
要實(shí)現(xiàn)換膚肯定是要獲取到需要換膚的控件;比如ImageView拦英、TextView等這些控件得提前去把它們收集起來在用戶點(diǎn)擊換膚的時(shí)候就批量的找到對(duì)應(yīng)的資源替換的一個(gè)過程;控件是如何做到集中采集的呢?這個(gè)就需要去看View的加載過程了测秸,這里就不去分析了疤估,在LayoutInflater中的createViewFromTag()中可以找到每一個(gè)View都是通過mFactory2.onCreateView();的方法創(chuàng)建的;那么我們需要得到每一個(gè)View則需要自已去創(chuàng)建這些View灾常,則要去自己實(shí)現(xiàn)這個(gè)OnCreateView方法來替換掉系統(tǒng)用自已實(shí)現(xiàn)的;具體代碼實(shí)現(xiàn)如下:
/**
* Create by Wayne on 2020/3/7
* Describe:View的控件采集
*/
public class SkinLayoutInflaterFactory implements LayoutInflater.Factory2 {
private static final String TAG = SkinLayoutInflaterFactory.class.getName();
//記錄對(duì)應(yīng)View的構(gòu)造函數(shù)
private static final Map<String,Constructor<? extends View>> mConstructorMap = new HashMap<>();
private static final Class<?>[] mConstructorSignature = new Class[]{Context.class,AttributeSet.class};
private Activity activity;
private SkinAttribute skinAttribute = null;
private static final String [] mClassPrefixList = {
"android.widget.",
"android.view.",
"android.webkit."
};
public SkinLayoutInflaterFactory(Activity activity, Typeface typeface) {
this.activity = activity;
skinAttribute = new SkinAttribute(typeface);
}
@Nullable
@Override
public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attributeSet) {
View view = createViewFormTag(name,context,attributeSet);
if(view == null){
view = createView(name,context,attributeSet);
}
if(view != null){
skinAttribute.load(view,attributeSet,activity);
}
return view;
}
@Nullable
@Override
public View onCreateView(@NonNull String s, @NonNull Context context, @NonNull AttributeSet attributeSet) {
return null;
}
private View createViewFormTag(String name,Context context,AttributeSet attrs){
if(-1 != name.indexOf('.')){
return null;
}
for (int i = 0; i < mClassPrefixList.length; i++) {
return createView(mClassPrefixList[i]+name,context,attrs);
}
return null;
}
private View createView(String name,Context context,AttributeSet attrs){
Constructor<? extends View> constructor = findConstructor(context,name);
try {
return constructor.newInstance(context,attrs);
}catch (Exception e){
e.printStackTrace();
}
return null;
}
private Constructor<? extends View> findConstructor(Context context, String name) {
Constructor<? extends View> constructor = mConstructorMap.get(name);
if(null == constructor){
try {
Class<? extends View> clazz = context.getClassLoader().loadClass(name).asSubclass(View.class);
constructor = clazz.getConstructor(mConstructorSignature);
mConstructorMap.put(name,constructor);
}catch (Exception e){
e.printStackTrace();
}
}
return constructor;
}
}
接下來就需要替換掉系統(tǒng)中Factory2的對(duì)象,需要用自已實(shí)現(xiàn)的類以達(dá)到控件采集的目的,在類寫好后可以調(diào)用LayoytInflaterCompat中的setFactory2();方法把我們寫好的類設(shè)置進(jìn)去替換掉系統(tǒng)的铃拇。
SkinLayoutInflaterFactory skinLayoutInflaterFactory = new SkinLayoutInflaterFactory(activity,typeface);
LayoutInflaterCompat.setFactory2(layoutInflater,skinLayoutInflaterFactory);
所有的控件已經(jīng)采集到了;則需要去分析出哪一些控件中包含需要換膚的屬性保存起來,比如:background钞瀑、src、textColor等屬性都需要換膚;具體要采集的控件屬性如下:
private static final List<String> mAttributes = new ArrayList<>();
static {
mAttributes.add("background");
mAttributes.add("src");
mAttributes.add("textColor");
mAttributes.add("drawableLeft");
mAttributes.add("drawableTop");
mAttributes.add("drawableRight");
mAttributes.add("drawableBottom");
mAttributes.add("progressDrawable");
mAttributes.add("thumb");
mAttributes.add("style");
mAttributes.add("track");
mAttributes.add("button");
mAttributes.add("skinTypeface");
}
每一個(gè)控件創(chuàng)建完成后都去和這個(gè)集合中的屬性對(duì)比,如果有匹配則把該View和對(duì)應(yīng)的屬性保存起來;代碼實(shí)現(xiàn)如下:
//用于記錄換膚需要操作的View與屬性信息
private List<SkinView> mSkinViews = new ArrayList<>();
public void load(View view, AttributeSet attrs, Activity activity){
this.mActivity = activity;
List<SkinPair> mSkinPars = new ArrayList<>();
for (int i = 0;i<attrs.getAttributeCount();i++) {
//獲得屬性名
String attributeName = attrs.getAttributeName(i);
Log.d(TAG,"屬性名為:"+attributeName);
if(mAttributes.contains(attributeName)){
String attributeValue = attrs.getAttributeValue(i);
//如果是Color的 以#開頭的表示寫死的顏色不需要換膚
if(attributeValue.startsWith("#")){
continue;
}
int resId = 0;
if(attributeValue.startsWith("?")){
int attrId = Integer.parseInt(attributeValue.substring(1));
resId = SkinUtils.getResId(view.getContext(),new int[]{attrId})[0];
String typeName = view.getResources().getResourceTypeName(resId);
SkinPair skinPair = new SkinPair(attributeName,typeName,mActivity.getComponentName().toShortString(),resId);
if(!mSkinPars.contains(skinPair)){
mSkinPars.add(skinPair);
}
}
if(attributeName.startsWith("@") && attributeValue.startsWith("@0")){
//正常是以 @ 開頭,但是有可能會(huì)有以@0開頭的,比如沒有寫id的,就會(huì)以@0開頭的,所以得過慮掉這些
resId = Integer.parseInt(attributeValue.substring(1));
String typeName = view.getResources().getResourceTypeName(resId);
SkinPair skinPair = new SkinPair(attributeName,typeName,mActivity.getComponentName().toShortString(),resId);
if(!mSkinPars.contains(skinPair)){
mSkinPars.add(skinPair);
}
}
}
}
if(!mSkinPars.isEmpty()){
SkinView skinView = new SkinView(view,mSkinPars,activity.getComponentName().toShortString());
skinView.applySkin(typeface);
mSkinViews.add(skinView);
}else if(view instanceof TextView || view instanceof SkinViewSupport){
//沒有屬性滿足但是需要修改字體
SkinView skinView = new SkinView(view,mSkinPars,activity.getComponentName().toShortString());
skinView.applySkin(typeface);
mSkinViews.add(skinView);
}
}
2.皮膚包加載
上面已經(jīng)把所有的View已經(jīng)采集完了慷荔,那么接下來就需要把皮膚包加載進(jìn)來這樣才能去獲取皮膚包中的資源雕什,Android中的資源管理都是通過AssetManager來管理的,那么則需要去創(chuàng)建AssetManager的實(shí)例显晶,注意:這里不能用App中的AssetManager贷岸,因?yàn)槠つw包是從SD卡加載進(jìn)來的,不是原App中的磷雇,所以不能使用偿警,需要重新創(chuàng)建AssetManager的實(shí)例才可以,通過反射去創(chuàng)建AssetManager的實(shí)例;代碼實(shí)現(xiàn):
/**
* 皮膚包加載
* @param skinPath 皮膚路徑 如果為空則使用默認(rèn)皮膚
*/
public void loadSkin(String skinPath){
if(TextUtils.isEmpty(skinPath)){
SkinResource.getInstance().reset();
//清空資源管理器 皮膚資源屬性
SkinResource.getInstance().reset();
}else {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",String.class);
addAssetPath.invoke(addAssetPath,skinPath);
Resources appResource = mContext.getResources();
Resources skinResource = new Resources(assetManager,appResource.getDisplayMetrics(),appResource.getConfiguration());
SkinPreference.getInstance().setSkinPath(skinPath);
PackageManager mPm = mContext.getPackageManager();
PackageInfo info = mPm.getPackageArchiveInfo(skinPath,PackageManager.GET_ACTIVITIES);
mSkinPackageName = info.packageName;
SkinResource.getInstance().applySkin(skinResource,mSkinPackageName);
}catch (Exception e){
onLoadSkinFailure(e);
e.printStackTrace();
}
}
setChanged();
notifyObservers(null);
}
皮膚包加載進(jìn)來后就可以實(shí)例Resource對(duì)象了唯笙,這樣就可以拿到皮膚包的Resource對(duì)象就可以拿到皮膚中的資源文件;當(dāng)換膚時(shí)則需要用皮膚包的Resource去取對(duì)應(yīng)的資源螟蒸。
3.控件的資源替換
上面控件已經(jīng)采集,皮膚包也已經(jīng)加載后內(nèi)存后則就可以把需要換膚的控件進(jìn)行皮膚替換睁本,代碼實(shí)現(xiàn):
public void applySkin(Typeface typeface) {
applyTypeFace(typeface);
applySkinSupport();
for (SkinPair skinPair : skinPairs) {
Drawable left = null, top = null, right = null, bottom = null;
switch (skinPair.attributeName) {
case "background":
Object background = SkinResource.getInstance().getBackground(skinPair
.resId);
if(skinPair.typeName.equals("color")){
view.setBackgroundColor(SkinResource.getInstance().getColor(skinPair.resId));
}else if(skinPair.typeName.equals("drawable") || skinPair.typeName.equals("mipmap")){
if (background instanceof Integer) {
view.setBackgroundColor((int) background);
} else {
view.setBackgroundDrawable((Drawable) background);
}
}
break;
case "src":
background = SkinResource.getInstance().getBackground(skinPair
.resId);
if (skinPair.typeName.equals("drawable") || skinPair.typeName.equals("mipmap")) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
if (view instanceof ImageButton) {
ImageButton imageButton = (ImageButton) view;
if (background instanceof Integer) {
imageButton.setImageDrawable(new ColorDrawable((Integer) background));
} else {
imageButton.setImageDrawable((Drawable) background);
}
} else if (view instanceof EditText) {
EditText editText = (EditText) view;
if (background instanceof Integer) {
editText.setBackground(new ColorDrawable((Integer) background));
} else {
editText.setBackground((Drawable) background);
}
} else if (view instanceof ImageView) {
ImageView imageView = (ImageView) view;
if (background instanceof Integer) {
imageView.setImageDrawable(new ColorDrawable((Integer) background));
} else {
imageView.setImageDrawable((Drawable) background);
}
} else {
if (background instanceof Integer) {
view.setBackground(new ColorDrawable((Integer) background));
} else {
view.setBackground((Drawable) background);
}
}
}
}
break;
case "textColor":
if (view instanceof TextView) {
((TextView) view).setTextColor(SkinResource.getInstance().getColorStateList
(skinPair.resId));
} else if (view instanceof Button) {
((Button) view).setTextColor(SkinResource.getInstance().getColorStateList
(skinPair.resId));
}
break;
}
}
根據(jù)具體的View判斷來進(jìn)行資源替換完成換膚操作;以上就是換膚三個(gè)步驟;換膚分析的是總理思路尿庐,還有一細(xì)節(jié)處理這里就大概描述一下忠怖,如果要完善還要考慮內(nèi)存泄露的問題呢堰,因?yàn)樾枰獡Q膚的View是保存起來的,所以當(dāng)Activity在onDestroyed的時(shí)候則需要把Activity中的View去釋放掉凡泣,這里就涉及Activity的生命周期管理了枉疼,這樣可以去實(shí)現(xiàn) Application中的ActivityLifecycleCallbacks去監(jiān)聽Activity的生命周期;還有就是字體的替換和狀態(tài)欄的換膚也沒有分析,其實(shí)這兩個(gè)很簡單字體的替換也是加載皮膚包中字體出來然后TextView動(dòng)態(tài)設(shè)置字體文件就可以達(dá)到字體替換效果鞋拟,狀態(tài)欄的換膚可以通過Activity拿到Window去替換顏色就可以了骂维。以上就是換膚的全部內(nèi)容,如果發(fā)現(xiàn)哪里描述有問題或者是錯(cuò)誤可以私信指正贺纲,畢竟本人知識(shí)水平有限難免出錯(cuò);需要demo也可以聯(lián)系我航闺,如果文章讓你get到干貨請(qǐng)給我一個(gè)贊吧。