android 換膚(1)——外掛式無縫換膚(解析鴻洋大神的換膚流程)
對於app換膚,這是一個常見而又常用的功能。雖然我做的專案中還沒涉及到換膚,但是還是想研究下。
於是,下載了鴻洋大神的換膚demo來研究。
先看效果圖:(尊重鴻洋大神的程式碼,效果圖上原創)
鴻洋大神的換膚有兩種:
這兩種我都分析過。各有各的好處。不過我還是推薦使用第一種。
博文後面我會比較這兩種換膚的差異,以及為什麼推薦使用第一種tag換膚的原因。
這兩種換膚的核心方法其實是一樣的,只不過怎麼獲取需要換膚的資源的方法不同而已。
先來分析關鍵程式碼:
先看專案目錄:
我們可以從資源的命名看出來,資源的差異在於字尾名的不同,這是app內部換膚的關鍵,根據字尾名不同來區分。
而外掛式換膚,也就是獲得sd卡里的apk檔案,然後獲得其中的資原始檔進行換膚的資源命名也是遵循這個規則。
1,介面
ISkinCallback
/**
* 設定面板狀態回撥
*/
public interface ISkinCallback {
void onStart();//開始
void onError(Exception e);//錯誤
void onComplete();//完成
}
ISkinListener
/**
* 通知面板設定的介面
*/
public interface ISkinListener {
//這個介面用於通知應該改變面板資源了
void onSkinChanged();
}
執行換膚的時候其實就是呼叫的ISkinListener介面而已。所有繼承它的view都能得到換膚的通知。
2,儲存使用者的面板喜好設定
SkinSharedPreferencesUtils 類
/**
* 儲存使用者對該app的面板設定
*/
public class SkinSharedPreferencesUtils {
private SharedPreferences sharedPreferences;//鍵值對例項
public SkinSharedPreferencesUtils(Context context) {
sharedPreferences = context.getSharedPreferences(SkinContacts.PREF_NAME, Context.MODE_PRIVATE);
}
//獲得外掛路徑
public String getPluginPath() {
if (sharedPreferences == null) return "";
return sharedPreferences.getString(SkinContacts.KEY_PLUGIN_PATH, "");
}
//新增外掛路徑
public void setPluginPath(String pluginPath) {
if (sharedPreferences == null) return;
sharedPreferences.edit().putString(SkinContacts.KEY_PLUGIN_PATH, pluginPath).apply();
}
//獲得資源字尾
public String getAttrSuffix() {
if (sharedPreferences == null) return "";
return sharedPreferences.getString(SkinContacts.KEY_PLUGIN_SUFFIX, "");
}
//新增資源字尾
public void setAttrSuffix(String attrSuffix) {
if (sharedPreferences == null) return;
sharedPreferences.edit().putString(SkinContacts.KEY_PLUGIN_SUFFIX, attrSuffix).apply();
}
//獲得外掛的包名
public String getPluginPackage() {
if (sharedPreferences == null) return "";
return sharedPreferences.getString(SkinContacts.KEY_PLUGIN_PACKAGE, "");
}
//新增外掛的包名
public void setPluginPackage(String pluginPackage) {
if (sharedPreferences == null) return;
sharedPreferences.edit().putString(SkinContacts.KEY_PLUGIN_PACKAGE, pluginPackage).apply();
}
//清理當前的sharedPreferences
public boolean clear() {
if (sharedPreferences == null) return false;
return sharedPreferences.edit().clear().commit();
}
}
這沒啥好說的,鍵值對儲存在app中。
3,定義換膚常量
SkinContacts類
/**
* 面板常量
*/
public class SkinContacts {
public static final String PREF_NAME = "skin_plugin_name";//外掛工廠名
public static final String KEY_PLUGIN_PATH = "key_plugin_path";//外掛路徑
public static final String KEY_PLUGIN_PACKAGE = "key_plugin_package";//外掛包名
public static final String KEY_PLUGIN_SUFFIX = "key_plugin_suffix";//外掛字尾
public static final String ATTR_PREFIX = "skin:";//資源驗證的字首,只有帶有skin的字首才是要被修改的字首
public static final int SKIN_TAG = 0x5201314;//表填驗證,獲得當前view的所有名字
}
4,重點方法1:面板資源管理器
ResourceManager類
/**
* 面板資源管理器
*/
public class ResourceManager {
private Resources mResources;//資源物件
private String mPluginPackageName;//外掛包名
private String mSuffix;//面板區別的字尾名
//預設圖片和顏色型別
private static final String DEFTYPE_DRAWABLE = "drawable";
private static final String DEFTYPE_COLOR = "color";
public ResourceManager(Resources mResources, String mPluginPackageName, String mSuffix) {
this.mResources = mResources;
this.mPluginPackageName = mPluginPackageName;
this.mSuffix = TextUtils.isEmpty(mSuffix) ? "" : mSuffix;
}
//將資源名新增字尾,用於app內部的面板修改
private String appendSuffix(String name) {
//如果設定了面板的字尾名,則在資源名稱的後面新增字尾名
//例如:預設面板的資源名是:skin_index_drawable
//如果添加了字尾名,則說明使用了另外一套面板:skin_index_drawable_red
return TextUtils.isEmpty(mSuffix) ? name : name + "_" + mSuffix;
}
//傳入資源名稱,根據包名和資源字尾名來確定返回的資源
public Drawable getDrawableByName(String name) {
try {
name = appendSuffix(name);
//這段程式碼的意思相當於在指定的包名下找到指定的資原始檔夾名字然後找到指定的資源名稱。引數是相反的。
//引數:1,資源名;2,資源型別;3,包名
return mResources.getDrawable(mResources.getIdentifier(name, DEFTYPE_DRAWABLE, mPluginPackageName));
} catch (Resources.NotFoundException e) {
try {
//如果在圖片中沒有找到資源就在顏色資源裡找
return mResources.getDrawable(mResources.getIdentifier(name, DEFTYPE_COLOR, mPluginPackageName));
} catch (Resources.NotFoundException e2) {
e.printStackTrace();
return null;
}
}
}
//根據資源名獲得顏色
public int getColorByName(String name) {
try {
name = appendSuffix(name);
return mResources.getColor(mResources.getIdentifier(name, DEFTYPE_COLOR, mPluginPackageName));
} catch (Resources.NotFoundException e) {
e.printStackTrace();
return -1;
}
}
//根據資源名獲得顏色集合
public ColorStateList getColorStateList(String name) {
try {
name = appendSuffix(name);
return mResources.getColorStateList(mResources.getIdentifier(name, DEFTYPE_COLOR, mPluginPackageName));
} catch (Resources.NotFoundException e) {
e.printStackTrace();
return null;
}
}
//根據資源名獲得圖片集合
public ColorStateList getColorStateListtDrawable(String name) {
try {
name = appendSuffix(name);
return mResources.getColorStateList(mResources.getIdentifier(name, DEFTYPE_DRAWABLE, mPluginPackageName));
} catch (Resources.NotFoundException e) {
e.printStackTrace();
return null;
}
}
}
ResourceManager類用於儲存當前app的資源物件或者從外掛中獲取的資源物件,用於得到資源物件中的資源。實際的換面板的資源和方法都由它管理。
5,重點方法2:skin
從專案結構圖可以看出,skin目錄下有四個java檔案。
他們建立的順序是:
1,SkinAttrType 具體修改面板的列舉
2,SkinAttr 儲存資源名和資源型別,一一對應
3,SkinView 儲存需要更改面板的view和view下的所有資源,一一對應
4,SkinAttrSupport 獲得一個view下所有資源集合的工具類
1,SkinAttrType
/**
* 3,資源型別列舉
*/
public enum SkinAttrType {
BACKGROUD("background")//背景,將給傳入的view設定新的背景
{
@Override
public void apply(View view, String resName) {
//背景可能是圖片也可能只是顏色
Drawable drawable = getResourceManager().getDrawableByName(resName);
if (drawable == null) return;
view.setBackgroundDrawable(drawable);
LogUtils.i("背景:" + resName + "view的id:" + view.getId());
}
},
TEXT_COLOR("textColor")//字型顏色,將給傳入的view設定新的字型顏色
{
@Override
public void apply(View view, String resName) {
if (view instanceof TextView) {
ColorStateList colorlist = getResourceManager().getColorStateList(resName);
if (colorlist == null) return;
((TextView) view).setTextColor(colorlist);
LogUtils.i("字型顏色:" + resName + "view的id:" + view.getId());
}
}
},
SRC("src")//src圖片,將給傳入的view指定新的圖片
{
@Override
public void apply(View view, String resName) {
if (view instanceof ImageView) {
Drawable drawable = getResourceManager().getDrawableByName(resName);
if (drawable == null) return;
((ImageView) view).setImageDrawable(drawable);
LogUtils.i("src圖片:" + resName + "view的id:" + view.getId());
}
}
};
private String attrType;//資源型別
//提供外部呼叫方法,獲得當前資源的型別
public String getAttrType() {
return attrType;
}
//構造方法
SkinAttrType(String attrType) {
this.attrType = attrType;
}
//抽象方法傳入需要改變的view和資源名
protected abstract void apply(View view, String resName);
//獲得資源管理器
public ResourceManager getResourceManager() {
ResourceManager resourceManager = SkinManagerOutdated.getInstance().getResourceManager();
return resourceManager;
}
}
2,SkinAttr
/**
* 2,該類儲存了一個view下的資源名和資源型別,對應關係
*/
public class SkinAttr {
private String resName;//資源名
private SkinAttrType attrType;//資源型別
//構造方法中傳入資源型別例項和資源名
public SkinAttr(SkinAttrType attrType, String resName) {
this.resName = resName;
this.attrType = attrType;
}
//執行換膚,對於傳進來的view換膚成相對應傳進來的資源名
protected void apply(View view) {
//列舉中有具體的換膚操作
attrType.apply(view, resName);
}
}
3,SkinView
/**
* 1,該類提供了一個activity所有view的資源替換方法
*/
public class SkinView {
private View view;//需要改變面板的view
private List<SkinAttr> attrs;//這個view下所有的資源例項
//傳入view和所有資源
public SkinView(View view, List<SkinAttr> skinAttrs) {
this.view = view;
this.attrs = skinAttrs;
}
//執行換膚,將該view下的所有資源都進行換膚
public void apply() {
if (view == null) return;
for (SkinAttr attr : attrs) {
//skinattr中有針對單個資源的換膚
attr.apply(view);
}
}
}
4,SkinAttrSupprot
注意:這個SkinAttrSupprot類我將兩種換膚方式的方法都寫在其中了,以便於更好理解。真正實際用到的,只有通過資源擷取或者tag樣式獲取。兩者二選一。因為這兩個方法屬於不同的換膚方式。
/**
* 4,面板屬性相容類
* 因為每個activity都會使用到該方法所以直接將它設定成靜態的,以免多次例項化和釋放造成記憶體壓力過大
*/
public class SkinAttrSupport {
/**
* 通過資源擷取
*
* @param attrs
* @param context
* @return
*/
public static List<SkinAttr> getSkinAttrs(AttributeSet attrs, Context context) {
List<SkinAttr> skinAttrs = new ArrayList<>();
SkinAttr skinAttr;
//在這裡迴圈遍歷出這個activity中所包含的所有資源
for (int i = 0; i < attrs.getAttributeCount(); i++) {
String attrName = attrs.getAttributeName(i);//屬性名
String attrValue = attrs.getAttributeValue(i);//屬性值
//通過屬性名獲得到屬性型別,這個屬性型別列舉中已經包含了被查詢到的列舉屬性:例如包含了backage
SkinAttrType attrType = getSupprotAttrType(attrName);
if (attrType == null) continue;
if (attrValue.startsWith("@")) {
//通過屬性值獲得屬性id,以@開頭驗證
int id = Integer.parseInt(attrValue.substring(1));
//通過屬性id獲得這個屬性的名稱,例如R檔案id是0x7f050000;可以通過這個id得到它的命名:例如:skin_index_bg
String entryName = context.getResources().getResourceEntryName(id);
if (entryName.startsWith(SkinContacts.ATTR_PREFIX)) {
//我們驗證skin以後的資源名稱加上帶有資源型別的列舉,例如:color型別的列舉
skinAttr = new SkinAttr(attrType, entryName);
skinAttrs.add(skinAttr);
//LogUtils.i("新增資源SkinAttr:" + entryName);
}
}
}
return skinAttrs;
}
/**
* tag的樣式,我們可以根據tag來擷取
*
* @param tagStr 樣式:skin_left_menu_icon:src|skin_color_red:textColor
* @return
*/
public static List<SkinAttr> getSkinTags(String tagStr) {
List<SkinAttr> skinAttrs = new ArrayList<>();
if (TextUtils.isEmpty(tagStr)) return skinAttrs;
//將string擷取成一|分隔符的字串陣列
String[] items = tagStr.split("\\|");
for (String item : items) {
//如果不包含標識換膚的字首,則表示它不是需要換膚的
if (!tagStr.startsWith(SkinContacts.ATTR_PREFIX))
return skinAttrs;
//截取出資源名和資源型別
String[] resItems = item.split(":");
String resName = resItems[1];
String resType = resItems[2];
//通過屬性名獲得到屬性型別,這個屬性型別列舉中已經包含了被查詢到的列舉屬性:例如包含了backage
SkinAttrType attrType = getSupprotAttrType(resType);
if (attrType == null) continue;
SkinAttr attr = new SkinAttr(attrType, resName);
skinAttrs.add(attr);
}
return skinAttrs;
}
//傳入資源名得到資源型別例項
private static SkinAttrType getSupprotAttrType(String attrName) {
for (SkinAttrType attrType : SkinAttrType.values()) {
//如果列舉中有一個資源型別匹配了,則返回這個列舉所帶有的資源型別例項
if (attrType.getAttrType().equals(attrName))
return attrType;
}
return null;
}
}
6,重點方法3:SkinManager
這個類決定了你如何選擇面板,是呼叫換膚的核心。以上程式碼是換膚的核心。
我們先來看看tag式換膚的SkinManager。
先將SkinManager設定成單例,然後定義屬性
public class SkinManager {
private Context mContext;//這裡的上下文其實是appliaction的上下文,因為換膚全域性有效
private Resources mResources;//換膚的資源物件
private ResourceManager mResourceManager;//換膚的資源管理器
private SkinSharedPreferencesUtils mPrefUtils;//使用者偏好設定
private boolean usePlugin;//是否可以換膚標識
private String mSuffix = "";//應用內換膚的標識字尾
private String mCurPluginPath;//外掛換膚的外掛檔案路徑
private String mCurPluginPkg;//外掛的內建包名
private Map<ISkinListener, List<SkinView>> mSkinViewMaps = new HashMap<>();//鍵值對指向哪一個view對應的它之下所有的需要更換的面板資源
private List<ISkinListener> mActivities = new ArrayList<>();//儲存整個app被標記的要被換膚的物件
private SkinManager() {
}
private static class SingletonHolder {
static SkinManager sInstance = new SkinManager();
}
public static SkinManager getInstance() {
return SingletonHolder.sInstance;
}
public ResourceManager getResourceManager() {
if (!usePlugin) {
mResourceManager = new ResourceManager(mContext.getResources(), mContext.getPackageName(), mSuffix);
}
return mResourceManager;
}
init是在appliaction中呼叫的方法,為app第一次進來的時候,驗證使用者是否使用了外掛面板,如果使用了則載入面板。
//使用者儲存的面板,初始化的時候就可以直接載入進去
public void init(Context context) {
mContext = context.getApplicationContext();
mPrefUtils = new SkinSharedPreferencesUtils(context);
String skinPluginPath = mPrefUtils.getPluginPath();
String skinPluginPkg = mPrefUtils.getPluginPackage();
mSuffix = mPrefUtils.getAttrSuffix();
if (TextUtils.isEmpty(skinPluginPath))//如果沒有外掛路徑則返回
return;
File file = new File(skinPluginPath);
if (!file.exists()) return;//如果外掛檔案不存在,則返回
try {
loadPlugin(skinPluginPath, skinPluginPkg, mSuffix);
mCurPluginPath = skinPluginPath;
mCurPluginPackage = skinPluginPkg;
} catch (Exception e) {
mPrefUtils.clear();
e.printStackTrace();
}
}
loadPlugin 載入外掛資源,這個方法沒得說。要想載入sd卡中的面板資源就這個方法。
//載入外掛資源,建議在非ui執行緒工作;該方法是通用的,必須的方法
private void loadPlugin(String skinPath, String skinPkgName, String suffix) throws Exception {
//獲得系統資源管理類例項
AssetManager assetManager = AssetManager.class.newInstance();
//獲得系統資源管理類下的新增資源路徑方法
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
//呼叫該方法,並且傳入面板所在的路徑
addAssetPath.invoke(assetManager, skinPath);
//獲得resources物件,現在獲得的還是當前app內的
Resources superRes = mContext.getResources();
//獲得一個資源管理,顯示指標,獲取配置
//建立一個新的資源物件的一組現有的資產管理公司的資產。
mResources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
mResourceManager = new ResourceManager(mResources, skinPkgName, suffix);
isUsePlugin = true;
}
clearPluginInfo清理外掛資訊,即回覆預設面板
//清理外掛資訊
private void clearPluginInfo() {
mCurPluginPath = null;
mCurPluginPackage = null;
isUsePlugin = false;
mSuffix = null;
mPrefUtils.clear();
}
//清除所有外掛
public void removeAnySkin() {
clearPluginInfo();
notifyChangedListeners();//通知所有的view,修改面板
}
//更新外掛資訊
private void updatePluginInfo(String skinPluginPath, String pkgName, String suffix) {
mPrefUtils.setPluginPath(skinPluginPath);
mPrefUtils.setPluginPackage(pkgName);
mPrefUtils.setAttrSuffix(suffix);
mCurPluginPackage = pkgName;
mCurPluginPath = skinPluginPath;
mSuffix = suffix;
}
getSkinViews/putSkinViews;存取放在map中的對應view資源集合
//傳入一個繼承了換膚介面的activity,並且返回這個activity中所有的可以換膚的SkinView物件,獲得一個需要換膚的view
//該map裡面存入的是整個app被記錄的需要換膚的view,傳入介面,返回對應的實現了介面的view的所有資源物件
public List<SkinView> getSkinViews(ISkinListener listener) {
return mSkinViewMaps.get(listener);
}
//將每個面板介面的所有view的資源都儲存在map中,然後方便存取對iang
public void putSkinViews(ISkinListener listener, List<SkinView> skinViews) {
mSkinViewMaps.put(listener, skinViews);
}
註冊/反註冊;傳入介面資訊,並儲存,用於全域性換膚;傳入view,用於更換資源。
/**
* 註冊換膚
*/
public void register(final ISkinListener listener, final View view) {
mActivities.add(listener);
//這裡將注入面板的操作新增到佇列中執行,要不然會獲取不到
view.post(new Runnable() {
@Override
public void run() {
injectSkin(listener, view);
}
});
}
//反註冊
public void unregister(ISkinListener listener) {
mActivities.remove(listener);
mSkinViewMaps.remove(listener);
}
injectSkin注入面板;該方法為初始化的時候所呼叫的
//注入面板,在activity初始化的時候進行
public void injectSkin(ISkinListener iSkinListener, View view) {
List<SkinView> skinViews = new ArrayList<>();
addSkinViews(view, skinViews);
putSkinViews(iSkinListener, skinViews);
for (SkinView skinView : skinViews) {
skinView.apply();
}
}
遞迴傳入view的資源到skinview集合中
//遞迴新增view的資源到skinview結合中
public void addSkinViews(View view, List<SkinView> skinViews) {
SkinView skinView = getSkinView(view);
if (skinView != null) skinViews.add(skinView);
if (view instanceof ViewGroup) {
ViewGroup container = (ViewGroup) view;
int n = container.getChildCount();
for (int i = 0; i < n; i++) {
View child = container.getChildAt(i);
addSkinViews(child, skinViews);
}
}
}
//獲得該view下所有的資源屬性
public SkinView getSkinView(View view) {
//獲得每個view所附帶的標籤,這是關鍵所在,後面會講到如何給view賦值tag和拿到view的tag
Object tag = view.getTag(SkinContacts.SKIN_TAG);
if (tag == null)
tag = view.getTag();
if (tag == null) return null;
if (!(tag instanceof String)) return null;
List<SkinAttr> skinAttrs = SkinAttrSupport.getSkinTags(tag.toString());
if (!skinAttrs.isEmpty()) {
changeViewTag(view);
return new SkinView(view, skinAttrs);
}
return null;
}
//修改view所持有的tag標籤
private void changeViewTag(View view) {
Object tag = view.getTag(SkinContacts.SKIN_TAG);
if (tag == null) {
tag = view.getTag();
view.setTag(SkinContacts.SKIN_TAG, tag);
//view.setTag(null);//我不知道為什麼鴻洋大神最後要在添加了tag後又置空,我把它註釋了,不影響執行。
}
}
改變面板的方法changeskin
/**
* 應用內換膚,傳入資源區別的字尾
*/
public void changeSkin(String suffix) {
clearPluginInfo();
mSuffix = suffix;
mPrefUtils.setAttrSuffix(suffix);
notifyChangedListeners();
}
/**
* 更換外掛面板,預設為""
*
* @param skinPluginPath
* @param skinPluginPkg
* @param callback
*/
public void changeSkin(final String skinPluginPath, final String skinPluginPkg, ISkinCallback callback) {
changeSkin(skinPluginPath, skinPluginPkg, "", callback);
}
/**
* 根據suffix選擇外掛內某套面板
*
* @param skinPluginPath
* @param skinPluginPkg
* @param suffix
* @param callback
*/
public void changeSkin(final String skinPluginPath, final String skinPluginPkg, final String suffix, final ISkinCallback callback) {
if (callback == null) return;
callback.onStart();
try {
checkPluginParamsThrow(skinPluginPath, skinPluginPkg);
} catch (IllegalArgumentException e) {
callback.onError(new RuntimeException("checkPlugin occur error"));
return;
}
new AsyncTask<Void, Void, Integer>() {
@Override
protected Integer doInBackground(Void... params) {
try {
loadPlugin(skinPluginPath, skinPluginPkg, suffix);
return 1;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
@Override
protected void onPostExecute(Integer res) {
if (res == 0) {
callback.onError(new RuntimeException("loadPlugin occur error"));
return;
}
try {
updatePluginInfo(skinPluginPath, skinPluginPkg, suffix);
notifyChangedListeners();
callback.onComplete();
} catch (Exception e) {
e.printStackTrace();
callback.onError(e);
}
}
}.execute();
}
notifyChangedListeners發出換膚的通知
//通知所有註冊過的view進行換膚
public void notifyChangedListeners() {
for (ISkinListener listener : mActivities) {
apply(listener);
}
}
ok。SkinManager的方法就這些。它主要是例項化了資源管理器,得到資源例項,並且去呼叫skin資料夾下的更換面板的流程。
接下來我們寫一個抽象activity,實現ISkinListener介面,然後供外部去呼叫。
public abstract class BaseSkinActivity extends AppCompatActivity implements ISkinListener {
//抽象方法傳入當前activity 的資源id
protected abstract int setContentView();
protected abstract void initview();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(setContentView());
//獲得當前activity的最外層view
ViewGroup content = (ViewGroup) findViewById(android.R.id.content);
//在activity建立的時候新增換膚的監聽
//鴻洋大神寫的是直接傳入一個activity例項,我這裡稍微的修改了一下,並不算是優化,只能說方便約束和統一管理
//我這裡不再傳入activity物件,而是傳入實現了這個介面的activity物件和它的view
//目的就是為了當我們繼承這個抽象類的時候,其他地方如果用到了ViewGroup的話,我們也不用再次獲取了
SkinManagerOutdated.getInstance().register(this, content);
initview();
}
/**
* 當程式要求app修改面板的時候就會呼叫這個方法
* 該方法裡面寫實時的修改面板的方法
*/
@Override
public void onSkinChanged() {
//因為在onCreateView方法中已經將引數聲明瞭,所以這裡直接呼叫
//如果面板管理器向所有註冊過的view發出通知,改變面板;則就會出發該方法
//而該方法中寫的就是單個的修改view的面板
SkinManagerOutdated.getInstance().apply(this);
}
/**
* 當activity被finish的時候我們從換膚集合中移除這個activity,下一次換膚就不會對該activity產生作用了
*/
@Override
protected void onDestroy() {
super.onDestroy();
//反註冊,從面板註冊集合中移除,以免造成記憶體洩漏
SkinManagerOutdated.getInstance().unregister(this);
}
}
這樣的話,只要我們的activity繼承了這個baseskinactivity的話就可以實現換膚了,並且是無縫換膚和支援外掛或者app內部換膚。
這裡有幾點需要注意:
1:外掛換膚要宣告讀寫sd卡許可權,要不然獲取不到外掛。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
2:每個需要換膚的資源需要在xml中指明tag。例如:
<TextView
android:id="@+id/id_tv_tip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/id_iv_icon"
android:layout_toRightOf="@id/id_iv_icon"
android:text="鴻洋"
android:tag="skin:skin_item_text_color:textColor"
android:textColor="@color/skin_item_text_color" />
其中tag就是用於換膚的標識;有嚴格的格式規定。
以skin:開頭 +資源名+:+資源型別
skin:skin:skin_item_text_color:textColor
這是鴻洋大神寫的關於tag方式的換膚。
因為篇幅太長,我會在第二篇博文中繼續解析。這一片主要分析了tag方式的換膚。下一篇就分析一下侵入式換膚和tag式的區別以及不好之處。