Android換膚原理和Android-Skin-Loader框架解析
Android換膚技術(shù)已經(jīng)是很久之前就已經(jīng)被成熟使用的技術(shù)了,然而我最近才在學(xué)習(xí)和接觸熱修復(fù)的時(shí)候才看到。在看了一些換膚的方法之后,并且對市面上比較認(rèn)可的Android-Skin-Loader換膚框架的源碼進(jìn)行了分析總結(jié)。再次記錄一下祭奠自己逝去的時(shí)間。
換膚介紹換膚本質(zhì)上是對資源的一中替換包括、字體、顏色、背景、圖片、大小等等。當(dāng)然這些我們都有成熟的api可以通過控制代碼邏輯做到。比如View的修改背景顏色 setBackgroundColor ,TextView的 setTextSize 修改字體等等。但是作為程序員我們怎么能忍受對每個(gè)頁面的每個(gè)元素一個(gè)行行代碼做換膚處理呢?我們需要用最少的代碼實(shí)現(xiàn)最容易維護(hù)和使用效果完美(動(dòng)態(tài)切換,及時(shí)生效)的換膚框架。
換膚方式一:切換使用主題Theme使用相同的資源id,但在不同的Theme下邊自定義不同的資源。我們通過主動(dòng)切換到不同的Theme從而切換界面元素創(chuàng)建時(shí)使用的資源。這種方案的代碼量不多發(fā),而且有個(gè)很明顯的缺點(diǎn)不支持已經(jīng)創(chuàng)建界面的換膚,必須重新加載界面元素。 GitHub Demo
換膚方式二:加載資源包加載資源包是各種應(yīng)用程序都在使用的換膚方法,例如我們最常用的輸入法皮膚、瀏覽器皮膚等等。我們可以將皮膚的資源文件放入安裝包內(nèi)部,也可以進(jìn)行下載緩存到磁盤上。Android的應(yīng)用程序可以使用這種方式進(jìn)行換膚。GitHub上面有一個(gè)start非常高的換膚框架 Android-Skin-Loader 就是通過加載資源包對app進(jìn)行換膚。對這個(gè)框架的分析這個(gè)也是這篇文章主要的講述內(nèi)容。
對比一下發(fā)現(xiàn)切換Theme可以進(jìn)行小幅度的換膚設(shè)置(比如某個(gè)自定義組件的主題),而如果我們想要對整個(gè)app做主題切換那么通過加載資源包的這種方式目前應(yīng)該說是比較好的了。
Android換膚知識點(diǎn) 換膚相應(yīng)的API我們先來看一下Android提供的一些基本的api,通過使用這些api可以在App內(nèi)部進(jìn)行資源對象的替換。
public class Resources{ public String getString(int id)throws NotFoundException {CharSequence res = mAssets.getResourceText(id);if (res != null) { return res;}throw new NotFoundException('String resource ID #0x' + Integer.toHexString(id)); } public Drawable getDrawable(int id)throws NotFoundException {/********部分代碼省略*******/ } public int getColor(int id)throws NotFoundException {{/********部分代碼省略*******/ } /********部分代碼省略*******/}
這個(gè)是我們常用的Resources類的api,我們通常可以使用在資源文件中定義的 @+id String類型,然后在編譯出的R.java中對應(yīng)的資源文件生產(chǎn)的id(int類型),從而通過這個(gè)id(int類型)調(diào)用Resources提供的這些api獲取到對應(yīng)的資源對象。這個(gè)在同一個(gè)app下沒有任何問題,但是在皮膚包中我們怎么獲取這個(gè)id值呢。
public class Resources{ /********部分代碼省略*******/ /*** 通過給的資源名稱返回一個(gè)資源的標(biāo)識id。*@paramname 描述資源的名稱*@paramdefType 資源的類型*@paramdefPackage 包名**@return返回資源id,0標(biāo)識未找到該資源*/ public int getIdentifier(String name, String defType, String defPackage){if (name == null) { throw new NullPointerException('name is null');}try { return Integer.parseInt(name);} catch (Exception e) { // Ignore}return mAssets.getResourceIdentifier(name, defType, defPackage); }}
Resources提供了可以通過 @+id 、Type、PackageName這三個(gè)參數(shù)就可以在AssetManager中尋找相應(yīng)的PackageName中有沒有Type類型并且id值都能與參數(shù)對應(yīng)上的id,進(jìn)行返回。然后我們可以通過這個(gè)id再調(diào)用Resource的獲取資源的api就可以得到相應(yīng)的資源。
這里我們需要注意的一點(diǎn)是 getIdentifier(String name, String defType, String defPackage) 方法和 getString(int id) 方法所調(diào)用Resources對象的mAssets對象必須是同一個(gè),并且包含有PackageName這個(gè)資源包。
AssetManager構(gòu)造怎么構(gòu)造一個(gè)包含特定packageName資源的AssetManager對象實(shí)例呢?
public final class AssetManagerimplements AutoCloseable{ /********部分代碼省略*******/ /*** Create a new AssetManager containing only the basic system assets.* Applications will not generally use this method, instead retrieving the* appropriate asset manager with {@linkResources#getAssets}. Not for* use by applications.* {@hide}*/ public AssetManager(){synchronized (this) { if (DEBUG_REFS) {mNumRefs = 0;incRefsLocked(this.hashCode()); } init(false); if (localLOGV) Log.v(TAG, 'New asset manager: ' + this); ensureSystemAssets();} }
從AssetManager的構(gòu)造函數(shù)來看有 {@hide} 的朱姐,所以在其他類里面是直接創(chuàng)建AssetManager實(shí)例。但是不要忘記Java中還有反射機(jī)制可以創(chuàng)建類對象。
AssetManager assetManager = AssetManager.class.newInstance();
讓創(chuàng)建的assetManager包含特定的PackageName的資源信息,怎么辦?我們在AssetManager中找到相應(yīng)的api可以調(diào)用。
public final class AssetManagerimplements AutoCloseable{ /********部分代碼省略*******/ /*** 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){synchronized (this) { int res = addAssetPathNative(path); if (mStringBlocks != null) {makeStringBlocks(mStringBlocks); } return res;} }}
同樣改方法也不支持外部調(diào)用,我們只能通過反射的方法來調(diào)用。
/*** apk路徑*/String apkPath = Environment.getExternalStorageDirectory()+'/skin.apk';AssetManager assetManager = null;try { AssetManager assetManager = AssetManager.class.newInstance(); AssetManager.class.getDeclaredMethod('addAssetPath', String.class).invoke(assetManager, apkPath);} catch (Throwable th) { th.printStackTrace();}
至此我們可以構(gòu)造屬于自己換膚的Resources了。
換膚Resources構(gòu)造public Resources getSkinResources(Context context){ /*** 插件apk路徑*/ String apkPath = Environment.getExternalStorageDirectory()+'/skin.apk'; AssetManager assetManager = null; try {AssetManager assetManager = AssetManager.class.newInstance();AssetManager.class.getDeclaredMethod('addAssetPath', String.class).invoke(assetManager, apkPath); } catch (Throwable th) {th.printStackTrace(); } return new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());} 使用資源包中的資源換膚
我們將上述所有的代碼組合在一起就可以實(shí)現(xiàn),使用資源包中的資源對app進(jìn)行換膚。
public Resources getSkinResources(Context context){ /*** 插件apk路徑*/ String apkPath = Environment.getExternalStorageDirectory()+'/skin.apk'; AssetManager assetManager = null; try {AssetManager assetManager = AssetManager.class.newInstance();AssetManager.class.getDeclaredMethod('addAssetPath', String.class).invoke(assetManager, apkPath); } catch (Throwable th) {th.printStackTrace(); } return new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());}@Overrideprotected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ImageView imageView = (ImageView) findViewById(R.id.imageView); TextView textView = (TextView) findViewById(R.id.text); /*** 插件資源對象*/ Resources resources = getSkinResources(this); /*** 獲取圖片資源*/ Drawable drawable = resources.getDrawable(resources.getIdentifier('night_icon', 'drawable','com.tzx.skin')); /*** 獲取文本資源*/ int color = resources.getColor(resources.getIdentifier('night_color','color','com.tzx.skin')); imageView.setImageDrawable(drawable); textView.setText(text);}
通過上述介紹,我們可以簡單的對當(dāng)前頁面進(jìn)行換膚了。但是想要做出一個(gè)一個(gè)成熟換膚框架那么僅僅這些還是不夠的,提高一下我們的思維高度,如果我們在View創(chuàng)建的時(shí)候就直接使用皮膚資源包中的資源文件,那么這無疑就使換膚更加的簡單已維護(hù)。
LayoutInflater.Factory看過我前一篇 遇見LayoutInflater&Factory 文章的這部分可以省略掉.
很幸運(yùn)Android給我們在View生產(chǎn)的時(shí)候做修改提供了法門。
public abstract class LayoutInflater{ /***部分代碼省略****/ public interface Factory{public View onCreateView(String name, Context context, AttributeSet attrs); } public interface Factory2extends Factory{public View onCreateView(View parent, String name, Context context, AttributeSet attrs); } /***部分代碼省略****/}
我們可以給當(dāng)前的頁面的Window對象在創(chuàng)建的時(shí)候設(shè)置Factory,那么在Window中的View進(jìn)行創(chuàng)建的時(shí)候就會(huì)先通過自己設(shè)置的Factory進(jìn)行創(chuàng)建。Factory使用方式和相關(guān)注意事項(xiàng)請移位到 遇見LayoutInflater&Factory ,關(guān)于Factory的相關(guān)知識點(diǎn)盡在其中。
Android-Skin-Loader解析 初始化 初始化換膚框架,導(dǎo)入需要換膚的資源包(當(dāng)前為一個(gè)apk文件,其中只有資源文件)。public class SkinApplicationextends Application{public void onCreate(){super.onCreate();initSkinLoader();}/*** Must call init first*/private void initSkinLoader(){SkinManager.getInstance().init(this);SkinManager.getInstance().load();}} 構(gòu)造換膚對象 導(dǎo)入需要換膚的資源包,并構(gòu)造換膚的Resources實(shí)例。
/*** Load resources from apk in asyc task*@paramskinPackagePath path of skin apk*@paramcallback callback to notify user*/public void load(String skinPackagePath,final ILoaderListener callback){new AsyncTask<String, Void, Resources>() {protected void onPreExecute(){if (callback != null) {callback.onStart();}};@Overrideprotected Resources doInBackground(String... params){try {if (params.length == 1) {String skinPkgPath = params[0];File file = new File(skinPkgPath); if(file == null || !file.exists()){return null;}PackageManager mPm = context.getPackageManager();//檢索程序外的一個(gè)安裝包文件PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);//獲取安裝包報(bào)名skinPackageName = mInfo.packageName; //構(gòu)建換膚的AssetManager實(shí)例AssetManager assetManager = AssetManager.class.newInstance();Method addAssetPath = assetManager.getClass().getMethod('addAssetPath', String.class);addAssetPath.invoke(assetManager, skinPkgPath); //構(gòu)建換膚的Resources實(shí)例Resources superRes = context.getResources();Resources skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());//存儲當(dāng)前皮膚路徑SkinConfig.saveSkinPath(context, skinPkgPath);skinPath = skinPkgPath;isDefaultSkin = false;return skinResource;}return null;} catch (Exception e) {e.printStackTrace();return null;}};protected void onPostExecute(Resources result){mResources = result;if (mResources != null) {if (callback != null) callback.onSuccess();//更新多有可換膚的界面notifySkinUpdate();}else{isDefaultSkin = true;if (callback != null) callback.onFailed();}};}.execute(skinPackagePath);} 定義基類 換膚頁面的基類的通用代碼實(shí)現(xiàn)基本換膚功能。
public class BaseFragmentActivityextends FragmentActivityimplements ISkinUpdate,IDynamicNewView{/***部分代碼省略****///自定義LayoutInflater.Factory private SkinInflaterFactory mSkinInflaterFactory;@Override protected void onCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState); try { //設(shè)置LayoutInflater的mFactorySet為true,表示還未設(shè)置mFactory,否則會(huì)拋出異常。 Field field = LayoutInflater.class.getDeclaredField('mFactorySet'); field.setAccessible(true); field.setBoolean(getLayoutInflater(), false); //設(shè)置LayoutInflater的MFactory mSkinInflaterFactory = new SkinInflaterFactory(); getLayoutInflater().setFactory(mSkinInflaterFactory);} catch (NoSuchFieldException e) { e.printStackTrace();} catch (IllegalArgumentException e) { e.printStackTrace();} catch (IllegalAccessException e) { e.printStackTrace();} } @Override protected void onResume(){super.onResume();//注冊皮膚管理對象SkinManager.getInstance().attach(this); }@Override protected void onDestroy(){super.onDestroy();//反注冊皮膚管理對象SkinManager.getInstance().detach(this); } /***部分代碼省略****/} SkinInflaterFactory SkinInflaterFactory進(jìn)行View的創(chuàng)建并對View進(jìn)行換膚。 構(gòu)造View
public class SkinInflaterFactoryimplements Factory{ /***部分代碼省略****/ public View onCreateView(String name, Context context, AttributeSet attrs){//讀取View的skin:enable屬性,false為不需要換膚// if this is NOT enable to be skined , simplly skip itboolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);if (!isSkinEnable){return null;}//創(chuàng)建ViewView view = createView(context, name, attrs);if (view == null){ return null;}//如果View創(chuàng)建成功,對View進(jìn)行換膚parseSkinAttr(context, attrs, view);return view; } //創(chuàng)建View,類比可以查看LayoutInflater的createViewFromTag方法 private View createView(Context context, String name, AttributeSet attrs){View view = null;try { if (-1 == name.indexOf(’.’)){if ('View'.equals(name)) { view = LayoutInflater.from(context).createView(name, 'android.view.', attrs);} if (view == null) { view = LayoutInflater.from(context).createView(name, 'android.widget.', attrs);} if (view == null) { view = LayoutInflater.from(context).createView(name, 'android.webkit.', attrs);} }else {view = LayoutInflater.from(context).createView(name, null, attrs); } L.i('about to create ' + name);} catch (Exception e) { L.e('error while create 【' + name + '】 : ' + e.getMessage()); view = null;}return view; }} 對生產(chǎn)的View進(jìn)行換膚
public class SkinInflaterFactoryimplements Factory{ //存儲當(dāng)前Activity中的需要換膚的View private List<SkinItem> mSkinItems = new ArrayList<SkinItem>(); /***部分代碼省略****/ private void parseSkinAttr(Context context, AttributeSet attrs, View view){//當(dāng)前View的所有屬性標(biāo)簽List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();for (int i = 0; i < attrs.getAttributeCount(); i++){ String attrName = attrs.getAttributeName(i); String attrValue = attrs.getAttributeValue(i);if(!AttrFactory.isSupportedAttr(attrName)){continue; } //過濾view屬性標(biāo)簽中屬性的value的值為引用類型 if(attrValue.startsWith('@')){try { int id = Integer.parseInt(attrValue.substring(1)); String entryName = context.getResources().getResourceEntryName(id); String typeName = context.getResources().getResourceTypeName(id); //構(gòu)造SkinAttr實(shí)例,attrname,id,entryName,typeName //屬性的名稱(background)、屬性的id值(int類型),屬性的id值(@+id,string類型),屬性的值類型(color) SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName); if (mSkinAttr != null) {viewAttrs.add(mSkinAttr); }} catch (NumberFormatException e) { e.printStackTrace();} catch (NotFoundException e) { e.printStackTrace();} }}//如果當(dāng)前View需要換膚,那么添加在mSkinItems中if(!ListUtils.isEmpty(viewAttrs)){ SkinItem skinItem = new SkinItem(); skinItem.view = view; skinItem.attrs = viewAttrs; mSkinItems.add(skinItem); //是否是使用外部皮膚進(jìn)行換膚 if(SkinManager.getInstance().isExternalSkin()){skinItem.apply(); }} }} 資源獲取
通過當(dāng)前的資源id,找到對應(yīng)的資源name。再從皮膚包中找到該資源name所對應(yīng)的資源id。
public class SkinManagerimplements ISkinLoader{ /***部分代碼省略****/ public int getColor(int resId){int originColor = context.getResources().getColor(resId);//是否沒有下載皮膚或者當(dāng)前使用默認(rèn)皮膚if(mResources == null || isDefaultSkin){ return originColor;}//根據(jù)resId值獲取對應(yīng)的xml的的@+id的String類型的值String resName = context.getResources().getResourceEntryName(resId);//更具resName在皮膚包的mResources中獲取對應(yīng)的resIdint trueResId = mResources.getIdentifier(resName, 'color', skinPackageName);int trueColor = 0;try{ //根據(jù)resId獲取對應(yīng)的資源value trueColor = mResources.getColor(trueResId);}catch(NotFoundException e){ e.printStackTrace(); trueColor = originColor;}return trueColor; } public Drawable getDrawable(int resId){...}} 其他
除此之外再增加以下對于皮膚的管理api(下載、監(jiān)聽回調(diào)、應(yīng)用、取消、異常處理、擴(kuò)展模塊等等)。
來自:http://dandanlove.com/2017/11/27/android-skin-changed/
相關(guān)文章:
1. python爬蟲實(shí)戰(zhàn)之制作屬于自己的一個(gè)IP代理模塊2. python實(shí)現(xiàn)讀取類別頻數(shù)數(shù)據(jù)畫水平條形圖案例3. Android Studio設(shè)置顏色拾色器工具Color Picker教程4. python 利用toapi庫自動(dòng)生成api5. Spring如何使用xml創(chuàng)建bean對象6. HTML 絕對路徑與相對路徑概念詳細(xì)7. Java程序的編碼規(guī)范(6)8. IntelliJ IDEA設(shè)置默認(rèn)瀏覽器的方法9. python實(shí)現(xiàn)PolynomialFeatures多項(xiàng)式的方法10. python實(shí)現(xiàn)在內(nèi)存中讀寫str和二進(jìn)制數(shù)據(jù)代碼
