本專欄專一分享大型Bat面試知識,後續會持續更新,喜歡的話麻煩點擊一個關注
面試官: 網易雲QQ的換膚是怎麼作到的,你對換膚有了解嗎?看過換膚的原理沒?android
心理分析:沒有接觸過換膚技術 第一次聽到該名詞確定會很茫然。面試官考的是對資源加載,監聽佈局,有沒有了解.本文從換膚實戰一對一講解。告訴你如何作以及實現。文章末尾帶換膚項目源碼git
求職者: 從監聽佈局開始到 換膚原理,詳細給面試官講解換膚的原理程序員
接下來咱們一塊兒分享這篇乾貨。 Android的主題換膚 ,可插件化提供皮膚包,無需Activity的重啓直接實現無縫切換,可高仿網易雲音樂的主題換膚。github
這個連接是本次的Demo打包出來的樣本SkinChangeDemo,能夠去下載下來先試試效果,皮膚文件需放到存儲卡的根目錄下。面試
關於Android的主題換膚都是個老生常談的問題了。網上給出的方案也是層出不窮,最近我也是很想去了解這方面的知識,因此我去搜一下就會有一大堆介紹這方面的文章,可是最後的結果都是不盡人意的,有的確實是給出了一些比較好的解決方案,可是沒有一個實質性的Demo能夠參考,因此也只能是紙上談兵罷了,有的呢,確實是給出了一個參考的Demo可是最後的結果不是我想要的。關於Android的換膚方案技術的總結,這篇文章仍是挺有參考價值的Android換膚技術總結。感興趣的同窗能夠去了解下,就當作是一個知識的普及。
今天我要實現的一個換膚方案是基於github上的這個開源框架Android-Skin-Loader。架構
這個框架的換膚機制是使用動態加載的機制去加載皮膚包裏面的內容,無需Acitvity重啓便可實現皮膚的實時更換,皮膚包是能夠與原安裝包相分離的,須要本身定作(這個皮膚包其實也就是一個普通的Android項目,只是只有資源文件沒有類文件而已),這樣作的好處就是能夠在線提供皮膚包供用戶去下載,也能夠大大的減小安裝包的體積,同時也很好的實現了插件化。其實這個框架是能夠拿來直接來用的,直接幾行代碼基本上就能夠解決Android的主題換膚,可是做爲一個程序員怎麼能夠只是簡單的知道怎麼用就好了嗎?若是真是這樣就真的太low了。遇到一個好的開源項目咱們至少須要把他的源碼大體看一下,走一下基本的流程,瞭解一下他的基本原理,這樣咱們在技術上纔會有所提高。本文實現的Demo是基於在我前段時間發佈的Android Material Design 兼容庫的使用詳解一文中的Demo改進的。最後實現的App也是MaterialDesign的設計風格。app
好了說了這麼多,經過本文你能夠學到什麼,這個多是你們比較關心的一點框架
說了這麼久可能就會有人按捺不住了:我是來看乾貨的,不是來這聽你瞎BB的。不要急乾貨立刻來。若是實在感受枯燥能夠直接跳到文末去看源碼。下面先來幾張效果圖來爽一下
這個是網易雲音樂的換膚界面,他提供了幾個默認的,也提供了能夠在線下載的主題,他的切換效果仍是很是讚的,用過這個軟件的同窗確定是知道的。學習完本文後就能夠作出相似於這個換膚效果。
這個動態圖是最終咱們這個Demo實現的效果,這個Demo整體來講仍是比較簡單的,只提供了三種皮膚。實現了一個基本的換膚效果,主要仍是用於拿來學習使用。固然更復雜的換膚基於這個Demo也是能夠辦到的,這裏主要仍是去講解原理。ide
在介紹以前還須要先給你們普及一下LayoutInflaterFactory相關的知識。若是已經知道了這方面的知識點,下面這一段能夠直接略過。佈局
對於LayoutInflater你們可能都不太陌生,當你須要把xml文件轉化成對應View的時候就必須用到它,我想對於他怎麼使用的就不用我介紹了。LayoutInflater 提供了setFactory(LayoutInflater.Factory factory)
和setFactory2(LayoutInflater.Factory2 factory)
兩個方法可讓你去自定義佈局的填充(有點相似於過濾器,咱們在填充這個View以前能夠作一些額外的事,但不徹底是),Factory2 是在API 11才添加的。 他們提供了下面的方法讓你去重寫。在這裏面你徹底能夠本身去定義去建立你所想要的View,若是在你在重寫的方法中返回null的話,就會以系統默認的方式去建立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 是實現了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這個庫吧(這裏面的內容大部分是來源於Android-Skin-Loader這個框架,我只作了部分修改,主要是適配AppCompatActivity,原框架是基於最初的Activty開發的,在這裏再次感謝開源做者),這個庫就是今天所講的核心內容
咱們都知道在Android中若是想去獲取資源文件都必須經過Resources去獲取。這個庫的核心思想就是動態的去加載第三方包裏面的包,獲取到其Resources而後以獲取到的這個Resources去獲取第三方包裏面的資源內容,最後設置到咱們有需響應皮膚更改的View上。
這裏我就只介紹load和base兩個包,其餘包的內容在講解的時候會涉及到
咱們先來看看這個load包裏面的內容(其實這裏就是今天核心內容的核心)。
裏面有兩個類文件:SkinInflaterFactory、SkinManager
咱們先來看看SkinManager的實現,直接跳到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); }
這個方法有兩個參數,第一個是皮膚包的路徑,第二個就是一個簡單的回調
其中doInBackground方法裏面就實現了動態的去獲取皮膚包的Resources,當獲取成功以後,在onPostExecute方法中就將這個Resources賦值到咱們定義好的變量中去,以方便咱們以後的使用,注意到當獲取到的這個Resources不爲空時,也就是咱們已經獲取到了皮膚包裏面的資源,咱們就調用notifySkinUpdate()這個方法來通知界面去更改皮膚,若是爲空就仍是使用默認的皮膚。
咱們來看看notifySkinUpdate()的實現
這裏很簡單,就是去遍歷mSkinObservers這個集合,而後去通知更新。對於ISkinUpdate是一個接口,每一個須要皮膚更新的Activity都須要去實現這個接口。
SkinManager這個類裏面還有諸如getColor(int resId)、getDrawable(int resId)這樣的方法,就是去獲取第三方包對應的資源文件,值得注意的是若是你的第三方包裏沒有對應的資源文件,那麼就會使用默認的資源文件,若是你有需求,你徹底能夠去添加一些相似getMipmap(int resID)這樣的方法。
對了,還有一個比較重要的方法忘了講
這個方法就是恢復到系統的默認主題,原理和load都差很少,實現還簡單了不少。SkinManager這個類就說這麼多,詳細實現請到源碼中去查看,不少地方我都給了註釋。
咱們再來看看SkinInflaterFactory,在這裏面主要就是作一些填充View相關的一些工做。我實現的是LayoutInflaterFactory這個接口而不是文章以前提到的LayoutInflater.Factory這個接口是由於這裏須要與AppCompatActivity兼容,若是你仍是用以前的那個就會出現一些錯誤,反正我剛弄的時候是折騰了好久的。無論怎麼樣原理始終是同樣的。SkinInflaterFactory的做用就是去搜集那些有須要響應皮膚更改的View。 咱們來看看onCreateView的實現
首先咱們先去判斷這個當前將要View是否有更改皮膚的需求,若是沒有咱們就返回默認的實現。若是有,咱們就本身去處理 來看看createView方法的實現
看起來不少,其實這個方法就是去動態的去建立View。
下面來看看parseSkinAttr的實現:
這個方法其實就是去搜集View中換膚的時候能夠更改的屬性,當咱們換膚的時候就是去更改的這些屬性的值,這裏你必需要注意一點,這個屬性的值必定要是引用類型的(例如:@color/red),千萬不能寫死,第二個if的判斷就是這個做用。到這裏可能你就會有個疑問,我怎麼知道哪些屬性在換膚的時候須要更改。若是你細心一點確定注意到了這行代碼
SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
這裏有個AttrFacory他的做用就是根據屬性名,動態的去建立SkinAttr。在AttrFacory中定義了一些相似於這樣的常量:
這就是咱們換膚的時候能夠更改的那些屬性。SkinAttr是一個抽象類,好比background就會去建立一個BackgroundAttr,本項目所用到的屬性全都在attr包中。SkinAttr是比較靈活的一個地方,若是你有哪一個屬性在換膚的時候須要更改,你就去實現一個對應的SkinAttr。
在parseSkinAttr這個方法的最後咱們將View和SkinAttr封裝成了一個SkinItem而後添加到一個集合中去,最後還需注意的是,若是當前皮膚不是默認皮膚,必定要去apply一下,這樣作主要是防止換了皮膚啓動一些新的頁面有可能致使換膚不及時的問題。SkinInflaterFactory這個類裏面還提供了動態的添加SkinItem的方法,原理都和這裏差很少,我就不過多的去說了。
load包裏面的這兩個類講的差很少了,這裏看懂了後面的內容也就是小菜一碟了,我相信你看了這裏再去看源碼必定會輕鬆地多。
能夠看見這個包裏面確定就是Activity、Fragment、Application的實現,做用確定就是封裝一些公用的方法和屬性在裏面。
下面咱們一個一個來分析
能夠看到這裏咱們對SkinManager作了一些初始化的操做。之後咱們有須要皮膚更改需求的應用必定要記得必定要繼承於SkinBaseApplication。
在這裏使用了咱們以前自定義的View的InflaterFactory,來替換默認的Factory。記住必定要在super.onCreate(savedInstanceState);這個方法以前調用。SkinBaseActivity裏面還提供了動態添加能夠響應皮膚更改需求的View的相關方法。固然須要響應換膚更改的Activity都須要繼承SkinBaseActivity。詳細實現請看源碼。
這個框架就介紹到這,下面咱們來看看怎麼去使用。
在使用的時候必定要記得要Activity要去繼承於SkinBaseActivity,Fragment要繼承於SkinBaseFragment,Application要繼承於SkinBaseApplication。固然把這個框架作爲你的項目依賴項確定是必不可少的。爲了Demo的簡單,這裏我只使用了下面三個顏色做爲能夠換膚的資源,固然若是你想要使用drawable文件也是能夠辦到的,前提是你必定要把這個Demo看懂。
來看一個佈局文件
其中 xmlns:skin=」http://schemas.android.com/android/skin「
是咱們自定義的,在SkinConfig有。 咱們只需在有皮膚更改需求的View中加入skin:enable=」true」 就OK了。
再來看看MainActicvity的部分代碼
這裏就是動態的添加有皮膚更改需求的View。
上面就介紹完了在佈局文件中使用方法和在代碼中使用方法。
咱們應該怎麼去換膚呢?很簡單,只需調用SkinManager的load方法就能夠了,把皮膚路徑傳進去就能夠了,個人這個Demo爲了簡單起見,沒有作在線換膚的功能,只是在本地提供了能夠更換的皮膚,看到這裏我相信你對怎樣在線換膚已經有想法了。
最最後咱們來看看怎麼去開發皮膚包。其實這個是最簡單的,皮膚包實際上就是一個基本的Android項目,裏面不包含類文件,只有資源文件。這裏只需注意 這裏的資源文件名字必定要和原項目中的相同,而且只用包含那些在皮膚更改時須要改變的那些就好了!例如個人這個Demo就只是簡單對上面的三種顏色作了簡單的切換。開發了棕色和黑色兩款皮膚,因此資源文件中只有三個color的值,開發完成以後咱們須要將其打包成apk文件,爲防止用戶點擊安裝,咱們將其後綴改爲了skin,這樣作也具備標識性。若是仍是不太清楚能夠直接去源碼中查看。
這下再來看一看文章開頭效果圖是否是忽然變得有思路了,快動起你的小手指去敲一個主題換膚的框架吧~~~
好了,本文到此結束。很感謝你的耐心看完!
200行代碼打造超越一線互聯網公司的換膚架構視頻教程
連接: https://pan.baidu.com/s/1d_d2LUnPEtSPU7n-N4zg-A 提取碼: j559 複製這段內容後打開百度網盤手機App,操做更方便哦
以下資料獲取加Q羣:892872246