0x0 背景
無論是出于使用者個性化的考慮,或者是不同場景下的氛圍渲染,用戶端應用存在着換膚的需求。本文舉出三種常見的換膚方案,并加以對比,以作後續參考。無論何種方案,換膚的核心都包含皮膚的管理,皮膚的加載,以及皮膚的生效。不同的方案在解決這些問題上有不同的思路。
0x1 手動重新設定UI資源
這種方式最簡單,在業務代碼裡面手動寫設定新皮膚的邏輯,當新皮膚下發時,回調該邏輯重新設定UI資源,就達到了換膚的邏輯。這種方案思路簡單,但是業務入侵的,需要手動寫代碼,有維護成本。而且由于需要手動寫重新設定UI的邏輯,是以一般不會對所有的控件都更換UI資源,是以換膚的範圍存在局限性。
public void onSkinChanged(JSONObject newSkin) { int newTextColor = newSkin.optInt("my_text_color"); TextView myTextView = (TextView)findViewById("R.id.mytext"); myTextView.setTextColor(newTextColor); String newImageUrl = newSkin.optString("my_img_url"); UrlImageView myImageView = (UrlImageView)findViewById("R.id.myimage"); myImageView.setImageUrl(newImageUrl); }
0x2 自定義資源架構
Android原生的資源都是通過
Resources加載定義在res目錄下的xml中的資源。由于res目錄下的資源是随打包釋出的,是以無法做到動态替換。是以這裡的思路是自定義一個CustomResources,并自定義resources目錄,裡面區分default,night, custom等等。default目錄下是預設資源,其他目錄下是特定場景的皮膚資源,其id和default目錄保持一緻,就可以實作替換。
resources |--- default |--- res |--- drawable |--- night |--- res |--- drawable
CustomResources可以加載APK包裡面的預設資源,也可以加載從服務端下發的資源。在應用開發過程中,不能再使用系統的Resources,必須使用CustomResources,否則無法實作換膚。
TextView myTextView = (TextView)findViewById("R.id.mytext"); myTextView.setTextColor(getCustomResrouce().getColor(R.color.mytextcolor));
當然至此為止還不能cover布局xml中直接使用資源id的場景,如果要cover,需要hook系統resources。這種方式需要在應用開發之初就有換膚的設計,開發團隊内部約定統一用CustomResources而不用系統的資源。
0x3 Hook系統LayoutInflater
這種思路是hook系統的LayoutInflater,在解析布局xml生成View的時候,替換成繼承了換膚邏輯的子類。
ximsfei/Android-skin-support是這種思路的代表。替換LayoutInflater的方式是:
private void installLayoutFactory(Context context) { LayoutInflater layoutInflater = LayoutInflater.from(context); try { Field field = LayoutInflater.class.getDeclaredField("mFactorySet"); field.setAccessible(true); field.setBoolean(layoutInflater, false); LayoutInflaterCompat.setFactory(layoutInflater, getSkinDelegate(context)); } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) { e.printStackTrace(); } }
在
的實作方案中,下發皮膚其實是在一個隻包含資源檔案的APK檔案中,保證資源APK中的資源命名和預設的保持一緻就能實作替換。讀取APK檔案中的資源的方式是:
public Resources getSkinResources(String skinPkgPath) { try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, skinPkgPath); Resources superRes = mAppContext.getResources(); return new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration()); } catch (Exception e) { e.printStackTrace(); } return null; }
這種方式對于應用開發者來說幾乎是透明的,對于業務也沒有侵入性,尤其是已有的應用代碼上疊代增加換膚功能,這是一種比較好的方式。