Android高級(jí)進(jìn)階之-動(dòng)態(tài)換膚原理及實(shí)現(xiàn)

話說(shuō)什么是動(dòng)態(tài)換膚?這里舉個(gè)例子:在APP中可以下載某一個(gè)皮膚包,然后應(yīng)用起來(lái)整個(gè)APP的界面就發(fā)生了改變匀归,諸如某些圖片约啊,文字字體,文字顏色等等另玖。

那么這種功能是怎么實(shí)現(xiàn)的呢休蟹?其實(shí)初步分析一把,應(yīng)該就是在應(yīng)用了皮膚包之后這些換膚了的控件的某些布局屬性發(fā)生了變化日矫,比如width赂弓、height、src哪轿、background盈魁、textsize、textcolor等窃诉。話說(shuō)回來(lái)杨耙,在沒(méi)有實(shí)現(xiàn)換膚功能之前我們的APP對(duì)控件進(jìn)行屬性指定一般都是寫(xiě)在屬性文件中,比如android:textColor="@color/textColorDefault"飘痛,我們會(huì)在專門的color.xml文件中定義這個(gè)顏色屬性的具體value值珊膜,那么我們換膚時(shí)就應(yīng)該是去替換color.xml文件中定義的textColorDefault這個(gè)屬性值。

現(xiàn)在開(kāi)始分析Android默認(rèn)是在什么時(shí)候開(kāi)始加載視圖組件的宣脉。我們應(yīng)該會(huì)聯(lián)想到Activity的onCreate()方法里面我們都要去調(diào)用setContentView(int id) 來(lái)指定當(dāng)前Activity的布局文件车柠,就像這樣:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

按照流程我們找到了這里:

    @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);//這里實(shí)現(xiàn)view布局的加載
        mOriginalWindowCallback.onContentChanged();
    }

LayoutInflater的功能我們?cè)趂ragment中應(yīng)該很熟悉了,多說(shuō)一句,在自定義viewGroup的時(shí)候我們也可以仿照這樣的寫(xiě)法對(duì)自定義viewGroup指定默認(rèn)的布局文件了竹祷。

好接下來(lái)我們順藤摸瓜來(lái)到了LayoutInflater.java里面看看inflate是怎么實(shí)現(xiàn)的:

 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
            final String name = parser.getName();
            final View temp = createViewFromTag(root, name, inflaterContext, attrs);
            return temp;
    }

可以看到inflate會(huì)返回具體的View對(duì)象出去谈跛,那么我們的關(guān)注焦點(diǎn)就放在createViewFromTag中了。

    /**
     * Creates a view from a tag name using the supplied attribute set.
     * <p>
     * <strong>Note:</strong> Default visibility so the BridgeInflater can
     * override it.
     *
     * @param parent the parent view, used to inflate layout params
     * @param name the name of the XML tag used to define the view
     * @param context the inflation context for the view, typically the
     *                {@code parent} or base layout inflater context
     * @param attrs the attribute set for the XML tag used to define the view
     * @param ignoreThemeAttr {@code true} to ignore the {@code android:theme}
     *                        attribute (if set) for the view being inflated,
     *                        {@code false} otherwise
     */
    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        try {
            View view;
            if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }
            return view;
        } catch (Exception e) {
        }
    }

這里我們先看看這幾個(gè)參數(shù)的意義塑陵,name指的是在layout.xml中給出的名稱感憾,例如:

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="skinSelect"
        android:text="個(gè)性換膚"/>

這里拿到的name值就是“Button”。再如:

    <com.dongnao.dnskin.widget.MyTabLayout
        android:id="@+id/tabLayout"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        app:tabIndicatorColor="@color/tabSelectedTextColor"
        app:tabTextColor="@color/tab_selector"/>

這里拿到的name值就是“com.dongnao.dnskin.widget.MyTabLayout”令花,或者:

    <android.support.v4.view.ViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

這里拿到的name值就是“android.support.v4.view.ViewPager”阻桅。

因此我們要明確一點(diǎn),參數(shù)name可能是View控件的java全路徑名稱兼都,也有可能不是嫂沉,比如第一種情況的Button,但是這種情況只會(huì)出現(xiàn)在系統(tǒng)已有控件里面俯抖,它們的包名我們是可以大膽猜測(cè)出來(lái)的输瓜,無(wú)非就是在這么幾個(gè)包下面:

