android方便簡單的零侵入可擴充套件的換膚框架
目前的外掛化正如火如荼,外掛化開源的也不少,比如360開源的Replugin,滴滴的VirtualApk等等,當然我們今天的主題並不是外掛化,而是外掛化換膚;
android的換膚功能的實現基本有兩種,一種是應用內換膚,一種是外掛化換膚,應用內換膚比較簡單,基本都是在內部預置幾套面板,但是這樣的話,一兩套的面板來說還好,如果更多的話,會造成apk的體積非常大,很不好,如果我們能夠動態的通過網路下載面板,動態的去更換,豈不是更好;外掛化換膚,就能夠滿足這種使用場景,下面開始詳細實現該方案;
想要外掛話換膚,我們可以通過網路下載只有資源的apk資源包,雖然資源有了,但是我們有以下幾個問題需要解決:
1,如何去獲取外掛資源?
2,如何找到需要資源的view?
3,我們該通過view的什麼方法去設定資源?
對於第一個問題,由於是外掛中的資源,外掛中的資源id在宿主的R檔案中是不存在的,我們該怎麼找到對應外掛中的資源呢。
其實這就需要我們定義一套規則了,我們可以規定外掛資源名字必須和宿主中的資源名字一致,有了這個規則,那麼我們可以根據資源的名字進行對應了: 宿主id —— 資源名字 —— 外掛id
我們通過宿主的資源id值找到對應的資源名字:
getResources().getResourceEntryName(resId);
我們通過該方法就可以拿到資源的名字,這一步完成了通過宿主id找資源名字的過程
getResources().getResourceTypeName(resId);
通過該方法我們可以拿到資源的型別,是color還是drawable等等;
現在我們需要構造外掛的Resources物件了,也很簡單,通過Assetmanager的addAssetpath方法,不過需要反射,不懂該操作的可以自行百度原理,我就不詳細深入分析了,直接上程式碼:
public class PluginResProvider { public static String getPkg(Context context,String skinApkPath){ PackageInfo packageInfo = context.getPackageManager().getPackageArchiveInfo(skinApkPath, PackageManager.GET_UNINSTALLED_PACKAGES); return packageInfo !=null? packageInfo.packageName : null; } public static Resources getResources(Context context,String skinApkPath){ AssetManager assetManager =getAssetManager(skinApkPath); if (assetManager !=null) { Resources hostRes = context.getResources(); return new Resources(assetManager,hostRes.getDisplayMetrics(),hostRes.getConfiguration()); }else { return null; } } public static AssetManager getAssetManager(String skinApkPath){ try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, skinApkPath); return assetManager; } catch (Exception e) { e.printStackTrace(); return null; } } }
應該已經一目瞭然了吧,現在我們就可以通過資源名字拿到對應的外掛資源的id了,通過外掛的Resources的getIdentifier方法我們就拿到了外掛中的資源id,這樣我們就也可以在外掛的Resources物件中通過外掛的id來查詢資源了。
接下來就是我們需要知道哪些view需要換膚的功能,當然這裡分為兩個方向,一個是對layout檔案入手,一個是從程式碼層面入手,如果從layout入手,我們需要自己去給view定義一個換膚的標識屬性,然後在給layoutInflater設定factory,在解析的時候去收集那些具有換膚tag的view,這種方法優點就是支援了在xml中的配置,減少了開發者程式碼層面的操作,缺點就是侵入性比較大,不光需要開發者在佈局檔案中去配置換膚屬性,也需要進行註冊,並且無法支援自定義屬性的一些換膚操作,比如一個自定義view有個setTitleColor方法,顯然這種方式擴充套件性比較差,靈活度也不高。另一種方式就是通過程式碼的方式,由開發者自己通過程式碼去設定哪個view需要換膚,需要什麼方法換膚等等,憂點就是靈活度高,擴充套件性強,幾乎零侵入,只需要開發者遵守資源名字一致即可,不需要再佈局檔案再去學習新的換膚標籤屬性,註冊解註冊很靈活,擴充套件性強,缺點就是需要程式碼量會多一些。
基於以上,所以我選擇通過程式碼去實現換膚功能,為了能夠支援自定義view的屬性方法,能夠擴充套件,所以我認為封裝的時候不應該將換膚的具體方法寫死,而是應該提供一個介面讓開發者自己去實現,當然我們可以提供常用的方法就好,比如,setBackgorund,setBackgroundColor,setImageDrawable,setTextColor等等。還有就是,雖然本文的重點是換膚,但是我覺得我們不該侷限於此,如果我想把外掛中只要有id的資源都拿來用可不可以,比如,我們想用外掛中的anim,string,array等等,我們不應該侷限於換膚,它應該是一個可以拿到任何有id的資源,所以,我們也可以提供一個介面,讓開發者有能力去擴充套件他想要的任何外掛中的資源,我們只需要提供預設的即可,比如獲取color,drawable等等;
這是我封裝的包結構圖,首先fetchers包下是資源抓取器,主要是實現了一些預設獲取圖片和顏色的資源抓取,如果想擴充套件可以直接繼承ResFetcher類進行擴充套件:
public class ColorResFetcher extends ResFetcher<Integer> {
@Override
protected Integer getRes(Resources pluginRes, int pluginId) {
return pluginRes.getColor(pluginId);
}
@Override
public String name() {
return FetcherType.COLOR;
}
}
這個是顏色的抓取,由於資源的型別不是隨意的,所以,所有的資源型別我都預先定義在FetcherType類中:
@Retention(RetentionPolicy.SOURCE)
@StringDef({FetcherType.COLOR, FetcherType.DRAWABLE, FetcherType.STRING, FetcherType.ARRAY, FetcherType.ANIM, FetcherType.DIMEN, FetcherType.LAYOUT})
public @interface FetcherType {
String COLOR = "color", DRAWABLE = "drawable", STRING = "string",
ARRAY = "array", ANIM = "anim", DIMEN = "dimen", LAYOUT = "layout";
}
這裡包含了很多資源型別名稱,如果想要擴充套件,可以直接仿照ColorResFetcher去實現自己想要的資源。
public abstract class ResFetcher<T> {
public T getRes(ResProxy resProxy, ViewAttr attr) {
T pluginRes = null;
int pluginId = resProxy.getCacheId(attr.hostResId);
if (pluginId == 0) {
pluginId = resProxy.getPulginId(attr);
}
try {
pluginRes = getRes(resProxy.getPluginRes(), pluginId);
} catch (Exception e) {
}
if (pluginRes != null) {
resProxy.saveId(attr.hostResId, pluginId);
} else {
}
return pluginRes;
}
protected abstract T getRes(Resources pluginRes, int pluginId);
public abstract @FetcherType
String name();
}
如果想要註冊自己的資源抓取器,可以通過SkinCenter.get().addFetcher方法,下面看看SkinCenter這個類:
public class SkinCenter {
private static final String TAG = SkinCenter.class.getSimpleName();
private static SkinCenter skinCenter;
private WeakHashMap<View, SparseArray<ViewAttr>> cacheMap = new WeakHashMap<>();
private ResProxy mResProxy;
private SkinCenter() {
addDefaultFetchers();
addDefaultConverts();
}
@MainThread
public static SkinCenter get() {
if (skinCenter == null) {
skinCenter = new SkinCenter();
}
return skinCenter;
}
public void init(Context context) {
mResProxy = new ResProxy(context.getApplicationContext());
loadPlugin(context);
}
/**
* 新增預設的資源解析器
*/
private void addDefaultFetchers() {
addFetcher(new ColorResFetcher()).addFetcher(new DrawableResFetcher());
}
public SkinCenter addFetcher(ResFetcher fetcher) {
ResFetcherM.addFetcher(fetcher);
return this;
}
/**
* 新增預設的方法轉換器
*/
private void addDefaultConverts() {
addConvert(new SetBackground())
.addConvert(new SetBackgroundColor())
.addConvert(new SetTextColor())
.addConvert(new SetImageDrawable());
}
public SkinCenter addConvert(IViewConvert convert) {
ConvertM.addConvert(convert);
return this;
}
/**
* 載入外掛資源
* @param context
*/
private void loadPlugin(Context context) {
String skinPath = SkinCache.getCurSkinPath(context);
if (TextUtils.isEmpty(skinPath)){
loadDefaultSkin(context);
}else {
loadSkin(context,skinPath);
}
}
/**載入宿主預設的資源
* @param context
*/
public void loadDefaultSkin(Context context){
SkinCache.saveSkinPath(context,"");
if (mResProxy ==null){
mResProxy =new ResProxy(context.getApplicationContext());
}
mResProxy.setPlugin(context.getResources(),context.getPackageName());
notifySkinChanged();
}
/**提供外部呼叫載入外掛資源
* @param context
* @param skinPath
*/
public void loadSkin(Context context,String skinPath){
if (TextUtils.isEmpty(skinPath) || !new File(skinPath).exists()){
return;
}
String pkg = PluginResProvider.getPkg(context, skinPath);
Resources pluginRes =PluginResProvider.getResources(context,skinPath);
if (pkg ==null || pluginRes ==null){
return;
}
SkinCache.saveSkinPath(context,skinPath);
if (mResProxy ==null){
mResProxy =new ResProxy(context.getApplicationContext());
}
mResProxy.setPlugin(pluginRes,pkg);
notifySkinChanged();
}
public <V extends View> void apply(V view, int resId, String convertName) {
apply(view, resId, ConvertM.getConvert(convertName));
}
public <V extends View> void apply(V view, int resId, IViewConvert convert) {
if (mResProxy == null) {
init(view.getContext());
}
applyAndRegist(view, resId, convert);
}
private <T,V extends View> void applyAndRegist(V view, int resId, IViewConvert convert) {
SparseArray<ViewAttr> attrs = cacheMap.get(view);
if (attrs != null) {
ViewAttr viewAttr = attrs.get(resId);
if (viewAttr != null) {
viewAttr.apply(view, mResProxy);
}
} else {
attrs = new SparseArray<>();
String idName = mResProxy.getResourceEntryName(resId);
String idType = mResProxy.getResourceTypeName(resId);
ResFetcher<T> fetcher = ResFetcherM.getFetcher(idType);
if (idName != null && idType != null && fetcher != null) {
ViewAttr attr = new ViewAttr<T,V>(resId, idName, idType, convert, fetcher);
attrs.put(resId, attr);
cacheMap.put(view, attrs);
attr.apply(view, mResProxy);
} else {
Log.e(TAG, "no match res,idName=" + idName + " ,idType=" + idType + " ,fetcher=" + fetcher);
}
}
}
private void notifySkinChanged() {
Iterator it = cacheMap.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
View view = (View) entry.getKey();
SparseArray<ViewAttr> attrs = (SparseArray<ViewAttr>) entry.getValue();
for (int i = 0; i < attrs.size(); i++) {
attrs.valueAt(i).apply(view, mResProxy);
}
}
}
}
這個類時對外提供的一個核心類,主要負責換膚view的註冊,資源抓取器的註冊,初始化等等,我們無需關心該怎麼初始化,比如:SkinCenter.get().apply(iv_image, R.color.colorAccent, SetBackgroundColor.class.getSimpleName());
一句程式碼就已經完成了換膚功能,iv_image是你要換膚的view,第二個引數是資源id,最後是方法轉換器的類名,這個是預設已經寫好的,在上面的類結構圖裡面已經列舉了,如果SetBackgroundColor不能滿足你,你可以自己實現IVewConvert介面,這個相當於方法轉換器:
public class SetBackgroundColor implements IViewConvert<Integer, View> {
@Override
public void apply(View view, Integer res) {
view.setBackgroundColor(res);
}
}
這個就是預設提供的,你可以自己去定義自己的方法邏輯;
當然如果你想要換膚,有了apk的路徑,那麼你只需要這樣做:
SkinCenter.get().loadSkin(context,skinPath);
一句話即可換膚,所有需要的換膚的view都會自動換膚,是不是很簡單。
有不足之處,歡迎大家提出,下面是原始碼地址: