文章目錄
-
- 1. 換膚效果
- 2. 換膚思路
- 3. 代碼實作
- 4. 生成皮膚包
- 5. 代碼下載下傳位址
1. 換膚效果
先看效果,此demo比較簡陋,主要實作了顔色、圖檔、自定義View、字型樣式、狀态欄換膚等子產品
2. 換膚思路
先說插件化換膚主要思路:一般應用換膚主要都是更換顔色、圖檔等資源,是以我們首先需要拿到要換膚的資源ID,然後在皮膚包中設定該屬性值為想改變的顔色或圖檔資源,原應用内下載下傳皮膚包,通過代碼即可實作換膚。
例如:一個TextView的顔色需要改變,那麼我們需要得到該TextView的屬性對應的顔色ID值,假設為
textColor
,原應用中colorAccent的值為
android:textColor="@color/colorAccent"
在皮膚包中,我們将colorAccent的值修改為任意想改變的值
<color name="colorAccent">#ffffff</color>
,打包成APK,通過代碼即可實作TextView的顔色的改變。
<color name="colorAccent">#569847</color>
一個成熟的項目一般都是批量化換膚,我們來一步步實作。
我們知道layout資源加載都是在setContentView中,在資源檔案加載之前替換資源實作換膚,閱讀源碼,重點在框起來的這行代碼
LayoutInflater也就是布局填充器,負責将xml布局加載到頁面上,繼續深入,進入inflate方法,最終定位到我們的目标方法
createViewFromTag
中,重點在框起來的部分,factory工廠
其中,Factory2繼承自Factory,比Factory多了一個parent參數。差別在于:如果需要提供将建立視圖的父級,則需要使用Factory2 。但如果應用的定位API級别為11+,則通常使用Factory2 ,否則,隻需使用Factory
public interface Factory {
public View onCreateView(String name, Context context, AttributeSet attrs);
}
public interface Factory2 extends Factory {
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
Factory2提供了一個接口
onCreateView
,我們可以通過實作這個接口,介入到建立view的這個過程中去,記錄所有的view,同時拿到view所有需要換膚的屬性,記錄下來,然後根據屬性替換,以上就是我們換膚的大緻思路。
3. 代碼實作
建立一個library,專門用于處理換膚的SDK,先看項目目錄如下
其實SkinManager是換膚庫的管理類,單利模式實作,在項目的MyApplication 中初始化,傳遞application
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
SkinManager.init(this);
}
}
通過自定義SkinActivityLifeCycle繼承自Application.ActivityLifecycleCallbacks,實作對應用中所有Activity的生命周期的管理
SkinActivityLifeCycle主要在onActivityCreated方法中建立自定義工廠SkinFactory,先隻看框起來的部分
SkinFactory繼承自LayoutInflater.Factory2,在onCreateView方法中周遊所有的View,得到可以換膚的View的集合
createViewFromTag
和
createView
實作如下,代碼注釋也寫得比較清楚,主要思路是用全類名,通過反射擷取其class得到該View的構造器,存儲到自定義的構造器集合裡,便于下次再遇到不用再通過反射建立,然後傳回該View的構造器(這段其實和源碼一樣,不想寫可以直接複制源碼,這裡寫出來主要是為了了解思路)
/**
* 拿到view
*
* @param name 布局控件的名稱,如ImageView,LinearLayout等
* @param attributeSet view的屬性集合
*/
private View createViewFromTag(String name, Context context, AttributeSet attributeSet) {
// 自定義view
// if (-1 != name.indexOf(".")) {
if (name.contains(".")) {
return null;
}
// 原生veiw
View view = null;
for (String aMClassPrefixlist : mClassPrefixlist) {
// mClassPrefixlist[i] + name === android.widget.TextView
view = createView(aMClassPrefixlist + name, context, attributeSet);
if (view != null) {
break;
}
}
return view;
}
/**
* 根據全類名建立view
* @param name 全類名,如 android.widget.TextView
* @param attributeSet view的屬性集合
*/
private View createView(String name, Context context, AttributeSet attributeSet) {
Constructor<? extends View> constructor = constructorHashMap.get(name);
// 先從集合中取,如果集合中沒有存儲過該view的構造器,反射擷取class,然後擷取構造器,再存儲到map中
if (constructor == null) {
try {
// 反射,通過全類名擷取class
Class<? extends View> aClass = context.getClassLoader().loadClass(name).asSubclass(View.class);
// 擷取構造參數,隻能擷取兩個參數的構造函數
constructor = aClass.getConstructor(mConstructorSignature);
//添加到map中
constructorHashMap.put(name, constructor);
// constructor.newInstance()表示根據構造器取到對應的對象
return constructor.newInstance(context, attributeSet);
} catch (Exception e) {
e.printStackTrace();
}
} else {
try {
return constructor.newInstance(context, attributeSet);
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
繼續回到我們實作的onCreateView方法中,既然得到了View,我們就可以通過SkinAttr的load方法,周遊該View種的屬性集合,篩選需要換膚的屬性,存儲起來。
SkinAttr實作如下,主要思路是自定義了一個mAttributes集合,集合中包含了需要換膚的屬性合集,在load方法中,通過周遊傳進來View的AttributeSet集合,與我們需要換膚的集合比較,如果有就擷取該屬性對應的屬性值,判斷屬性值開頭是
#
,
?
,
@
,等。
如果是
#
說明是固定顔色值,可以修改也可以不改,具體看項目需求,此處未修改。
?
代表是系統屬性,需要特殊處理,其他就是類似
@
開頭的值,依次得到resId後,建立自定義的SkinPain,包括屬性名和屬性id,添加到SkinPain的集合中。
周遊完成後,判斷SkinPain集合是否為空(代碼可以看到,這裡的條件還有
view instanceof TextView || view instanceof SkinViewSupport
,這兩個是更換字型和自定義view的判斷條件,後面再講),不為空則建立SkinView(屬性包含View和SkinPain),并将其添加到SkinView的集合中,至此我們得到了所有需要換膚的View的集合。
/**
* 周遊view的屬性集合類
*/
public class SkinAttr {
private Typeface typeface;
private String tag = SkinAttr.class.getSimpleName();
// 可更改的view的屬性集合
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("skinTypeface");
}
// view的name和id集合
private List<SkinView> skinViews = new ArrayList<>();
public SkinAttr(Typeface typeface) {
this.typeface = typeface;
}
/**
* 加載view的屬性集合,周遊得到它可以更換的屬性集合
*
* @param view
* @param attributeSet
*/
public void load(View view, AttributeSet attributeSet) {
List<SkinPain> skinPains = new ArrayList<>();
for (int i = 0; i < attributeSet.getAttributeCount(); i++) {
// 擷取屬性的名字
String attributeName = attributeSet.getAttributeName(i);//background
// 如果目前view的屬性集中包含這些屬性
if (mAttributes.contains(attributeName)) {
// 擷取對應的屬性值
String attributeValue = attributeSet.getAttributeValue(i);//取到的是R檔案裡對應的值,隻不過是string
Log.e(tag, "attributeValue == " + attributeValue);//?2130837582
if (attributeValue.startsWith("#")) {//#121212
continue;// 帶#的是寫死的,不改//當然也可以修改,看具體項目需求
}
int resId;
if (attributeValue.startsWith("?")) {// ?colorAccent
// 提取attributeValue值,去掉第一位的?剩下colorAccent,并轉化為id值
int attrId = Integer.parseInt(attributeValue.substring(1));
Log.e(tag, "attrId == " + attrId);//2130837582
resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
Log.e(tag, "resId? == " + resId);
} else {
// @color/colorAccent
resId = Integer.parseInt(attributeValue.substring(1));
Log.e(tag, "resId@ == " + resId);
}
// 儲存屬性名稱和對應id
if (resId != 0) {
SkinPain skinPain = new SkinPain(attributeName, resId);
skinPains.add(skinPain);
}
}
}
if (!skinPains.isEmpty() || view instanceof TextView || view instanceof SkinViewSupport) {
SkinView skinView = new SkinView(view, skinPains);
skinView.applySkin(typeface);
// 儲存view和他的可變屬性集合,用于後續修改
skinViews.add(skinView);
}
}
public void setTypeface(Typeface typeface) {
this.typeface = typeface;
}
// view
// view的名稱和id
private class SkinView {
View view;
List<SkinPain> skinPains;
public SkinView(View view, List<SkinPain> skinPains) {
this.view = view;
this.skinPains = skinPains;
}
// 循環周遊,應用修改皮膚屬性
public void applySkin(Typeface typeface) {
applyTypeFace(typeface);
applySkinSupport();
for (SkinPain skinPain : skinPains) {
Drawable left = null, top = null, right = null, bottom = null;
Log.e(tag, "skinPain == " + skinPain.attributeName);
switch (skinPain.attributeName) {
case "background":
Object background = SkinResources.getInstance().getBackground(skinPain.resId);
if (background instanceof Integer) {// 顔色值
view.setBackgroundColor((Integer) background);
} else {//drawable屬性值
ViewCompat.setBackground(view, (Drawable) background);
}
break;
case "src":
background = SkinResources.getInstance().getBackground(skinPain.resId);
if (background instanceof Integer) {
((ImageView) view).setImageDrawable(new ColorDrawable((Integer) background));
} else {
((ImageView) view).setImageDrawable((Drawable) background);
}
break;
case "textColor":
((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList(skinPain.resId));
break;
case "drawableLeft":
left = SkinResources.getInstance().getDrawable(skinPain.resId);
break;
case "drawableTop":
top = SkinResources.getInstance().getDrawable(skinPain.resId);
break;
case "drawableRight":
right = SkinResources.getInstance().getDrawable(skinPain.resId);
break;
case "drawableBottom":
bottom = SkinResources.getInstance().getDrawable(skinPain.resId);
break;
case "skinTypeface":
applyTypeFace(SkinResources.getInstance().getTypeface(skinPain.resId));
break;
default:
break;
}
if (null != left || null != right || null != top || null != bottom) {
((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
}
}
}
// 字型換膚
private void applyTypeFace(Typeface typeface) {
if (view instanceof TextView) {
Log.e(tag, "typeface == " + typeface.getStyle());
((TextView) view).setTypeface(typeface);
}
}
// 自定義View換膚
private void applySkinSupport() {
if (view instanceof SkinViewSupport) {
Log.e(tag,"applySkinSupport === ");
((SkinViewSupport) view).applySkinView();
}
}
}
/**
* view屬性名和在R檔案中對應id的類
*/
private class SkinPain {
String attributeName;
int resId;
public SkinPain(String attributeName, int resId) {
this.attributeName = attributeName;
this.resId = resId;
}
}
// 更換皮膚
public void applySkin() {
for (SkinView skinView : skinViews) {
skinView.applySkin(typeface);
}
}
}
通過實作Factory的onCreateView方法,我們在xml加載之前得到了所有需要換膚的View集合,那麼怎麼實作換膚呢?
SkinManager中的
loadSkin
方法,判斷傳過來的皮膚包位址是否為空,空就加載預設皮膚,否則加載給定路徑的皮膚,這裡我換膚路徑寫死了,給模拟器對應的路徑傳了自定義皮膚包的apk進去,一般線上是先下載下傳,然後換膚。
注意:這裡因為讀取了sd卡,是以需要添加讀寫權限,否則會空指針異常
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
通過反射得到PackageInfo,重點在
SkinResources.getInstance().applySkin(skinResource, packageName);
這行代碼,初始化皮膚包資源和包名。然後通過觀察者模式通知更新皮膚。
// 加載皮膚
public void loadSkin(String skinPath) {
Log.e(tag,"skinPath == " + skinPath);
if (TextUtils.isEmpty(skinPath)) {
// 清空資料總管,皮膚資源屬性
SkinResources.getInstance().reset();
// 位址為空,使用預設皮膚
SkinPreference.getInstance().setSkin("");
} else {
try {
// 反射建立AssetManager與Resource
AssetManager assetManager = AssetManager.class.newInstance();
// 資源路徑設定目錄或壓縮包
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, skinPath);
Resources appResources = application.getResources();
// 根據目前的顯示與配置(橫豎屏、語言等)建立Resources
Resources skinResource = new Resources(assetManager, appResources.getDisplayMetrics(), appResources.getConfiguration());
// 擷取外部APK(皮膚包)的包名
PackageInfo packageArchiveInfo = application.getPackageManager().getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES);
Log.e(tag,"packageArchiveInfo == "+packageArchiveInfo);
if(packageArchiveInfo!=null) {
String packageName = packageArchiveInfo.packageName;
// 初始化皮膚包資料總管
Log.e(tag,"skinResource == " + skinResource);
Log.e(tag,"packageName == " + packageName);
SkinResources.getInstance().applySkin(skinResource, packageName);
// 本地記錄
SkinPreference.getInstance().setSkin(skinPath);
} else {
Toast.makeText(application,"包資訊為null",Toast.LENGTH_SHORT).show();
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
// 通知采集的view更新皮膚
// 被觀察者改變 通知所有觀察者
setChanged();
notifyObservers(null);
}
SkinFactory的
update
方法中收到更新消息,skinAttr.applySkin()方法實作換膚
@Override
public void update(Observable observable, Object o) {
// 狀态欄換膚
SkinThemeUtils.updateStatusBarColor(activity);
// 字型換膚,此處是用于設定及時生效的
Typeface typeface = SkinThemeUtils.updateTypeFace(activity);
skinAttr.setTypeface(typeface);
// 普通屬性換膚
skinAttr.applySkin();
}
SkinAttr的
applySkin
方法,周遊之前得到的需要換膚的skinViews集合,通過applySkin方法,周遊屬性合集
// 更換皮膚
public void applySkin() {
for (SkinView skinView : skinViews) {
skinView.applySkin(typeface);
}
}
SkinAttr的
SkinView
内部類實作
// view的名稱和id
private class SkinView {
View view;
List<SkinPain> skinPains;
public SkinView(View view, List<SkinPain> skinPains) {
this.view = view;
this.skinPains = skinPains;
}
// 循環周遊,應用修改皮膚屬性
public void applySkin(Typeface typeface) {
applyTypeFace(typeface);
applySkinSupport();
for (SkinPain skinPain : skinPains) {
Drawable left = null, top = null, right = null, bottom = null;
Log.e(tag, "skinPain == " + skinPain.attributeName);
switch (skinPain.attributeName) {
case "background":
Object background = SkinResources.getInstance().getBackground(skinPain.resId);
if (background instanceof Integer) {// 顔色值
view.setBackgroundColor((Integer) background);
} else {//drawable屬性值
ViewCompat.setBackground(view, (Drawable) background);
}
break;
case "src":
background = SkinResources.getInstance().getBackground(skinPain.resId);
if (background instanceof Integer) {
((ImageView) view).setImageDrawable(new ColorDrawable((Integer) background));
} else {
((ImageView) view).setImageDrawable((Drawable) background);
}
break;
case "textColor":
((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList(skinPain.resId));
break;
case "drawableLeft":
left = SkinResources.getInstance().getDrawable(skinPain.resId);
break;
case "drawableTop":
top = SkinResources.getInstance().getDrawable(skinPain.resId);
break;
case "drawableRight":
right = SkinResources.getInstance().getDrawable(skinPain.resId);
break;
case "drawableBottom":
bottom = SkinResources.getInstance().getDrawable(skinPain.resId);
break;
case "skinTypeface":
applyTypeFace(SkinResources.getInstance().getTypeface(skinPain.resId));
break;
default:
break;
}
if (null != left || null != right || null != top || null != bottom) {
((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
}
}
}
// 字型換膚
private void applyTypeFace(Typeface typeface) {
if (view instanceof TextView) {
Log.e(tag, "typeface == " + typeface.getStyle());
((TextView) view).setTypeface(typeface);
}
}
// 自定義View換膚
private void applySkinSupport() {
if (view instanceof SkinViewSupport) {
Log.e(tag,"applySkinSupport === ");
((SkinViewSupport) view).applySkinView();
}
}
}
SkinResources是實作換膚的類,大緻思路是,如果是預設皮膚,就傳回原始包中對應的id值,如果需要換膚就傳回mSkinResources也就是通過皮膚包得到的id,然後在SkinAttr的SkinView的applySkin中設定給對應的View即可實作換膚。
public class SkinResources {
private static SkinResources instance;
private Resources mSkinResources;
private String mSkinPkgName;
private boolean isDefaultSkin = true;
private Resources mAppResources;
private String tag = SkinResources.class.getSimpleName();
private SkinResources(Context context) {
mAppResources = context.getResources();
}
public static void init(Context context) {
if (instance == null) {
synchronized (SkinResources.class) {
if (instance == null) {
instance = new SkinResources(context);
}
}
}
}
public static SkinResources getInstance() {
if (instance == null) {
throw new IllegalStateException("SkinResources 未初始化");
}
return instance;
}
public void reset() {
mSkinResources = null;
mSkinPkgName = "";
isDefaultSkin = true;
}
public void applySkin(Resources resources, String pkgName) {
mSkinResources = resources;
mSkinPkgName = pkgName;
//是否使用預設皮膚
isDefaultSkin = TextUtils.isEmpty(pkgName) || resources == null;
}
// 根據原生app view參數的id,得到name,取到皮膚包中相同name的id
public int getIdentifier(int resId) {
if (isDefaultSkin) {
return resId;
}
//在皮膚包中不一定就是 目前程式的 id
//擷取對應id 在目前的名稱 colorPrimary
//R.drawable.ic_launcher
String resName = mAppResources.getResourceEntryName(resId);//ic_launcher
Log.e(tag, " resName == " + resName);
String resType = mAppResources.getResourceTypeName(resId);//drawable
Log.e(tag, " resType == " + resType);
int skinId = mSkinResources.getIdentifier(resName, resType, mSkinPkgName);
Log.e(tag, " skinId == " + skinId);
return skinId;
}
public int getColor(int resId) {
if (isDefaultSkin) {
return mAppResources.getColor(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getColor(resId);
}
return mSkinResources.getColor(skinId);
}
public ColorStateList getColorStateList(int resId) {
if (isDefaultSkin) {
return mAppResources.getColorStateList(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getColorStateList(resId);
}
return mSkinResources.getColorStateList(skinId);
}
public Drawable getDrawable(int resId) {
//如果有皮膚 isDefaultSkin false 沒有就是true
if (isDefaultSkin) {
return mAppResources.getDrawable(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getDrawable(resId);
}
return mSkinResources.getDrawable(skinId);
}
/**
* 可能是Color 也可能是drawable
*
* @return
*/
public Object getBackground(int resId) {
String resourceTypeName = mAppResources.getResourceTypeName(resId);
if (resourceTypeName.equals("color")) {
return getColor(resId);
} else {
// drawable
return getDrawable(resId);
}
}
public String getString(int resId) {
try {
if (isDefaultSkin) {
Log.e("SkinResources", "mAppResources.getString(resId) == " + mAppResources.getString(resId));
return mAppResources.getString(resId);
}
int skinId = getIdentifier(resId);
Log.e("SkinResources", "skinId == " + skinId);
if (skinId == 0) {
Log.e("SkinResources", "mAppResources.getString(resId) == " + mAppResources.getString(resId));
return mAppResources.getString(resId);
}
Log.e("SkinResources", "mSkinResources.getString(resId) == " + mSkinResources.getString(skinId));
return mSkinResources.getString(skinId);
} catch (Resources.NotFoundException e) {
}
return null;
}
// 擷取字型
public Typeface getTypeface(int resId) {
String skinTypefacePath = getString(resId);
Log.e("tag", "typefacepath == " + skinTypefacePath);
if (TextUtils.isEmpty(skinTypefacePath)) {
return Typeface.DEFAULT;
}
try {
Typeface typeface;
if (isDefaultSkin) {
typeface = Typeface.createFromAsset(mAppResources.getAssets(), skinTypefacePath);
return typeface;
}
typeface = Typeface.createFromAsset(mSkinResources.getAssets(), skinTypefacePath);
return typeface;
} catch (RuntimeException e) {
}
return Typeface.DEFAULT;
}
}
最後在項目中添加點選事件實作換膚即可
寫得有點啰嗦,但是大緻思路和實作方法基本就是這些,不是很難,就是具體項目中皮膚包的實作比較繁瑣,需要細心細心再細心。
4. 生成皮膚包
說到這裡,說一下怎麼實作皮膚包吧,建立一個項目,不需要activity這些,隻保留value下的資源,設定需要換膚的屬性值,color,圖檔等,然後Build——Build Bundle(s)/APK(s)——Build APK(s),打包成一個apk就行了
5. 代碼下載下傳位址
https://download.csdn.net/download/mr_hmgo/21351930