private static final String[]mClassPrefixList = {
        "android.widget.",
        "android.view.",
        "android.webkit."
};

OK瓦胎,分析完name參數(shù)芬萍,我們?cè)诳纯碅ttributeSet是個(gè)什么梗,其實(shí)源碼注釋已經(jīng)寫(xiě)得很清楚了搔啊,就是xml文件中對(duì)這個(gè)View給出的屬性描述柬祠。

參數(shù)分析完了,我們看看方法體是怎么實(shí)現(xiàn)的负芋。會(huì)發(fā)現(xiàn)生成View的時(shí)候會(huì)優(yōu)先從mFactory2中的onCreateView里面去獲取View對(duì)象漫蛔,獲取到了就直接返回。所以我們是不是可以自己去實(shí)現(xiàn)這個(gè)mFactory2來(lái)代替系統(tǒng)生成View對(duì)象旧蛾?因?yàn)樯蒝iew對(duì)象的工作由我們自己來(lái)完成的話我們就可以很輕松的獲取attrs參數(shù)莽龟,并且根據(jù)attrs對(duì)象知道在layout.xml中對(duì)這個(gè)View做了哪些屬性描述,比如說(shuō)拿到了background=“@drawable/bg_01”锨天,當(dāng)需要換膚的時(shí)候毯盈,我們就可以去皮膚包里面找到“@drawable/bg_01”這個(gè)資源,用來(lái)給這個(gè)View替換上去View.setBackground(...)病袄,那么我們的換膚功能不就實(shí)現(xiàn)了嗎搂赋?答案也確實(shí)是這樣做的。

接下來(lái)把精力放在怎么實(shí)現(xiàn)mFactory2上面益缠,并且設(shè)置進(jìn)入LayoutInflater中脑奠。前面我們知道Activity的setContentView()回去調(diào)用LayoutInflater.from(Context)拿到 LayoutInflater對(duì)象,代碼如下:

    /**
     * Obtains the LayoutInflater from the given context.
     */
    public static LayoutInflater from(Context context) {
        LayoutInflater LayoutInflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }

通過(guò)源碼的注釋也可以看到每一個(gè)Activity會(huì)有自己的LayoutInflater對(duì)象幅慌,此外LayoutInflater還暴露了mFactory2的set方法提供給我們:

public void setFactory2(Factory2 factory) {
        if (mFactorySet) {
            throw new IllegalStateException("A factory has already been set on this LayoutInflater");
        }
        if (factory == null) {
            throw new NullPointerException("Given factory can not be null");
        }
        mFactorySet = true;
        if (mFactory == null) {
            mFactory = mFactory2 = factory;
        } else {
            mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
        }
    }

因此宋欺,這個(gè)setter就可以在每個(gè)Activity的onCreate之前進(jìn)行調(diào)用,達(dá)到我們想要的目的。到了這里迄靠,相信我們會(huì)想到使用ActivityLifecycleCallbacks回調(diào)來(lái)監(jiān)聽(tīng)Activity的各個(gè)生命周期回調(diào)秒咨,在onActivityCreated()進(jìn)行mFactory2的初始化并且調(diào)用setter。
接下來(lái)掌挚,我們定義一個(gè)單例類SkinManager.java :

public class SkinManager extends Observable {
    Application application;
    private static SkinManager instance;

    /**
     * 客戶端程序在application的onCreate()后調(diào)用.
     * @param application
     */
    public static void init(Application application) {
        synchronized (SkinManager.class) {
            if (null == instance) {
                instance = new SkinManager(application);
            }
        }
    }

    public static SkinManager getInstance() {
        return instance;
    }

    private SkinManager(Application application) {
        this.application = application;
        application.registerActivityLifecycleCallbacks(new SkinActivityLifecycleCallbacks());
    }

SkinActivityLifecycleCallbacks.java如下:

public class SkinActivityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        LayoutInflater layoutInflater = LayoutInflater.from(activity);
        try {
            Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
            field.setAccessible(true);
            field.setBoolean(layoutInflater, false);
        } catch (Exception e) {
            e.printStackTrace();
        }
        SkinLayoutFactory skinLayoutFactory = new SkinLayoutFactory(activity,typeface);
        LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutFactory);
    }
    ...
}

