網(wǎng)易云音樂、手機QQ的換膚是怎么做到的
Android的主題換膚 ,可插件化提供皮膚包,無需Activity的重啟直接實現(xiàn)無縫切換,可高仿網(wǎng)易云音樂的主題換膚。
這個鏈接是本次的Demo打包出來的樣本SkinChangeDemo,可以去下載下來先試試效果,皮膚文件需放到存儲卡的根目錄下。
關于Android的主題換膚都是個老生常談的問題了。網(wǎng)上給出的方案也是層出不窮,最近我也是很想去了解這方面的知識,所以我去搜一下就會有一大堆介紹這方面的文章,但是最后的結果都是不盡人意的,有的確實是給出了一些比較好的解決方案,但是沒有一個實質(zhì)性的Demo可以參考,所以也只能是紙上談兵罷了,有的呢,確實是給出了一個參考的Demo但是最后的結果不是我想要的。關于Android的換膚方案技術的總結,這篇文章還是挺有參考價值的Android換膚技術總結。感興趣的同學可以去了解下,就當做是一個知識的普及。
今天我要實現(xiàn)的一個換膚方案是基于github上的這個開源框架Android-Skin-Loader。
這個框架的換膚機制是使用動態(tài)加載的機制去加載皮膚包里面的內(nèi)容,無需Acitvity重啟即可實現(xiàn)皮膚的實時更換,皮膚包是可以與原安裝包相分離的,需要自己定做(這個皮膚包其實也就是一個普通的Android項目,只是只有資源文件沒有類文件而已),這樣做的好處就是可以在線提供皮膚包供用戶去下載,也可以大大的減少安裝包的體積,同時也很好的實現(xiàn)了插件化。其實這個框架是可以拿來直接來用的,直接幾行代碼基本上就可以解決Android的主題換膚,但是作為一個程序員怎么可以只是簡單的知道怎么用就行了嗎?如果真是這樣就真的太low了。遇到一個好的開源項目我們至少需要把他的源碼大致看一下,走一下基本的流程,了解一下他的基本原理,這樣我們在技術上才會有所提升。本文實現(xiàn)的Demo是基于在我前段時間發(fā)布的Android Material Design 兼容庫的使用詳解一文中的Demo改進的。最后實現(xiàn)的App也是MaterialDesign的設計風格。
好了說了這么多,通過本文你可以學到什么,這個可能是大家比較關心的一點
- 設計出一個基于MaterialDesign風格的App
- 自己實現(xiàn)一個主題換膚的框架
- 高仿網(wǎng)易云音樂的主題換膚(ps:其實本來我想以這個作為標題的,這樣做也可以增加流量,可我不想單純的做個標題黨,給大家?guī)砀韶洸攀亲钪匾?
- 讓你的技術更上一層樓(這個說了也是白說)
說了這么久可能就會有人按捺不住了:我是來看干貨的,不是來這聽你瞎BB的。不要急干貨馬上來。如果實在感覺枯燥可以直接跳到文末去看源碼。下面先來幾張效果圖來爽一下
這個是網(wǎng)易云音樂的換膚界面,他提供了幾個默認的,也提供了可以在線下載的主題,他的切換效果還是非常贊的,用過這個軟件的同學肯定是知道的。學習完本文后就可以做出類似于這個換膚效果。
這個動態(tài)圖是最終我們這個Demo實現(xiàn)的效果,這個Demo總體來說還是比較簡單的,只提供了三種皮膚。實現(xiàn)了一個基本的換膚效果,主要還是用于拿來學習使用。當然更復雜的換膚基于這個Demo也是可以辦到的,這里主要還是去講解原理。
在介紹之前還需要先給大家普及一下LayoutInflaterFactory相關的知識。如果已經(jīng)知道了這方面的知識點,下面這一段可以直接略過。
對于LayoutInflater大家可能都不太陌生,當你需要把xml文件轉(zhuǎn)化成對應View的時候就必須用到它,我想對于他怎么使用的就不用我介紹了。LayoutInflater 提供了setFactory(LayoutInflater.Factory factory)和setFactory2(LayoutInflater.Factory2 factory)兩個方法可以讓你去自定義布局的填充(有點類似于過濾器,我們在填充這個View之前可以做一些額外的事,但不完全是),F(xiàn)actory2 是在API 11才添加的。 他們提供了下面的方法讓你去重寫。在這里面你完全可以自己去定義去創(chuàng)建你所想要的View,如果在你在重寫的方法中返回null的話,就會以系統(tǒng)默認的方式去創(chuàng)建View。
- View onCreateView(String name, Context context, AttributeSet attrs)//LayoutInflater.Factory
- View onCreateView(View parent, String name, Context context, AttributeSet attrs)//LayoutInflater.Factory2
LayoutInflater都被設置了一個默認的Factory,Activity 是實現(xiàn)了LayoutInflater.Factory接口的,因此在你的Activity中直接重寫onCreateView就可以自定義View的填充了。
下面這句是對LayoutInflater.Factory一個比較好的理解
- Inflating your own custom views, instead of letting the system do it
這個也是這個Demo其中的一個比較重要技術點。如果有想更詳細了解的文末會有參考鏈接。
下面就正式開始介紹怎么去做這個主題換膚吧。
先來看看這個Demo的項目結構:
至于xRecyclerView可以不用管,這里我們用不到(這是之前用到的,與本次無關),他只是一個RecyclerView的一個擴展框架,支持下拉刷新和上拉加載,是一個在github上的一個開源項目。
這里我們直接來看看lib_skinloader這個庫吧(這里面的內(nèi)容大部分是來源于Android-Skin-Loader這個框架,我只做了部分修改,主要是適配AppCompatActivity,原框架是基于最初的Activty開發(fā)的,在這里再次感謝開源作者),這個庫就是今天所講的核心內(nèi)容
我們都知道在Android中如果想去獲取資源文件都必須通過Resources去獲取。這個庫的核心思想就是動態(tài)的去加載第三方包里面的包,獲取到其Resources然后以獲取到的這個Resources去獲取第三方包里面的資源內(nèi)容,最后設置到我們有需響應皮膚更改的View上。
這里我就只介紹load和base兩個包,其他包的內(nèi)容在講解的時候會涉及到
1.load包
我們先來看看這個load包里面的內(nèi)容(其實這里就是今天核心內(nèi)容的核心)。
里面有兩個類文件:SkinInflaterFactory、SkinManager
我們先來看看SkinManager的實現(xiàn),直接跳到load方法
- public void load(String skinPackagePath, final ILoaderListener callback) {
- new AsyncTask<String, Void, Resources>() {
- protected void onPreExecute() {
- if (callback != null) {
- callback.onStart();
- }
- }
- @Override
- protected Resources doInBackground(String... params) {
- try {
- if (params.length == 1) {
- String skinPkgPath = params[0];
- Log.i("loadSkin", skinPkgPath);
- File file = new File(skinPkgPath);
- if (file == null || !file.exists()) {
- return null;
- }
- PackageManager mPm = context.getPackageManager();
- PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
- skinPackageName = mInfo.packageName;
- AssetManager assetManager = AssetManager.class.newInstance();
- Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
- addAssetPath.invoke(assetManager, skinPkgPath);
- Resources superRes = context.getResources();
- Resources skinResource = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
- 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ù),第一個是皮膚包的路徑,第二個就是一個簡單的回調(diào)
其中doInBackground方法里面就實現(xiàn)了動態(tài)的去獲取皮膚包的Resources,當獲取成功之后,在onPostExecute方法中就將這個Resources賦值到我們定義好的變量中去,以方便我們之后的使用,注意到當獲取到的這個Resources不為空時,也就是我們已經(jīng)獲取到了皮膚包里面的資源,我們就調(diào)用notifySkinUpdate()這個方法來通知界面去更改皮膚,如果為空就還是使用默認的皮膚。
我們來看看notifySkinUpdate()的實現(xiàn)
這里很簡單,就是去遍歷mSkinObservers這個集合,然后去通知更新。對于ISkinUpdate是一個接口,每個需要皮膚更新的Activity都需要去實現(xiàn)這個接口。
SkinManager這個類里面還有諸如getColor(int resId)、getDrawable(int resId)這樣的方法,就是去獲取第三方包對應的資源文件,值得注意的是如果你的第三方包里沒有對應的資源文件,那么就會使用默認的資源文件,如果你有需求,你完全可以去添加一些類似getMipmap(int resID)這樣的方法。
對了,還有一個比較重要的方法忘了講
這個方法就是恢復到系統(tǒng)的默認主題,原理和load都差不多,實現(xiàn)還簡單了很多。SkinManager這個類就說這么多,詳細實現(xiàn)請到源碼中去查看,很多地方我都給了注釋。
我們再來看看SkinInflaterFactory,在這里面主要就是做一些填充View相關的一些工作。我實現(xiàn)的是LayoutInflaterFactory這個接口而不是文章之前提到的LayoutInflater.Factory這個接口是因為這里需要與AppCompatActivity兼容,如果你還是用之前的那個就會出現(xiàn)一些錯誤,反正我剛弄的時候是折騰了很久的。不管怎么樣原理始終是一樣的。SkinInflaterFactory的作用就是去搜集那些有需要響應皮膚更改的View。 我們來看看onCreateView的實現(xiàn)
首先我們先去判斷這個當前將要View是否有更改皮膚的需求,如果沒有我們就返回默認的實現(xiàn)。如果有,我們就自己去處理 來看看createView方法的實現(xiàn)
看起來很多,其實這個方法就是去動態(tài)的去創(chuàng)建View。
下面來看看parseSkinAttr的實現(xiàn):
這個方法其實就是去搜集View中換膚的時候可以更改的屬性,當我們換膚的時候就是去更改的這些屬性的值,這里你必須要注意一點,這個屬性的值一定要是引用類型的(例如:@color/red),千萬不能寫死,第二個if的判斷就是這個作用。到這里可能你就會有個疑問,我怎么知道哪些屬性在換膚的時候需要更改。如果你細心一點肯定注意到了這行代碼
- SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
這里有個AttrFacory他的作用就是根據(jù)屬性名,動態(tài)的去創(chuàng)建SkinAttr。在AttrFacory中定義了一些類似于這樣的常量:
這就是我們換膚的時候可以更改的那些屬性。SkinAttr是一個抽象類,比如background就會去創(chuàng)建一個BackgroundAttr,本項目所用到的屬性全都在attr包中。SkinAttr是比較靈活的一個地方,如果你有哪個屬性在換膚的時候需要更改,你就去實現(xiàn)一個對應的SkinAttr。
在parseSkinAttr這個方法的最后我們將View和SkinAttr封裝成了一個SkinItem然后添加到一個集合中去,最后還需注意的是,如果當前皮膚不是默認皮膚,一定要去apply一下,這樣做主要是防止換了皮膚啟動一些新的頁面有可能導致?lián)Q膚不及時的問題。SkinInflaterFactory這個類里面還提供了動態(tài)的添加SkinItem的方法,原理都和這里差不多,我就不過多的去說了。
load包里面的這兩個類講的差不多了,這里看懂了后面的內(nèi)容也就是小菜一碟了,我相信你看了這里再去看源碼一定會輕松地多。
2.base包
可以看見這個包里面肯定就是Activity、Fragment、Application的實現(xiàn),作用肯定就是封裝一些公用的方法和屬性在里面。
下面我們一個一個來分析
SkinBaseApplication:
可以看到這里我們對SkinManager做了一些初始化的操作。以后我們有需要皮膚更改需求的應用一定要記得一定要繼承于SkinBaseApplication。
SkinBaseActivity 我們來看看其onCreate方法
在這里使用了我們之前自定義的View的InflaterFactory,來替換默認的Factory。記住一定要在super.onCreate(savedInstanceState);這個方法之前調(diào)用。SkinBaseActivity里面還提供了動態(tài)添加可以響應皮膚更改需求的View的相關方法。當然需要響應換膚更改的Activity都需要繼承SkinBaseActivity。詳細實現(xiàn)請看源碼。
SkinBaseFragment 這個和SkinBaseActivity的思想差不多。具體實現(xiàn)看源碼,這里我只是給大家提供這個換膚框架的思想,讓大家在看源碼的時候更輕松。
這個框架就介紹到這,下面我們來看看怎么去使用。
在使用的時候一定要記得要Activity要去繼承于SkinBaseActivity,F(xiàn)ragment要繼承于SkinBaseFragment,Application要繼承于SkinBaseApplication。當然把這個框架做為你的項目依賴項肯定是必不可少的。為了Demo的簡單,這里我只使用了下面三個顏色作為可以換膚的資源,當然如果你想要使用drawable文件也是可以辦到的,前提是你一定要把這個Demo看懂。
來看一個布局文件
其中 xmlns:skin=”http://schemas.android.com/android/skin“ 是我們自定義的,在SkinConfig有。 我們只需在有皮膚更改需求的View中加入skin:enable=”true” 就OK了。
再來看看MainActicvity的部分代碼
這里就是動態(tài)的添加有皮膚更改需求的View。
上面就介紹完了在布局文件中使用方法和在代碼中使用方法。
我們應該怎么去換膚呢?很簡單,只需調(diào)用SkinManager的load方法就可以了,把皮膚路徑傳進去就可以了,我的這個Demo為了簡單起見,沒有做在線換膚的功能,只是在本地提供了可以更換的皮膚,看到這里我相信你對怎樣在線換膚已經(jīng)有想法了。
最最后我們來看看怎么去開發(fā)皮膚包。其實這個是最簡單的,皮膚包實際上就是一個基本的Android項目,里面不包含類文件,只有資源文件。這里只需注意 這里的資源文件名字一定要和原項目中的相同,并且只用包含那些在皮膚更改時需要改變的那些就行了!例如我的這個Demo就只是簡單對上面的三種顏色做了簡單的切換。開發(fā)了棕色和黑色兩款皮膚,所以資源文件中只有三個color的值,開發(fā)完成之后我們需要將其打包成apk文件,為防止用戶點擊安裝,我們將其后綴改成了skin,這樣做也具有標識性。如果還是不太清楚可以直接去源碼中查看。
這下再來看一看文章開頭效果圖是不是突然變得有思路了,快動起你的小手指去敲一個主題換膚的框架吧~~~