天天看點

Android系統更換主題外觀的實作方法

作者: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的代碼來看,設定桌面确實好複雜,非常多個類,各種邏輯,要完全明白,真的有點困難。在這個設定桌面上,也花費了較長時間進行分析。雖然現在也不是完全明白怎麼設定的,但是通過各方面的測試,最終達到了效果。