因?yàn)閟etFactory2()中有一個(gè)mFactorySet布爾類型的判斷雨席,我們使用了反射對(duì)mFactorySet置為true。
我們只需要在自定義application的onCreate()后面調(diào)用SkinManager.init()就完成了所有Activity的mFactory2設(shè)置吠式。

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        SkinManager.init(this);
    }

OK陡厘,到了這一步準(zhǔn)備工作就做完了,我們重心放入自定義Factory2的實(shí)現(xiàn)中來(lái)特占〔谥茫看看Factory2是個(gè)什么東西:

    public interface Factory2 extends Factory {
        /**
         * Version of {@link #onCreateView(String, Context, AttributeSet)}
         * that also supplies the parent that the view created view will be
         * placed in.
         *
         * @param parent The parent that the created view will be placed
         * in; <em>note that this may be null</em>.
         * @param name Tag name to be inflated.
         * @param context The context the view is being created in.
         * @param attrs Inflation attributes as specified in XML file.
         *
         * @return View Newly created view. Return null for the default
         *         behavior.
         */
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
    }

它是一個(gè)接口,聲明了一個(gè)創(chuàng)建View的函數(shù)等待實(shí)現(xiàn)類去實(shí)現(xiàn)是目。我們的實(shí)現(xiàn)類如下:

public class SkinLayoutFactory implements LayoutInflater.Factory2, Observer {
    private static final HashMap<String, Constructor<? extends View>> sConstructorMap =
            new HashMap<String, Constructor<? extends View>>();
    private static final String[] mClassPrefixList = {
            "android.widget.",
            "android.view.",
            "android.webkit."
    };

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        View view = createViewFromTag(name, context, attrs);
        return view;
    }

    private View createViewFromTag(String name, Context context, AttributeSet attrs) {
        //包含了. 自定義控件
        if (name.contains(".")) {
            return  createView(name,context,attrs);
        }
        for (String tag : mClassPrefixList) {
            View v = createView(tag + name, context, attrs);
            if (null == v)
                continue;
            return v;
        }
        return null;
    }

    private View createView(String name, Context context, AttributeSet attrs) {
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        if (null == constructor) {
            try {
                Class<? extends View> aClass = context.getClassLoader().loadClass(name).asSubclass(View.class);
                constructor = aClass.getConstructor(Context.class, AttributeSet.class);
                sConstructorMap.put(name, constructor);
            } catch (Exception e) {
            }
        }
        if (null != constructor) {
            try {
                return constructor.newInstance(context, attrs);
            } catch (Exception e) {
            }
        }
        return null;
    }

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        return null;
    }
}

這里的實(shí)現(xiàn)其實(shí)也很簡(jiǎn)單谤饭,就是我們獲取View的java全名稱,然后通過(guò)反射機(jī)制獲取View的構(gòu)造方法進(jìn)行實(shí)例化再返回懊纳。當(dāng)然揉抵,我們這里還做了一個(gè)靜態(tài)的map來(lái)緩存View的構(gòu)造方法,可以優(yōu)化一定的性能嗤疯,畢竟反射多了總是不好的對(duì)吧(其實(shí)你仔細(xì)看了LayoutInflater的源碼冤今,它就是這么做的,我們這里借鑒一下)茂缚。

OK戏罢,按照前面給出的思路,我們自己構(gòu)建mFactory2脚囊,代替系統(tǒng)來(lái)創(chuàng)建View對(duì)象龟糕,接下來(lái)還差一個(gè)步驟,就是通過(guò)attrs參數(shù)知道這個(gè)View在xml中被哪些屬性描述了悔耘,我們需要一個(gè)機(jī)制來(lái)記錄這個(gè)View被描述過(guò)了的并且可能會(huì)被皮膚包替換資源的屬性名稱還有默認(rèn)的資源Id讲岁,在換膚時(shí)就去這些記錄里面查找View及它的換膚屬性的名稱和資源Id,拿到默認(rèn)資源Id后就可以知道這個(gè)資源的類型和名稱淮逊,比如@string/s_name催首、@color/co_default_bg,然后拿著資源類型和名稱去皮膚包中查找同類型同名稱的資源泄鹏,然后根據(jù)屬性名稱給這個(gè)View更改相應(yīng)的表現(xiàn)郎任。比如描述屬性名稱和資源類型名稱是textColor=“@color/default_tx_color”,在皮膚包中找到“@color/default_tx_color”這個(gè)資源备籽,給View.setTextColor(皮膚包中找到的資源)舶治,如果屬性名稱是background分井,那么就是View.setBackground(皮膚包中找到的資源),這樣就達(dá)到了換膚效果霉猛。

