作者:Liuhua Chen
一、 實作思路
安卓應用在讀取資源時是由AssetManager和Resources兩個類來實作的。Resouce類是先根據ID來找到資源檔案名稱,然後再将該檔案名稱交給AssetManager來打開檔案。我們主題開發的核心思路就是在應用讀取資源時,先去主題包裡讀取資源,若有資源,直接傳回主題包的資源,若無資源,直接傳回應用本身的資源。
參考部落格:http://blog.csdn.net/luoshengyang/article/details/8806798
二、 方案實作
a) 修改源碼Resource.java
private LoadResourcesCallBack mCallBack;
public interface LoadResourcesCallBack{
ColorStateListloadColorStateList(TypedValue value, int id);
Drawable loadDrawable(TypedValue value, int id);
XmlResourceParser loadXmlResourceParser(int id, String type);
int getDimensionPixelSize(int index);
int getDimensionPixelOffset(int index);
float getDimension(int index);
Integer getInteger(int index);
}
public LoadResourcesCallBack getLoadResourcesCallBack() {
return mCallBack;
}
public boolean regitsterLoadResourcesCallBack(LoadResourcesCallBackcallBack) {
if (callBack== null || mCallBack != null) {
return false;
}
mCallBack = callBack;
return true;
}
在Resouces類是主要加這個接口,這個接口就是主題改變的關鍵所在。接口LoadResourcesCallBack中的抽象方法,從名子中可以發現,Resources類中也有同名的方法。LoadResourcesCallBack中的方法就是在Resources同名方法中調用,即Resouces類中在執行這些方法時,會先執行LoadResourcesCallBack實作的方法。
LoadResourcesCallBack抽象方法實作如下:
context.getResources().regitsterLoadResourcesCallBack(new Resources.LoadResourcesCallBack() {
@Override
public ColorStateListloadColorStateList(TypedValue typedValue, int i) {
if (i == 0) return null;
String entryName = context.getResources().getResourceEntryName(i);
ColorStateList color = ResourceManagerTPV.getInstance(context).getColorStateList(entryName);
return color;
}
@Override
public DrawableloadDrawable(TypedValue typedValue, int i) {
if(i == 0){
return null;
}
String entryName = context.getResources().getResourceEntryName(i);
Drawable drawable = ResourceManagerTPV.getInstance(context).getDrawable(entryName);
return drawable;
}
@Override
public XmlResourceParserloadXmlResourceParser(int i, String s) {
return null;
}
@Override
public int getDimensionPixelSize(int i) {
if(i == 0){
return -1;
}
String entryName = context.getResources().getResourceEntryName(i);
int dimensionPixelSize = ResourceManagerTPV.getInstance(context).getDimensionPixelSize(entryName);
return dimensionPixelSize;
}
@Override
public int getDimensionPixelOffset(int i) {
if(i == 0){
return -1;
}
String entryName = context.getResources().getResourceEntryName(i);
return ResourceManagerTPV.getInstance(context).getDimensionPixelOffset(entryName);
}
@Override
public float getDimension(int i) {
if(i == 0){
return -1;
}
String entryName = context.getResources().getResourceEntryName(i);
return ResourceManagerTPV.getInstance(context).getDimen(entryName);
}
@Override
public IntegergetInteger(int i) {
if(i == 0){
return null;
}
String entryName = context.getResources().getResourceEntryName(i);
return ResourceManagerTPV.getInstance(context).getInteger(entryName);
}
});
實作原理:在查找資源時會根據提供的ID進行查找,然後通過資源ID查找資源ID對應的資源名稱,然後擷取目前設定的主題包的Context,然後再由主題包的Context通過資源名稱查找目前主題包下是否有要查詢的資源,有就傳回具體資源的值,如圖檔,就傳回Drawable資源,沒有就傳回Null,Resouce類就會執行自己的方法。
b) 通知更新
實作原理:參考系統語言切換的實作方法。
參考部落格:http://blog.csdn.net/wxlinwzl/article/details/42614117
三、 開發中遇到的問題
a) 開發架構的設計
在方案實行前,已讨論過主題的實作方案,剛開始實作方案與台北TPV的主題實作方案類似,都是先制作一個預設的主題包。但是在制作完主題包後,發現該方案,資源為隻讀,不能同時支援多個主題動态切換,且在XML中不能直接引用。後來就參考了TUF的做法,覺得這個方案可行,就按這個方案來實行。
b) 代碼中讀取color資源,仍是應用本身的資源,不是主題包的資源
在與Launcher和SystemUI調試時,同樣的方法,在代碼讀取圖檔和Color值時,圖檔會讀取到安裝的主題包下的資源,而Color資源沒有讀取到,仍是應用本身設定的值。後來發現是在Resources源碼中getColor,如下紅色字型代碼中,還沒有判斷是用哪個資源時,已經直接傳回應用的Color資源。最後解決方法,在該段代碼前進行判斷。
@ColorInt
public int getColor(@ColorRes int id,@Nullable Theme theme) throws NotFoundException {
……..
if (value.type >=TypedValue.TYPE_FIRST_INT
&& value.type <=TypedValue.TYPE_LAST_INT) {
mTmpValue = value;
return value.data;
} else if (value.type !=TypedValue.TYPE_STRING) {
throw new NotFoundException(
"Resource ID#0x" + Integer.toHexString(id) + " type #0x"
+ Integer.toHexString(value.type) + " isnot valid");
}
mTmpValue = null;
}
…….
final ColorStateList csl =loadColorStateList(value, id, theme);
……..
}
c) SystemUI不會更新
SystemUI是一個很特殊的應用,切換資源時,無法同步切換,隻能通過重新開機手機才會更新資源,而重新開機手機又與UX的設計不一緻。最後隻能通過廣播通知其更新。
d) Widget應用無法更新
在與Clock調試時,同樣的方法,同樣的步驟,在widget中就是不切換資源,最後也隻能像SystemUI一樣通過廣播進行更新。隻有一個Clock,那時感覺用這種方法,也還好,修改的代碼不多,也不繁瑣。後來,天氣也需要更新,問題就來了。天氣widget的實作方法與Clock不一樣,而且天氣設定圖檔的方法也不一樣。若是天氣也是接收廣播,然後再自己代碼中更新,修改的代碼量非常多,且本身天氣應用的邏輯就比較複雜,如此下去不是一個可行的實作方案,不排除以後其他的Widget也需要修改,是以這個方案不能實行,隻能另辟方法。
是以就一直去看看Widget的實作原理,發現Widget都是通過RemoteView來遠端代理的。就去檢視RemoteView的源碼,
private View inflateView(Contextcontext, RemoteViews rv, ViewGroupparent) {
// RemoteViews may be built by an application installedin another
// user. So build a context thatloads resources from that user but
// still returns the current usersuserId so settings like data / time formats
// are loaded without requiring crossuser persmissions.
final ContextcontextForResources = getContextForResources(context);
Context inflationContext = new ContextWrapper(context){
@Override
public ResourcesgetResources() {
return contextForResources.getResources();
}
@Override
public Resources.ThemegetTheme() {
return contextForResources.getTheme();
}
@Override
public StringgetPackageName() {
return contextForResources.getPackageName();
}
};
LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
// Clone inflater so we load resources from correctcontext and
// we don't add a filter to thestatic version returned by getSystemService.
inflater = inflater.cloneInContext(inflationContext);
inflater.setFilter(this);
return inflater.inflate(rv.getLayoutId(), parent, false);
}
通過這個方法開頭的說明,這個方法就是RemoteView擷取資源的關鍵所在,我們隻要在getResources()方法中注冊一下Resources類中自定義的接口。事實證明,确實是如此,修改了此次代碼後,Clock和天氣都不需要執行任何操作。主題市場隻需要統計需要修改的Color和Drawable的名稱。
e) 鎖屏桌面和桌面桌面與效果不一緻
桌面的設定,系統有提供一些接口進行設定。該開始就用了系統提供的接口進行設定,發現桌面的桌面不會動,而且顯示得很模糊,鎖屏的桌面模糊效果與桌面不在同一個位置,出現分層。顯然這樣的設定方法是不可行的,詢問了Launcher負責人,具體Launcher的裁剪方法也不是非常清楚。最後自己去下載下傳了Launcher的代碼來看,設定桌面确實好複雜,非常多個類,各種邏輯,要完全明白,真的有點困難。在這個設定桌面上,也花費了較長時間進行分析。雖然現在也不是完全明白怎麼設定的,但是通過各方面的測試,最終達到了效果。