接下來(lái)就是怎么去實(shí)現(xiàn)這個(gè)記錄View及它的換膚屬性和資源名稱的機(jī)制了尺锚。

我們?cè)O(shè)計(jì)一套數(shù)據(jù)結(jié)構(gòu)來(lái)記錄這種關(guān)系。


記錄需要換膚View及其屬性名稱和資源id的數(shù)據(jù)結(jié)構(gòu)

其實(shí)可以在自定義Factory2返回View對(duì)象之前做這些工作惜浅,比如交給SkinAttribute對(duì)象去做瘫辩。SkinAttribute以及SkinView、SkinPair代碼如下:


public class SkinAttribute {
    private static final List<String> mAttributes = new ArrayList<>();//支持換膚的屬性

    static {
        mAttributes.add("background");
        mAttributes.add("src");
        mAttributes.add("textColor");
        mAttributes.add("drawableLeft");
        mAttributes.add("drawableTop");
        mAttributes.add("drawableRight");
        mAttributes.add("drawableBottom");
    }

    private List<SkinView> skinViews = new ArrayList<>();

    /**
     * 篩選符合屬性的view
     *
     * @param view
     * @param attrs
     */
    public void load(View view, AttributeSet attrs) {
        List<SkinPair> skinPairs = new ArrayList<>();
        for (int i = 0; i < attrs.getAttributeCount(); i++) {
            //獲得屬性名
            String attributeName = attrs.getAttributeName(i);
            if (mAttributes.contains(attributeName)) {
                String attributeValue = attrs.getAttributeValue(i);
                //寫(xiě)死了 不管了
                if (attributeValue.startsWith("#")) {
                    continue;
                }
                //資源id
                int resId = 0;
                if (attributeValue.startsWith("?")) {//?開(kāi)頭,  "?colorAccess" 對(duì)應(yīng)主題中的屬性名稱id
                    int attrId = Integer.parseInt(attributeValue.substring(1));//屬性id
                    //獲得主題style中對(duì)應(yīng)attr的資源id值
                    resId = SkinUtils.getResId(view.getContext(), new int[]{attrId})[0];
                } else {//@開(kāi)頭  "@ID"
                    resId = Integer.parseInt(attributeValue.substring(1));
                }
                if (resId != 0) {
                    //可以替換的屬性
                    SkinPair skinPair = new SkinPair(attributeName, resId);
                    skinPairs.add(skinPair);
                }
            }
        }
        if (!skinPairs.isEmpty() || view instanceof TextView) {
            SkinView skinView = new SkinView(view, skinPairs);
            skinViews.add(skinView);
        }
    }

    static class SkinView {
        View view;
        /**
         * 當(dāng)前view支持換膚特性的屬性與id鍵值對(duì)列表
         */
        List<SkinPair> skinPairs;

        public SkinView(View view, List<SkinPair> skinPairs) {
            this.view = view;
            this.skinPairs = skinPairs;
        }
    }

    static class SkinPair {
        /**
         * 屬性名稱,例如:background,src,textColor等
         */
        String attributeName;
        /**
         * 資源ID值
         */
        int resId;

        public SkinPair(String attributeName, int resId) {
            this.attributeName = attributeName;
            this.resId = resId;
        }
    }
}

上述代碼中有一處需要清楚的是:String attributeValue = attrs.getAttributeValue() 返回的是一個(gè)字符串坛悉,例如textColor=“#ffffff”伐厌,那么attributeValue = “#ffffff”,textColor=“@color/default_color”裸影,那么attributeValue = “@12345678”挣轨,這里的“12345678”指的就是“@color/default_color”對(duì)應(yīng)的資源ID,類似的還有“@drawable/default_bg”等等轩猩。當(dāng)然卷扮,還有一種情況就是textColor=“?colorAccess”,雖然程序最終引用的資源是style.xml中定義的屬性值“colorAccent”指向的“@color/colorAccent”均践,

   <style name="BaseTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorAccent">@color/colorAccent</item>
    </style>

這個(gè)時(shí)候attributeValue = “?12121212”晤锹,但是“12121212”并不是“@color/colorAccent”的資源ID,而是style屬性“colorAccent”代表的ID浊猾,因此對(duì)待“?12121212”這樣的情況我們還需要再去style.xml中去查找真正引用的資源ID抖甘。具體做法如下:

public class SkinUtils {
    public static int[] getResId(Context context, int[] attrs) {
        int[] resIds = new int[attrs.length];
        TypedArray typedArray = context.obtainStyledAttributes(attrs);

        for (int i = 0; i < typedArray.length(); i++) {
            resIds[i] = typedArray.getResourceId(i, 0);
        }
        typedArray.recycle();
        return resIds;
    }
}

到了這里热鞍,相信你也就知道了SkinLayoutFactory該怎么改造了:

public class SkinLayoutFactory implements LayoutInflater.Factory2{

    ......

    private SkinAttribute skinAttribute;

    public SkinLayoutFactory() {
        skinAttribute = new SkinAttribute();
    }

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        View view = createViewFromTag(name, context, attrs);
        skinAttribute.load(view,attrs);//在返回view之前維護(hù)我們需要的 屬性-資源 關(guān)系數(shù)據(jù)結(jié)構(gòu)
        return view;
    }
    
    ......

}

OK葫慎,現(xiàn)在需要換膚控件的資源信息也采集到了,接下來(lái)就是怎么去實(shí)現(xiàn)換膚了薇宠。
換膚之前我們要清楚什么是皮膚包偷办,又該怎么把皮膚包加載到系統(tǒng)里面供我們獲取資源并使用。

皮膚包其實(shí)就是一個(gè)apk文件澄港,只不過(guò)內(nèi)部只包含資源文件椒涯,我們的皮膚包目錄結(jié)構(gòu)如下:


皮膚包工程目錄結(jié)構(gòu)

接下來(lái),我們?cè)撊绾伟岩粋€(gè)皮膚包加載進(jìn)項(xiàng)目中回梧,并且根據(jù)資源類型和名稱來(lái)獲取指定資源的Id呢废岂?
首先是將皮膚包加載進(jìn)入項(xiàng)目,我們會(huì)用到AssetManager這個(gè)工具:

 /**
     * 加載皮膚包 并 立即通知觀察者更新
     *
     * @param path 皮膚包路徑
     */
    public void loadSkin(String path) {
            try {
                AssetManager assetManager = AssetManager.class.newInstance();
                // 添加資源進(jìn)入資源管理器
                Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String
                        .class);
                addAssetPath.setAccessible(true);
                addAssetPath.invoke(assetManager, path);

                //app默認(rèn)資源
                Resources resources = application.getResources();
                //皮膚包資源
                Resources skinResource = new Resources(assetManager, resources.getDisplayMetrics(),
                        resources.getConfiguration());
                //獲取外部Apk(皮膚包) 包名
                PackageManager mPm = application.getPackageManager();
                PackageInfo info = mPm.getPackageArchiveInfo(path, PackageManager
                        .GET_ACTIVITIES);
                String packageName = info.packageName;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

上述代碼我們傳入皮膚包的路徑狱意,通過(guò)AssetManager獲取到Resource對(duì)象湖苞,其實(shí)到這一步就已經(jīng)將皮膚包的資源文件加載進(jìn)來(lái)了。
那么加載到了皮膚包的Resource對(duì)象详囤,我們?cè)撊绾瓮ㄟ^(guò)APP程序默認(rèn)的一個(gè)資源id去拿到在皮膚包中同類型同名稱的這個(gè)資源的id呢财骨?

    public int getIdentifier(int resId) {
        if (isDefaultSkin) {
            return resId;
        }
        //在皮膚包中不一定就是 當(dāng)前程序的 id
        //獲取對(duì)應(yīng)id 在當(dāng)前的名稱 colorPrimary
        //R.drawable.ic_launcher
        String resName = mAppResources.getResourceEntryName(resId);//ic_launcher
        String resType = mAppResources.getResourceTypeName(resId);//drawable
        int skinId = mSkinResources.getIdentifier(resName, resType, mSkinPkgName);
        return skinId;
    }

這個(gè)方法其實(shí)就是將默認(rèn)資源id轉(zhuǎn)化成皮膚包中對(duì)應(yīng)資源的id,獲取到了id我們就可以通過(guò)Resources.getXXX(int id)來(lái)拿到想要的資源了。

到了這個(gè)時(shí)候隆箩,相信我們都有了一個(gè)完整的換膚思路了:
①重寫(xiě)Factory2该贾,代替系統(tǒng)創(chuàng)建View對(duì)象,在這期間記錄下 需要換膚控件的 屬性名-資源ID 的集合捌臊。
②通過(guò)AssetManager加載外部的皮膚包資源Resource杨蛋,通過(guò)默認(rèn)的資源ID找到在皮膚包中對(duì)應(yīng)的資源ID,通過(guò)屬性名稱去動(dòng)態(tài)修改View的具體表現(xiàn)理澎。
③開(kāi)始換膚時(shí)六荒,我們可以使用觀察者模式來(lái)通知所有還未銷毀的Activity持有的 SkinLayoutFactory(作為觀察者),讓SkinLayoutFactory去遍歷其下面的所有 SkinView來(lái)完成應(yīng)用換膚資源的工作矾端。

/**
         * 對(duì)當(dāng)前view進(jìn)行支持換膚的屬性進(jìn)行配置,應(yīng)用原生或者皮膚包的資源.
         * @param typeface
         */
        public void applySkin() {
            for (SkinPair skinPair : skinPairs) {
                Drawable left = null, top = null, right = null, bottom = null;
                switch (skinPair.attributeName) {
                    case "background":
                        Object background = SkinResources.getInstance().getBackground(skinPair
                                .resId);
                        //Color
                        if (background instanceof Integer) {
                            view.setBackgroundColor((Integer) background);
                        } else {
                            ViewCompat.setBackground(view, (Drawable) background);
                        }
                        break;
                    case "src":
                        background = SkinResources.getInstance().getBackground(skinPair
                                .resId);
                        if (background instanceof Integer) {
                            ((ImageView) view).setImageDrawable(new ColorDrawable((Integer)
                                    background));
                        } else {
                            ((ImageView) view).setImageDrawable((Drawable) background);
                        }
                        break;
                    case "textColor":
                        ((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList
                                (skinPair.resId));
                        break;
                    case "drawableLeft":
                        left = SkinResources.getInstance().getDrawable(skinPair.resId);
                        break;
                    case "drawableTop":
                        top = SkinResources.getInstance().getDrawable(skinPair.resId);
                        break;
                    case "drawableRight":
                        right = SkinResources.getInstance().getDrawable(skinPair.resId);
                        break;
                    case "drawableBottom":
                        bottom = SkinResources.getInstance().getDrawable(skinPair.resId);
                        break;
                    default:
                        break;
                }
                if (null != left || null != right || null != top || null != bottom) {
                    ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right,
                            bottom);
                }
            }
        }

上述代碼清晰的展示了通過(guò)屬性名稱來(lái)做出不同View展示調(diào)整的邏輯掏击。

我們?cè)賮?lái)看下SkinResources.java的代碼:

public class SkinResources {

    private static SkinResources instance;

    private Resources mSkinResources;
    private String mSkinPkgName;
    private boolean isDefaultSkin = true;

    private Resources mAppResources;

    private SkinResources(Context context) {
        mAppResources = context.getResources();
    }

    public static void init(Context context) {
        if (instance == null) {
            synchronized (SkinResources.class) {
                if (instance == null) {
                    instance = new SkinResources(context);
                }
            }
        }
    }

    public static SkinResources getInstance() {
        return instance;
    }

    public void reset() {
        mSkinResources = null;
        mSkinPkgName = "";
        isDefaultSkin = true;
    }

    public void applySkin(Resources resources, String pkgName) {
        mSkinResources = resources;
        mSkinPkgName = pkgName;
        //是否使用默認(rèn)皮膚
        isDefaultSkin = TextUtils.isEmpty(pkgName) || resources == null;
    }

    public int getIdentifier(int resId) {
        if (isDefaultSkin) {
            return resId;
        }
        //在皮膚包中不一定就是 當(dāng)前程序的 id
        //獲取對(duì)應(yīng)id 在當(dāng)前的名稱 colorPrimary
        //R.drawable.ic_launcher
        String resName = mAppResources.getResourceEntryName(resId);//ic_launcher
        String resType = mAppResources.getResourceTypeName(resId);//drawable
        int skinId = mSkinResources.getIdentifier(resName, resType, mSkinPkgName);
        return skinId;
    }

    public int getColor(int resId) {
        if (isDefaultSkin) {
            return mAppResources.getColor(resId);
        }
        int skinId = getIdentifier(resId);
        if (skinId == 0) {
            return mAppResources.getColor(resId);
        }
        return mSkinResources.getColor(skinId);
    }

    public ColorStateList getColorStateList(int resId) {
        if (isDefaultSkin) {
            return mAppResources.getColorStateList(resId);
        }
        int skinId = getIdentifier(resId);
        if (skinId == 0) {
            return mAppResources.getColorStateList(resId);
        }
        return mSkinResources.getColorStateList(skinId);
    }

    public Drawable getDrawable(int resId) {
        //如果有皮膚  isDefaultSkin false 沒(méi)有就是true
        if (isDefaultSkin) {
            return mAppResources.getDrawable(resId);
        }
        int skinId = getIdentifier(resId);
        if (skinId == 0) {
            return mAppResources.getDrawable(resId);
        }
        return mSkinResources.getDrawable(skinId);
    }


    /**
     * 可能是Color 也可能是drawable
     *
     * @return
     */
    public Object getBackground(int resId) {
        String resourceTypeName = mAppResources.getResourceTypeName(resId);

        if (resourceTypeName.equals("color")) {
            return getColor(resId);
        } else {
            // drawable
            return getDrawable(resId);
        }
    }

    public String getString(int resId) {
        try {
            if (isDefaultSkin) {
                return mAppResources.getString(resId);
            }
            int skinId = getIdentifier(resId);
            if (skinId == 0) {
                return mAppResources.getString(skinId);
            }
            return mSkinResources.getString(skinId);
        } catch (Resources.NotFoundException e) {

        }
        return null;
    }
}

OK,整個(gè)換膚原理基本就講完了秩铆,當(dāng)然還有字體的動(dòng)態(tài)全局及單個(gè)切換砚亭,自定義view的自定義屬性切換,某些控件加載時(shí)序問(wèn)題導(dǎo)致無(wú)法換膚等問(wèn)題殴玛,后面繼續(xù)補(bǔ)充捅膘。

附上源碼地址
鏈接:https://share.weiyun.com/51Q5YxV

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市滚粟,隨后出現(xiàn)的幾起案子寻仗,更是在濱河造成了極大的恐慌,老刑警劉巖凡壤,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件署尤,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡亚侠,警方通過(guò)查閱死者的電腦和手機(jī)曹体,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)硝烂,“玉大人箕别,你說(shuō)我怎么就攤上這事≈托唬” “怎么了串稀?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)狮杨。 經(jīng)常有香客問(wèn)我母截,道長(zhǎng),這世上最難降的妖魔是什么禾酱? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任微酬,我火速辦了婚禮绘趋,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘颗管。我一直安慰自己陷遮,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布垦江。 她就那樣靜靜地躺著帽馋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪比吭。 梳的紋絲不亂的頭發(fā)上绽族,一...
    開(kāi)封第一講書(shū)人閱讀 51,115評(píng)論 1 296
  • 那天,我揣著相機(jī)與錄音衩藤,去河邊找鬼吧慢。 笑死,一個(gè)胖子當(dāng)著我的面吹牛赏表,可吹牛的內(nèi)容都是我干的检诗。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼瓢剿,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼逢慌!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起间狂,我...
    開(kāi)封第一講書(shū)人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤攻泼,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后鉴象,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體忙菠,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年炼列,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了只搁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片音比。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡俭尖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出洞翩,到底是詐尸還是另有隱情稽犁,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布骚亿,位于F島的核電站已亥,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏来屠。R本人自食惡果不足惜虑椎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一震鹉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧捆姜,春花似錦传趾、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至珊豹,卻和暖如春簸呈,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背店茶。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工蜕便, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人贩幻。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓玩裙,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親段直。 傳聞我的和親對(duì)象是個(gè)殘疾皇子吃溅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容