Browse Source

实现切换皮肤时,支持TextView中的字体大小设置

zengjiebin 7 years ago
parent
commit
c935e0b162
61 changed files with 6465 additions and 2 deletions
  1. 1 0
      .idea/gradle.xml
  2. 2 1
      app/build.gradle
  3. 1 1
      settings.gradle
  4. 446 0
      skin-support/src/main/java/skin/support/SkinCompatManager.java
  5. 12 0
      skin-support/src/main/java/skin/support/annotation/Skinable.java
  6. 221 0
      skin-support/src/main/java/skin/support/app/SkinActivityLifecycle.java
  7. 91 0
      skin-support/src/main/java/skin/support/app/SkinCompatActivity.java
  8. 111 0
      skin-support/src/main/java/skin/support/app/SkinCompatDelegate.java
  9. 393 0
      skin-support/src/main/java/skin/support/app/SkinCompatViewInflater.java
  10. 14 0
      skin-support/src/main/java/skin/support/app/SkinLayoutInflater.java
  11. 600 0
      skin-support/src/main/java/skin/support/content/res/ColorState.java
  12. 754 0
      skin-support/src/main/java/skin/support/content/res/SkinCompatDrawableManager.java
  13. 88 0
      skin-support/src/main/java/skin/support/content/res/SkinCompatDrawableUtils.java
  14. 274 0
      skin-support/src/main/java/skin/support/content/res/SkinCompatResources.java
  15. 141 0
      skin-support/src/main/java/skin/support/content/res/SkinCompatThemeUtils.java
  16. 397 0
      skin-support/src/main/java/skin/support/content/res/SkinCompatUserThemeManager.java
  17. 7 0
      skin-support/src/main/java/skin/support/exception/SkinCompatException.java
  18. 49 0
      skin-support/src/main/java/skin/support/load/SkinAssetsLoader.java
  19. 46 0
      skin-support/src/main/java/skin/support/load/SkinBuildInLoader.java
  20. 40 0
      skin-support/src/main/java/skin/support/load/SkinNoneLoader.java
  21. 46 0
      skin-support/src/main/java/skin/support/load/SkinPrefixBuildInLoader.java
  22. 57 0
      skin-support/src/main/java/skin/support/load/SkinSDCardLoader.java
  23. 50 0
      skin-support/src/main/java/skin/support/observe/SkinObservable.java
  24. 9 0
      skin-support/src/main/java/skin/support/observe/SkinObserver.java
  25. 37 0
      skin-support/src/main/java/skin/support/utils/ImageUtils.java
  26. 151 0
      skin-support/src/main/java/skin/support/utils/SkinCompatVersionUtils.java
  27. 9 0
      skin-support/src/main/java/skin/support/utils/SkinConstants.java
  28. 37 0
      skin-support/src/main/java/skin/support/utils/SkinFileUtils.java
  29. 73 0
      skin-support/src/main/java/skin/support/utils/SkinPreference.java
  30. 28 0
      skin-support/src/main/java/skin/support/utils/Slog.java
  31. 116 0
      skin-support/src/main/java/skin/support/widget/SkinCompatAutoCompleteTextView.java
  32. 59 0
      skin-support/src/main/java/skin/support/widget/SkinCompatBackgroundHelper.java
  33. 81 0
      skin-support/src/main/java/skin/support/widget/SkinCompatButton.java
  34. 97 0
      skin-support/src/main/java/skin/support/widget/SkinCompatCheckBox.java
  35. 111 0
      skin-support/src/main/java/skin/support/widget/SkinCompatCheckedTextView.java
  36. 67 0
      skin-support/src/main/java/skin/support/widget/SkinCompatCompoundButtonHelper.java
  37. 88 0
      skin-support/src/main/java/skin/support/widget/SkinCompatEditText.java
  38. 43 0
      skin-support/src/main/java/skin/support/widget/SkinCompatFrameLayout.java
  39. 18 0
      skin-support/src/main/java/skin/support/widget/SkinCompatHelper.java
  40. 60 0
      skin-support/src/main/java/skin/support/widget/SkinCompatImageButton.java
  41. 61 0
      skin-support/src/main/java/skin/support/widget/SkinCompatImageHelper.java
  42. 59 0
      skin-support/src/main/java/skin/support/widget/SkinCompatImageView.java
  43. 43 0
      skin-support/src/main/java/skin/support/widget/SkinCompatLinearLayout.java
  44. 116 0
      skin-support/src/main/java/skin/support/widget/SkinCompatMultiAutoCompleteTextView.java
  45. 35 0
      skin-support/src/main/java/skin/support/widget/SkinCompatProgressBar.java
  46. 168 0
      skin-support/src/main/java/skin/support/widget/SkinCompatProgressBarHelper.java
  47. 95 0
      skin-support/src/main/java/skin/support/widget/SkinCompatRadioButton.java
  48. 37 0
      skin-support/src/main/java/skin/support/widget/SkinCompatRadioGroup.java
  49. 37 0
      skin-support/src/main/java/skin/support/widget/SkinCompatRatingBar.java
  50. 43 0
      skin-support/src/main/java/skin/support/widget/SkinCompatRelativeLayout.java
  51. 43 0
      skin-support/src/main/java/skin/support/widget/SkinCompatScrollView.java
  52. 38 0
      skin-support/src/main/java/skin/support/widget/SkinCompatSeekBar.java
  53. 63 0
      skin-support/src/main/java/skin/support/widget/SkinCompatSeekBarHelper.java
  54. 116 0
      skin-support/src/main/java/skin/support/widget/SkinCompatSpinner.java
  55. 9 0
      skin-support/src/main/java/skin/support/widget/SkinCompatSupportable.java
  56. 202 0
      skin-support/src/main/java/skin/support/widget/SkinCompatTextHelper.java
  57. 97 0
      skin-support/src/main/java/skin/support/widget/SkinCompatTextHelperV17.java
  58. 81 0
      skin-support/src/main/java/skin/support/widget/SkinCompatTextView.java
  59. 113 0
      skin-support/src/main/java/skin/support/widget/SkinCompatToolbar.java
  60. 44 0
      skin-support/src/main/java/skin/support/widget/SkinCompatView.java
  61. 40 0
      skin-support/src/main/java/skin/support/widget/SkinCompatViewGroup.java

+ 1 - 0
.idea/gradle.xml

@@ -14,6 +14,7 @@
             <option value="$PROJECT_DIR$/media/app" />
             <option value="$PROJECT_DIR$/media/cge_library" />
             <option value="$PROJECT_DIR$/media/share_library" />
+            <option value="$PROJECT_DIR$/skin-support" />
             <option value="$PROJECT_DIR$/ucrop" />
             <option value="$PROJECT_DIR$/view" />
           </set>

+ 2 - 1
app/build.gradle

@@ -450,7 +450,8 @@ dependencies {
 //    implementation 'skin.support:skin-support-design:4.0.1'            // skin-support-design material design 控件支持[可选]
 //    implementation 'skin.support:skin-support-cardview:4.0.1'          // skin-support-cardview CardView 控件支持[可选]
 //    implementation 'skin.support:skin-support-constraint-layout:4.0.1' // skin-support-constraint-layout ConstraintLayout 控件支持[可选]
-    implementation 'skin.support:skin-support:3.1.1'                   // skin-support 基础控件支持
+    implementation project(':skin-support')
+//    implementation 'skin.support:skin-support:3.1.1'                   // skin-support 基础控件支持
     implementation 'skin.support:skin-support-design:3.1.1'            // skin-support-design material design 控件支持[可选]
     implementation 'skin.support:skin-support-cardview:3.1.1'          // skin-support-cardview CardView 控件支持[可选]
     implementation 'skin.support:skin-support-constraint-layout:3.1.1' // skin-support-constraint-layout ConstraintLayout 控件支持[可选]

+ 1 - 1
settings.gradle

@@ -1,4 +1,4 @@
-include ':app', ':view', ':ucrop', ':WaterWaveProgress', ':media', ':share_library', ':joevideolib', ':cge_library'//, ':RxGalleryFinal', ':Aria', ':datashare', ':AriaAnnotations'
+include ':app', ':view', ':ucrop', ':WaterWaveProgress', ':media', ':share_library', ':joevideolib', ':cge_library', ':skin-support'//, ':RxGalleryFinal', ':Aria', ':datashare', ':AriaAnnotations'
 
 project(':media').projectDir = new File('media/app')
 project(':share_library').projectDir = new File('media/share_library')

+ 446 - 0
skin-support/src/main/java/skin/support/SkinCompatManager.java

@@ -0,0 +1,446 @@
+package skin.support;
+
+import android.app.Application;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import skin.support.app.SkinActivityLifecycle;
+import skin.support.app.SkinLayoutInflater;
+import skin.support.load.SkinAssetsLoader;
+import skin.support.load.SkinBuildInLoader;
+import skin.support.load.SkinNoneLoader;
+import skin.support.load.SkinPrefixBuildInLoader;
+import skin.support.observe.SkinObservable;
+import skin.support.utils.SkinPreference;
+import skin.support.content.res.SkinCompatResources;
+
+public class SkinCompatManager extends SkinObservable {
+    public static final int SKIN_LOADER_STRATEGY_NONE = -1;
+    public static final int SKIN_LOADER_STRATEGY_ASSETS = 0;
+    public static final int SKIN_LOADER_STRATEGY_BUILD_IN = 1;
+    public static final int SKIN_LOADER_STRATEGY_PREFIX_BUILD_IN = 2;
+    private static volatile SkinCompatManager sInstance;
+    private final Object mLock = new Object();
+    private final Context mAppContext;
+    private boolean mLoading = false;
+    private List<SkinLayoutInflater> mInflaters = new ArrayList<>();
+    private List<SkinLayoutInflater> mHookInflaters = new ArrayList<>();
+    private SparseArray<SkinLoaderStrategy> mStrategyMap = new SparseArray<>();
+    private boolean mSkinAllActivityEnable = true;
+    private boolean mSkinStatusBarColorEnable = false;
+    private boolean mSkinWindowBackgroundColorEnable = true;
+
+    /**
+     * 皮肤包加载监听.
+     */
+    public interface SkinLoaderListener {
+        /**
+         * 开始加载.
+         */
+        void onStart();
+
+        /**
+         * 加载成功.
+         */
+        void onSuccess();
+
+        /**
+         * 加载失败.
+         *
+         * @param errMsg 错误信息.
+         */
+        void onFailed(String errMsg);
+    }
+
+    /**
+     * 皮肤包加载策略.
+     */
+    public interface SkinLoaderStrategy {
+        /**
+         * 加载皮肤包.
+         *
+         * @param context  {@link Context}
+         * @param skinName 皮肤包名称.
+         * @return 加载成功,返回皮肤包名称;失败,则返回空。
+         */
+        String loadSkinInBackground(Context context, String skinName);
+
+        /**
+         * 根据应用中的资源ID,获取皮肤包相应资源的资源名.
+         *
+         * @param context  {@link Context}
+         * @param skinName 皮肤包名称.
+         * @param resId    应用中需要换肤的资源ID.
+         * @return 皮肤包中相应的资源名.
+         */
+        String getTargetResourceEntryName(Context context, String skinName, int resId);
+
+        /**
+         * 开发者可以拦截应用中的资源ID,返回对应color值。
+         *
+         * @param context  {@link Context}
+         * @param skinName 皮肤包名称.
+         * @param resId    应用中需要换肤的资源ID.
+         * @return 获得拦截后的颜色值,添加到ColorStateList的defaultColor中。不需要拦截,则返回空
+         */
+        ColorStateList getColor(Context context, String skinName, int resId);
+
+        /**
+         * 开发者可以拦截应用中的资源ID,返回对应ColorStateList。
+         *
+         * @param context  {@link Context}
+         * @param skinName 皮肤包名称.
+         * @param resId    应用中需要换肤的资源ID.
+         * @return 返回对应ColorStateList。不需要拦截,则返回空
+         */
+        ColorStateList getColorStateList(Context context, String skinName, int resId);
+
+        /**
+         * 开发者可以拦截应用中的资源ID,返回对应Drawable。
+         *
+         * @param context  {@link Context}
+         * @param skinName 皮肤包名称.
+         * @param resId    应用中需要换肤的资源ID.
+         * @return 返回对应Drawable。不需要拦截,则返回空
+         */
+        Drawable getDrawable(Context context, String skinName, int resId);
+
+        /**
+         * {@link #SKIN_LOADER_STRATEGY_NONE}
+         * {@link #SKIN_LOADER_STRATEGY_ASSETS}
+         * {@link #SKIN_LOADER_STRATEGY_BUILD_IN}
+         * {@link #SKIN_LOADER_STRATEGY_PREFIX_BUILD_IN}
+         *
+         * @return 皮肤包加载策略类型.
+         */
+        int getType();
+    }
+
+    /**
+     * 初始化换肤框架. 通过该方法初始化,应用中Activity需继承自{@link skin.support.app.SkinCompatActivity}.
+     *
+     * @param context
+     * @return
+     */
+    public static SkinCompatManager init(Context context) {
+        if (sInstance == null) {
+            synchronized (SkinCompatManager.class) {
+                if (sInstance == null) {
+                    sInstance = new SkinCompatManager(context);
+                }
+            }
+        }
+        SkinPreference.init(context);
+        return sInstance;
+    }
+
+    public static SkinCompatManager getInstance() {
+        return sInstance;
+    }
+
+    /**
+     * 初始化换肤框架,监听Activity生命周期. 通过该方法初始化,应用中Activity无需继承{@link skin.support.app.SkinCompatActivity}.
+     *
+     * @param application 应用Application.
+     * @return
+     */
+    public static SkinCompatManager withoutActivity(Application application) {
+        init(application);
+        SkinActivityLifecycle.init(application);
+        return sInstance;
+    }
+
+    private SkinCompatManager(Context context) {
+        mAppContext = context.getApplicationContext();
+        initLoaderStrategy();
+    }
+
+    private void initLoaderStrategy() {
+        mStrategyMap.put(SKIN_LOADER_STRATEGY_NONE, new SkinNoneLoader());
+        mStrategyMap.put(SKIN_LOADER_STRATEGY_ASSETS, new SkinAssetsLoader());
+        mStrategyMap.put(SKIN_LOADER_STRATEGY_BUILD_IN, new SkinBuildInLoader());
+        mStrategyMap.put(SKIN_LOADER_STRATEGY_PREFIX_BUILD_IN, new SkinPrefixBuildInLoader());
+    }
+
+    public Context getContext() {
+        return mAppContext;
+    }
+
+    /**
+     * 添加皮肤包加载策略.
+     *
+     * @param strategy 自定义加载策略
+     * @return
+     */
+    public SkinCompatManager addStrategy(SkinLoaderStrategy strategy) {
+        mStrategyMap.put(strategy.getType(), strategy);
+        return this;
+    }
+
+    public SparseArray<SkinLoaderStrategy> getStrategies() {
+        return mStrategyMap;
+    }
+
+    /**
+     * 自定义View换肤时,可选择添加一个{@link SkinLayoutInflater}
+     *
+     * @param inflater 在{@link skin.support.app.SkinCompatViewInflater#createView(Context, String, String)}方法中调用.
+     * @return
+     */
+    public SkinCompatManager addInflater(SkinLayoutInflater inflater) {
+        mInflaters.add(inflater);
+        return this;
+    }
+
+    public List<SkinLayoutInflater> getInflaters() {
+        return mInflaters;
+    }
+
+
+    /**
+     * 自定义View换肤时,可选择添加一个{@link SkinLayoutInflater}
+     *
+     * @param inflater 在{@link skin.support.app.SkinCompatViewInflater#createView(Context, String, String)}方法中最先调用.
+     * @return
+     */
+    public SkinCompatManager addHookInflater(SkinLayoutInflater inflater) {
+        mHookInflaters.add(inflater);
+        return this;
+    }
+
+    public List<SkinLayoutInflater> getHookInflaters() {
+        return mHookInflaters;
+    }
+
+    /**
+     * 获取当前皮肤包.
+     *
+     * @return
+     */
+    @Deprecated
+    public String getCurSkinName() {
+        return SkinPreference.getInstance().getSkinName();
+    }
+
+    /**
+     * 恢复默认主题,使用应用自带资源.
+     */
+    public void restoreDefaultTheme() {
+        loadSkin("", SKIN_LOADER_STRATEGY_NONE);
+    }
+
+    /**
+     * 设置是否所有Activity都换肤.
+     *
+     * @param enable true: 所有Activity都换肤; false: 添加注解Skinable或实现SkinCompatSupportable的Activity支持换肤.
+     * @return
+     */
+    public SkinCompatManager setSkinAllActivityEnable(boolean enable) {
+        mSkinAllActivityEnable = enable;
+        return this;
+    }
+
+    public boolean isSkinAllActivityEnable() {
+        return mSkinAllActivityEnable;
+    }
+
+    /**
+     * 设置状态栏换肤,使用Theme中的{@link android.R.attr#statusBarColor}属性. 5.0以上有效.
+     *
+     * @param enable true: 打开; false: 关闭.
+     * @return
+     */
+    public SkinCompatManager setSkinStatusBarColorEnable(boolean enable) {
+        mSkinStatusBarColorEnable = enable;
+        return this;
+    }
+
+    public boolean isSkinStatusBarColorEnable() {
+        return mSkinStatusBarColorEnable;
+    }
+
+    /**
+     * 设置WindowBackground换肤,使用Theme中的{@link android.R.attr#windowBackground}属性.
+     *
+     * @param enable true: 打开; false: 关闭.
+     * @return
+     */
+    public SkinCompatManager setSkinWindowBackgroundEnable(boolean enable) {
+        mSkinWindowBackgroundColorEnable = enable;
+        return this;
+    }
+
+    public boolean isSkinWindowBackgroundEnable() {
+        return mSkinWindowBackgroundColorEnable;
+    }
+
+    /**
+     * 加载记录的皮肤包,一般在Application中初始化换肤框架后调用.
+     *
+     * @return
+     */
+    public AsyncTask loadSkin() {
+        String skin = SkinPreference.getInstance().getSkinName();
+        int strategy = SkinPreference.getInstance().getSkinStrategy();
+        if (TextUtils.isEmpty(skin) || strategy == SKIN_LOADER_STRATEGY_NONE) {
+            return null;
+        }
+        return loadSkin(skin, null, strategy);
+    }
+
+    /**
+     * 加载记录的皮肤包,一般在Application中初始化换肤框架后调用.
+     *
+     * @param listener 皮肤包加载监听.
+     * @return
+     */
+    public AsyncTask loadSkin(SkinLoaderListener listener) {
+        String skin = SkinPreference.getInstance().getSkinName();
+        int strategy = SkinPreference.getInstance().getSkinStrategy();
+        if (TextUtils.isEmpty(skin) || strategy == SKIN_LOADER_STRATEGY_NONE) {
+            return null;
+        }
+        return loadSkin(skin, listener, strategy);
+    }
+
+    @Deprecated
+    public AsyncTask loadSkin(String skinName) {
+        return loadSkin(skinName, null);
+    }
+
+    @Deprecated
+    public AsyncTask loadSkin(String skinName, final SkinLoaderListener listener) {
+        return loadSkin(skinName, listener, SKIN_LOADER_STRATEGY_ASSETS);
+    }
+
+    /**
+     * 加载皮肤包.
+     *
+     * @param skinName 皮肤包名称.
+     * @param strategy 皮肤包加载策略.
+     * @return
+     */
+    public AsyncTask loadSkin(String skinName, int strategy) {
+        return loadSkin(skinName, null, strategy);
+    }
+
+    /**
+     * 加载皮肤包.
+     *
+     * @param skinName 皮肤包名称.
+     * @param listener 皮肤包加载监听.
+     * @param strategy 皮肤包加载策略.
+     * @return
+     */
+    public AsyncTask loadSkin(String skinName, SkinLoaderListener listener, int strategy) {
+        SkinLoaderStrategy loaderStrategy = mStrategyMap.get(strategy);
+        if (loaderStrategy == null) {
+            return null;
+        }
+        return new SkinLoadTask(listener, loaderStrategy).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, skinName);
+    }
+
+    private class SkinLoadTask extends AsyncTask<String, Void, String> {
+        private final SkinLoaderListener mListener;
+        private final SkinLoaderStrategy mStrategy;
+
+        SkinLoadTask(@Nullable SkinLoaderListener listener, @NonNull SkinLoaderStrategy strategy) {
+            mListener = listener;
+            mStrategy = strategy;
+        }
+
+        protected void onPreExecute() {
+            if (mListener != null) {
+                mListener.onStart();
+            }
+        }
+
+        @Override
+        protected String doInBackground(String... params) {
+            synchronized (mLock) {
+                while (mLoading) {
+                    try {
+                        mLock.wait();
+                    } catch (InterruptedException e) {
+                        e.printStackTrace();
+                    }
+                }
+                mLoading = true;
+            }
+            try {
+                if (params.length == 1) {
+                    String skinName = mStrategy.loadSkinInBackground(mAppContext, params[0]);
+                    if (TextUtils.isEmpty(skinName)) {
+                        SkinCompatResources.getInstance().reset(mStrategy);
+                    }
+                    return params[0];
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            SkinCompatResources.getInstance().reset();
+            return null;
+        }
+
+        protected void onPostExecute(String skinName) {
+            synchronized (mLock) {
+                // skinName 为""时,恢复默认皮肤
+                if (skinName != null) {
+                    SkinPreference.getInstance().setSkinName(skinName).setSkinStrategy(mStrategy.getType()).commitEditor();
+                    notifyUpdateSkin();
+                    if (mListener != null) mListener.onSuccess();
+                } else {
+                    SkinPreference.getInstance().setSkinName("").setSkinStrategy(SKIN_LOADER_STRATEGY_NONE).commitEditor();
+                    if (mListener != null) mListener.onFailed("皮肤资源获取失败");
+                }
+                mLoading = false;
+                mLock.notifyAll();
+            }
+        }
+    }
+
+    /**
+     * 获取皮肤包包名.
+     *
+     * @param skinPkgPath sdcard中皮肤包路径.
+     * @return
+     */
+    public String getSkinPackageName(String skinPkgPath) {
+        PackageManager mPm = mAppContext.getPackageManager();
+        PackageInfo info = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
+        return info.packageName;
+    }
+
+    /**
+     * 获取皮肤包资源{@link Resources}.
+     *
+     * @param skinPkgPath sdcard中皮肤包路径.
+     * @return
+     */
+    @Nullable
+    public Resources getSkinResources(String skinPkgPath) {
+        try {
+            PackageInfo packageInfo = mAppContext.getPackageManager().getPackageArchiveInfo(skinPkgPath, 0);
+            packageInfo.applicationInfo.sourceDir = skinPkgPath;
+            packageInfo.applicationInfo.publicSourceDir = skinPkgPath;
+            Resources res = mAppContext.getPackageManager().getResourcesForApplication(packageInfo.applicationInfo);
+            Resources superRes = mAppContext.getResources();
+            return new Resources(res.getAssets(), superRes.getDisplayMetrics(), superRes.getConfiguration());
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+}

+ 12 - 0
skin-support/src/main/java/skin/support/annotation/Skinable.java

@@ -0,0 +1,12 @@
+package skin.support.annotation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+@Retention(RUNTIME)
+@Target({TYPE})
+public @interface Skinable {
+}

+ 221 - 0
skin-support/src/main/java/skin/support/app/SkinActivityLifecycle.java

@@ -0,0 +1,221 @@
+package skin.support.app;
+
+import android.app.Activity;
+import android.app.Application;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.view.LayoutInflaterCompat;
+import android.view.LayoutInflater;
+
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Field;
+import java.util.WeakHashMap;
+
+import skin.support.SkinCompatManager;
+import skin.support.annotation.Skinable;
+import skin.support.content.res.SkinCompatResources;
+import skin.support.observe.SkinObservable;
+import skin.support.observe.SkinObserver;
+import skin.support.utils.Slog;
+import skin.support.widget.SkinCompatSupportable;
+import skin.support.content.res.SkinCompatThemeUtils;
+
+import static skin.support.widget.SkinCompatHelper.INVALID_ID;
+import static skin.support.widget.SkinCompatHelper.checkResourceId;
+
+public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks {
+    private static final String TAG = "SkinActivityLifecycle";
+    private static volatile SkinActivityLifecycle sInstance = null;
+    private WeakHashMap<Context, SkinCompatDelegate> mSkinDelegateMap;
+    private WeakHashMap<Context, LazySkinObserver> mSkinObserverMap;
+    /**
+     * 用于记录当前Activity,在换肤后,立即刷新当前Activity以及非Activity创建的View。
+     */
+    private WeakReference<Activity> mCurActivityRef;
+
+    public static SkinActivityLifecycle init(Application application) {
+        if (sInstance == null) {
+            synchronized (SkinActivityLifecycle.class) {
+                if (sInstance == null) {
+                    sInstance = new SkinActivityLifecycle(application);
+                }
+            }
+        }
+        return sInstance;
+    }
+
+    private SkinActivityLifecycle(Application application) {
+        application.registerActivityLifecycleCallbacks(this);
+        installLayoutFactory(application);
+        SkinCompatManager.getInstance().addObserver(getObserver(application));
+    }
+
+    @Override
+    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
+        if (isContextSkinEnable(activity)) {
+            installLayoutFactory(activity);
+            updateStatusBarColor(activity);
+            updateWindowBackground(activity);
+            if (activity instanceof SkinCompatSupportable) {
+                ((SkinCompatSupportable) activity).applySkin();
+            }
+        }
+    }
+
+    @Override
+    public void onActivityStarted(Activity activity) {
+
+    }
+
+    @Override
+    public void onActivityResumed(Activity activity) {
+        mCurActivityRef = new WeakReference<>(activity);
+        if (isContextSkinEnable(activity)) {
+            LazySkinObserver observer = getObserver(activity);
+            SkinCompatManager.getInstance().addObserver(observer);
+            observer.updateSkinIfNeeded();
+        }
+    }
+
+    @Override
+    public void onActivityPaused(Activity activity) {
+    }
+
+    @Override
+    public void onActivityStopped(Activity activity) {
+
+    }
+
+    @Override
+    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
+
+    }
+
+    @Override
+    public void onActivityDestroyed(Activity activity) {
+        if (isContextSkinEnable(activity)) {
+            SkinCompatManager.getInstance().deleteObserver(getObserver(activity));
+            mSkinObserverMap.remove(activity);
+            mSkinDelegateMap.remove(activity);
+        }
+    }
+
+    private void installLayoutFactory(Context context) {
+        LayoutInflater layoutInflater = LayoutInflater.from(context);
+        try {
+            Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
+            field.setAccessible(true);
+            field.setBoolean(layoutInflater, false);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        try {
+            LayoutInflaterCompat.setFactory(layoutInflater, getSkinDelegate(context));
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private SkinCompatDelegate getSkinDelegate(Context context) {
+        if (mSkinDelegateMap == null) {
+            mSkinDelegateMap = new WeakHashMap<>();
+        }
+
+        SkinCompatDelegate mSkinDelegate = mSkinDelegateMap.get(context);
+        if (mSkinDelegate == null) {
+            mSkinDelegate = SkinCompatDelegate.create(context);
+            mSkinDelegateMap.put(context, mSkinDelegate);
+        }
+        return mSkinDelegate;
+    }
+
+    private LazySkinObserver getObserver(final Context context) {
+        if (mSkinObserverMap == null) {
+            mSkinObserverMap = new WeakHashMap<>();
+        }
+        LazySkinObserver observer = mSkinObserverMap.get(context);
+        if (observer == null) {
+            observer = new LazySkinObserver(context);
+            mSkinObserverMap.put(context, observer);
+        }
+        return observer;
+    }
+
+    private void updateStatusBarColor(Activity activity) {
+        if (SkinCompatManager.getInstance().isSkinStatusBarColorEnable()
+                && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            int statusBarColorResId = SkinCompatThemeUtils.getStatusBarColorResId(activity);
+            int colorPrimaryDarkResId = SkinCompatThemeUtils.getColorPrimaryDarkResId(activity);
+            if (checkResourceId(statusBarColorResId) != INVALID_ID) {
+                activity.getWindow().setStatusBarColor(SkinCompatResources.getColor(activity, statusBarColorResId));
+            } else if (checkResourceId(colorPrimaryDarkResId) != INVALID_ID) {
+                activity.getWindow().setStatusBarColor(SkinCompatResources.getColor(activity, colorPrimaryDarkResId));
+            }
+        }
+    }
+
+    private void updateWindowBackground(Activity activity) {
+        if (SkinCompatManager.getInstance().isSkinWindowBackgroundEnable()) {
+            int windowBackgroundResId = SkinCompatThemeUtils.getWindowBackgroundResId(activity);
+            if (checkResourceId(windowBackgroundResId) != INVALID_ID) {
+                Drawable drawable = SkinCompatResources.getDrawableCompat(activity, windowBackgroundResId);
+                if (drawable != null) {
+                    activity.getWindow().setBackgroundDrawable(drawable);
+                }
+            }
+        }
+    }
+
+    private boolean isContextSkinEnable(Context context) {
+        return SkinCompatManager.getInstance().isSkinAllActivityEnable()
+                || context.getClass().getAnnotation(Skinable.class) != null
+                || context instanceof SkinCompatSupportable;
+    }
+
+    private class LazySkinObserver implements SkinObserver {
+        private final Context mContext;
+        private boolean mMarkNeedUpdate = false;
+
+        LazySkinObserver(Context context) {
+            mContext = context;
+        }
+
+        @Override
+        public void updateSkin(SkinObservable observable, Object o) {
+            // 当前Activity,或者非Activity,立即刷新,否则延迟到下次onResume方法中刷新。
+            if (mCurActivityRef == null
+                    || mContext == mCurActivityRef.get()
+                    || !(mContext instanceof Activity)) {
+                updateSkinForce();
+            } else {
+                mMarkNeedUpdate = true;
+            }
+        }
+
+        void updateSkinIfNeeded() {
+            if (mMarkNeedUpdate) {
+                updateSkinForce();
+            }
+        }
+
+        void updateSkinForce() {
+            if (Slog.DEBUG) {
+                Slog.i(TAG, "Context: " + mContext + " updateSkinForce");
+            }
+            if (mContext == null) {
+                return;
+            }
+            if (mContext instanceof Activity && isContextSkinEnable(mContext)) {
+                updateStatusBarColor((Activity) mContext);
+                updateWindowBackground((Activity) mContext);
+            }
+            getSkinDelegate(mContext).applySkin();
+            if (mContext instanceof SkinCompatSupportable) {
+                ((SkinCompatSupportable) mContext).applySkin();
+            }
+            mMarkNeedUpdate = false;
+        }
+    }
+}

+ 91 - 0
skin-support/src/main/java/skin/support/app/SkinCompatActivity.java

@@ -0,0 +1,91 @@
+package skin.support.app;
+
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.view.LayoutInflaterCompat;
+import android.support.v7.app.AppCompatActivity;
+
+import skin.support.SkinCompatManager;
+import skin.support.content.res.SkinCompatResources;
+import skin.support.observe.SkinObservable;
+import skin.support.observe.SkinObserver;
+import skin.support.content.res.SkinCompatThemeUtils;
+
+import static skin.support.widget.SkinCompatHelper.INVALID_ID;
+import static skin.support.widget.SkinCompatHelper.checkResourceId;
+
+/**
+ * Created by ximsfei on 17-1-8.
+ */
+@Deprecated
+public class SkinCompatActivity extends AppCompatActivity implements SkinObserver {
+
+    private SkinCompatDelegate mSkinDelegate;
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        LayoutInflaterCompat.setFactory(getLayoutInflater(), getSkinDelegate());
+        super.onCreate(savedInstanceState);
+        updateStatusBarColor();
+        updateWindowBackground();
+    }
+
+    @NonNull
+    public SkinCompatDelegate getSkinDelegate() {
+        if (mSkinDelegate == null) {
+            mSkinDelegate = SkinCompatDelegate.create(this);
+        }
+        return mSkinDelegate;
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        SkinCompatManager.getInstance().addObserver(this);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        SkinCompatManager.getInstance().deleteObserver(this);
+    }
+
+    /**
+     * @return true: 打开5.0以上状态栏换肤, false: 关闭5.0以上状态栏换肤;
+     */
+    protected boolean skinStatusBarColorEnable() {
+        return true;
+    }
+
+    protected void updateStatusBarColor() {
+        if (skinStatusBarColorEnable() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            int statusBarColorResId = SkinCompatThemeUtils.getStatusBarColorResId(this);
+            int colorPrimaryDarkResId = SkinCompatThemeUtils.getColorPrimaryDarkResId(this);
+            if (checkResourceId(statusBarColorResId) != INVALID_ID) {
+                getWindow().setStatusBarColor(SkinCompatResources.getColor(this, statusBarColorResId));
+            } else if (checkResourceId(colorPrimaryDarkResId) != INVALID_ID) {
+                getWindow().setStatusBarColor(SkinCompatResources.getColor(this, colorPrimaryDarkResId));
+            }
+        }
+    }
+
+    protected void updateWindowBackground() {
+        int windowBackgroundResId = SkinCompatThemeUtils.getWindowBackgroundResId(this);
+        if (checkResourceId(windowBackgroundResId) != INVALID_ID) {
+            Drawable drawable = SkinCompatResources.getDrawableCompat(this, windowBackgroundResId);
+            if (drawable != null) {
+                getWindow().setBackgroundDrawable(drawable);
+            }
+        }
+    }
+
+    @Override
+    public void updateSkin(SkinObservable observable, Object o) {
+        updateStatusBarColor();
+        updateWindowBackground();
+        getSkinDelegate().applySkin();
+    }
+}

+ 111 - 0
skin-support/src/main/java/skin/support/app/SkinCompatDelegate.java

@@ -0,0 +1,111 @@
+package skin.support.app;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.v4.view.LayoutInflaterFactory;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.VectorEnabledTintResources;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewParent;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+import skin.support.widget.SkinCompatSupportable;
+
+/**
+ * Created by ximsfei on 2017/1/9.
+ */
+
+public class SkinCompatDelegate implements LayoutInflaterFactory {
+    private final Context mContext;
+    private SkinCompatViewInflater mSkinCompatViewInflater;
+    private List<WeakReference<SkinCompatSupportable>> mSkinHelpers = new ArrayList<>();
+
+    private SkinCompatDelegate(Context context) {
+        mContext = context;
+    }
+
+    @Override
+    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
+        View view = null;
+        try {
+            view = createView(parent, name, context, attrs);
+        } catch (Exception e) {
+
+        }
+
+        if (view == null) {
+            return null;
+        }
+        if (view instanceof SkinCompatSupportable) {
+            mSkinHelpers.add(new WeakReference<>((SkinCompatSupportable) view));
+        }
+
+        return view;
+    }
+
+    public View createView(View parent, final String name, @NonNull Context context,
+                           @NonNull AttributeSet attrs) {
+        final boolean isPre21 = Build.VERSION.SDK_INT < 21;
+
+        if (mSkinCompatViewInflater == null) {
+            mSkinCompatViewInflater = new SkinCompatViewInflater();
+        }
+
+        // We only want the View to inherit its context if we're running pre-v21
+        final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);
+
+        return mSkinCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
+                isPre21, /* Only read android:theme pre-L (L+ handles this anyway) */
+                true, /* Read read app:theme as a fallback at all times for legacy reasons */
+                VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
+        );
+    }
+
+    private boolean shouldInheritContext(ViewParent parent) {
+        if (parent == null) {
+            // The initial parent is null so just return false
+            return false;
+        }
+        if (mContext instanceof Activity) {
+            final View windowDecor = ((Activity) mContext).getWindow().getDecorView();
+            while (true) {
+                if (parent == null) {
+                    // Bingo. We've hit a view which has a null parent before being terminated from
+                    // the loop. This is (most probably) because it's the root view in an inflation
+                    // call, therefore we should inherit. This works as the inflated layout is only
+                    // added to the hierarchy at the end of the inflate() call.
+                    return true;
+                } else if (parent == windowDecor || !(parent instanceof View)
+                        || ViewCompat.isAttachedToWindow((View) parent)) {
+                    // We have either hit the window's decor view, a parent which isn't a View
+                    // (i.e. ViewRootImpl), or an attached view, so we know that the original parent
+                    // is currently added to the view hierarchy. This means that it has not be
+                    // inflated in the current inflate() call and we should not inherit the context.
+                    return false;
+                }
+                parent = parent.getParent();
+            }
+        }
+        return false;
+    }
+
+    public static SkinCompatDelegate create(Context context) {
+        return new SkinCompatDelegate(context);
+    }
+
+    public void applySkin() {
+        if (mSkinHelpers != null && !mSkinHelpers.isEmpty()) {
+            for (WeakReference ref : mSkinHelpers) {
+                if (ref != null && ref.get() != null) {
+                    ((SkinCompatSupportable) ref.get()).applySkin();
+                }
+            }
+        }
+    }
+}

+ 393 - 0
skin-support/src/main/java/skin/support/app/SkinCompatViewInflater.java

@@ -0,0 +1,393 @@
+package skin.support.app;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.res.TypedArray;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.util.ArrayMap;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.appcompat.R;
+import android.support.v7.view.ContextThemeWrapper;
+import android.support.v7.widget.TintContextWrapper;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.InflateException;
+import android.view.View;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Map;
+
+import skin.support.SkinCompatManager;
+import skin.support.widget.SkinCompatAutoCompleteTextView;
+import skin.support.widget.SkinCompatButton;
+import skin.support.widget.SkinCompatCheckBox;
+import skin.support.widget.SkinCompatCheckedTextView;
+import skin.support.widget.SkinCompatEditText;
+import skin.support.widget.SkinCompatFrameLayout;
+import skin.support.widget.SkinCompatImageButton;
+import skin.support.widget.SkinCompatImageView;
+import skin.support.widget.SkinCompatLinearLayout;
+import skin.support.widget.SkinCompatMultiAutoCompleteTextView;
+import skin.support.widget.SkinCompatProgressBar;
+import skin.support.widget.SkinCompatRadioButton;
+import skin.support.widget.SkinCompatRadioGroup;
+import skin.support.widget.SkinCompatRatingBar;
+import skin.support.widget.SkinCompatRelativeLayout;
+import skin.support.widget.SkinCompatScrollView;
+import skin.support.widget.SkinCompatSeekBar;
+import skin.support.widget.SkinCompatSpinner;
+import skin.support.widget.SkinCompatTextView;
+import skin.support.widget.SkinCompatToolbar;
+import skin.support.widget.SkinCompatView;
+
+/**
+ * Created by ximsfei on 17-1-9.
+ */
+
+public class SkinCompatViewInflater {
+    private static final Class<?>[] sConstructorSignature = new Class[]{
+            Context.class, AttributeSet.class};
+    private static final int[] sOnClickAttrs = new int[]{android.R.attr.onClick};
+
+    private static final String[] sClassPrefixList = {
+            "android.widget.",
+            "android.view.",
+            "android.webkit."
+    };
+
+    private static final String LOG_TAG = "SkinCompatViewInflater";
+
+    private static final Map<String, Constructor<? extends View>> sConstructorMap
+            = new ArrayMap<>();
+
+    private final Object[] mConstructorArgs = new Object[2];
+
+    public final View createView(View parent, final String name, @NonNull Context context,
+                                 @NonNull AttributeSet attrs, boolean inheritContext,
+                                 boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
+        final Context originalContext = context;
+
+        // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
+        // by using the parent's context
+        if (inheritContext && parent != null) {
+            context = parent.getContext();
+        }
+        if (readAndroidTheme || readAppTheme) {
+            // We then apply the theme on the context, if specified
+            context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
+        }
+        if (wrapContext) {
+            context = TintContextWrapper.wrap(context);
+        }
+
+        View view = createViewFromHackInflater(context, name, attrs);
+
+        // We need to 'inject' our tint aware Views in place of the standard framework versions
+        if (view == null) {
+            view = createViewFromFV(context, name, attrs);
+        }
+
+        if (view == null) {
+            view = createViewFromV7(context, name, attrs);
+        }
+
+        if (view == null) {
+            view = createViewFromInflater(context, name, attrs);
+        }
+
+        if (view == null) {
+            view = createViewFromTag(context, name, attrs);
+        }
+
+        if (view != null) {
+            // If we have created a view, check it's android:onClick
+            checkOnClickListener(view, attrs);
+        }
+
+        return view;
+    }
+
+    private View createViewFromHackInflater(Context context, String name, AttributeSet attrs) {
+        View view = null;
+        for (SkinLayoutInflater inflater : SkinCompatManager.getInstance().getHookInflaters()) {
+            view = inflater.createView(context, name, attrs);
+            if (view == null) {
+                continue;
+            } else {
+                break;
+            }
+        }
+        return view;
+    }
+
+    private View createViewFromFV(Context context, String name, AttributeSet attrs) {
+        View view = null;
+        if (name.contains(".")) {
+            return null;
+        }
+        switch (name) {
+            case "View":
+                view = new SkinCompatView(context, attrs);
+                break;
+            case "LinearLayout":
+                view = new SkinCompatLinearLayout(context, attrs);
+                break;
+            case "RelativeLayout":
+                view = new SkinCompatRelativeLayout(context, attrs);
+                break;
+            case "FrameLayout":
+                view = new SkinCompatFrameLayout(context, attrs);
+                break;
+            case "TextView":
+                view = new SkinCompatTextView(context, attrs);
+                break;
+            case "ImageView":
+                view = new SkinCompatImageView(context, attrs);
+                break;
+            case "Button":
+                view = new SkinCompatButton(context, attrs);
+                break;
+            case "EditText":
+                view = new SkinCompatEditText(context, attrs);
+                break;
+            case "Spinner":
+                view = new SkinCompatSpinner(context, attrs);
+                break;
+            case "ImageButton":
+                view = new SkinCompatImageButton(context, attrs);
+                break;
+            case "CheckBox":
+                view = new SkinCompatCheckBox(context, attrs);
+                break;
+            case "RadioButton":
+                view = new SkinCompatRadioButton(context, attrs);
+                break;
+            case "RadioGroup":
+                view = new SkinCompatRadioGroup(context, attrs);
+                break;
+            case "CheckedTextView":
+                view = new SkinCompatCheckedTextView(context, attrs);
+                break;
+            case "AutoCompleteTextView":
+                view = new SkinCompatAutoCompleteTextView(context, attrs);
+                break;
+            case "MultiAutoCompleteTextView":
+                view = new SkinCompatMultiAutoCompleteTextView(context, attrs);
+                break;
+            case "RatingBar":
+                view = new SkinCompatRatingBar(context, attrs);
+                break;
+            case "SeekBar":
+                view = new SkinCompatSeekBar(context, attrs);
+                break;
+            case "ProgressBar":
+                view = new SkinCompatProgressBar(context, attrs);
+                break;
+            case "ScrollView":
+                view = new SkinCompatScrollView(context, attrs);
+                break;
+        }
+        return view;
+    }
+
+    private View createViewFromV7(Context context, String name, AttributeSet attrs) {
+        View view = null;
+        switch (name) {
+            case "android.support.v7.widget.Toolbar":
+                view = new SkinCompatToolbar(context, attrs);
+                break;
+        }
+        return view;
+    }
+
+    private View createViewFromInflater(Context context, String name, AttributeSet attrs) {
+        View view = null;
+        for (SkinLayoutInflater inflater : SkinCompatManager.getInstance().getInflaters()) {
+            view = inflater.createView(context, name, attrs);
+            if (view == null) {
+                continue;
+            } else {
+                break;
+            }
+        }
+        return view;
+    }
+
+    public View createViewFromTag(Context context, String name, AttributeSet attrs) {
+        if (name.equals("view")) {
+            name = attrs.getAttributeValue(null, "class");
+        }
+
+        try {
+            mConstructorArgs[0] = context;
+            mConstructorArgs[1] = attrs;
+
+            if (-1 == name.indexOf('.')) {
+                for (int i = 0; i < sClassPrefixList.length; i++) {
+                    final View view = createView(context, name, sClassPrefixList[i]);
+                    if (view != null) {
+                        return view;
+                    }
+                }
+                return null;
+            } else {
+                return createView(context, name, null);
+            }
+        } catch (Exception e) {
+            // We do not want to catch these, lets return null and let the actual LayoutInflater
+            // try
+            return null;
+        } finally {
+            // Don't retain references on context.
+            mConstructorArgs[0] = null;
+            mConstructorArgs[1] = null;
+        }
+    }
+
+    /**
+     * android:onClick doesn't handle views with a ContextWrapper context. This method
+     * backports new framework functionality to traverse the Context wrappers to find a
+     * suitable target.
+     */
+    private void checkOnClickListener(View view, AttributeSet attrs) {
+        final Context context = view.getContext();
+
+        if (!(context instanceof ContextWrapper) ||
+                (Build.VERSION.SDK_INT >= 15 && !ViewCompat.hasOnClickListeners(view))) {
+            // Skip our compat functionality if: the Context isn't a ContextWrapper, or
+            // the view doesn't have an OnClickListener (we can only rely on this on API 15+ so
+            // always use our compat code on older devices)
+            return;
+        }
+
+        final TypedArray a = context.obtainStyledAttributes(attrs, sOnClickAttrs);
+        final String handlerName = a.getString(0);
+        if (handlerName != null) {
+            view.setOnClickListener(new DeclaredOnClickListener(view, handlerName));
+        }
+        a.recycle();
+    }
+
+    private View createView(Context context, String name, String prefix)
+            throws ClassNotFoundException, InflateException {
+        Constructor<? extends View> constructor = sConstructorMap.get(name);
+
+        try {
+            if (constructor == null) {
+                // Class not found in the cache, see if it's real, and try to add it
+                Class<? extends View> clazz = context.getClassLoader().loadClass(
+                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
+
+                constructor = clazz.getConstructor(sConstructorSignature);
+                sConstructorMap.put(name, constructor);
+            }
+            constructor.setAccessible(true);
+            return constructor.newInstance(mConstructorArgs);
+        } catch (Exception e) {
+            // We do not want to catch these, lets return null and let the actual LayoutInflater
+            // try
+            return null;
+        }
+    }
+
+    /**
+     * Allows us to emulate the {@code android:theme} attribute for devices before L.
+     */
+    private static Context themifyContext(Context context, AttributeSet attrs,
+                                          boolean useAndroidTheme, boolean useAppTheme) {
+        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.View, 0, 0);
+        int themeId = 0;
+        if (useAndroidTheme) {
+            // First try reading android:theme if enabled
+            themeId = a.getResourceId(R.styleable.View_android_theme, 0);
+        }
+        if (useAppTheme && themeId == 0) {
+            // ...if that didn't work, try reading app:theme (for legacy reasons) if enabled
+            themeId = a.getResourceId(R.styleable.View_theme, 0);
+
+            if (themeId != 0) {
+                Log.i(LOG_TAG, "app:theme is now deprecated. "
+                        + "Please move to using android:theme instead.");
+            }
+        }
+        a.recycle();
+
+        if (themeId != 0 && (!(context instanceof ContextThemeWrapper)
+                || ((ContextThemeWrapper) context).getThemeResId() != themeId)) {
+            // If the context isn't a ContextThemeWrapper, or it is but does not have
+            // the same theme as we need, wrap it in a new wrapper
+            context = new ContextThemeWrapper(context, themeId);
+        }
+        return context;
+    }
+
+    /**
+     * An implementation of OnClickListener that attempts to lazily load a
+     * named click handling method from a parent or ancestor context.
+     */
+    private static class DeclaredOnClickListener implements View.OnClickListener {
+        private final View mHostView;
+        private final String mMethodName;
+
+        private Method mResolvedMethod;
+        private Context mResolvedContext;
+
+        public DeclaredOnClickListener(@NonNull View hostView, @NonNull String methodName) {
+            mHostView = hostView;
+            mMethodName = methodName;
+        }
+
+        @Override
+        public void onClick(@NonNull View v) {
+            if (mResolvedMethod == null) {
+                resolveMethod(mHostView.getContext(), mMethodName);
+            }
+
+            try {
+                mResolvedMethod.invoke(mResolvedContext, v);
+            } catch (IllegalAccessException e) {
+                throw new IllegalStateException(
+                        "Could not execute non-public method for android:onClick", e);
+            } catch (InvocationTargetException e) {
+                throw new IllegalStateException(
+                        "Could not execute method for android:onClick", e);
+            }
+        }
+
+        @NonNull
+        private void resolveMethod(@Nullable Context context, @NonNull String name) {
+            while (context != null) {
+                try {
+                    if (!context.isRestricted()) {
+                        final Method method = context.getClass().getMethod(mMethodName, View.class);
+                        if (method != null) {
+                            mResolvedMethod = method;
+                            mResolvedContext = context;
+                            return;
+                        }
+                    }
+                } catch (NoSuchMethodException e) {
+                    // Failed to find method, keep searching up the hierarchy.
+                }
+
+                if (context instanceof ContextWrapper) {
+                    context = ((ContextWrapper) context).getBaseContext();
+                } else {
+                    // Can't search up the hierarchy, null out and fail.
+                    context = null;
+                }
+            }
+
+            final int id = mHostView.getId();
+            final String idText = id == View.NO_ID ? "" : " with id '"
+                    + mHostView.getContext().getResources().getResourceEntryName(id) + "'";
+            throw new IllegalStateException("Could not find method " + mMethodName
+                    + "(View) in a parent or ancestor Context for android:onClick "
+                    + "attribute defined on view " + mHostView.getClass() + idText);
+        }
+    }
+}

+ 14 - 0
skin-support/src/main/java/skin/support/app/SkinLayoutInflater.java

@@ -0,0 +1,14 @@
+package skin.support.app;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * Created by ximsfei on 2017/1/13.
+ */
+
+public interface SkinLayoutInflater {
+    View createView(@NonNull Context context, final String name, @NonNull AttributeSet attrs);
+}

+ 600 - 0
skin-support/src/main/java/skin/support/content/res/ColorState.java

@@ -0,0 +1,600 @@
+package skin.support.content.res;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.support.annotation.ColorRes;
+import android.text.TextUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import skin.support.exception.SkinCompatException;
+import skin.support.utils.Slog;
+
+public final class ColorState {
+    private static final String TAG = "ColorState";
+    boolean onlyDefaultColor;
+    String colorName;
+    String colorWindowFocused;
+    String colorSelected;
+    String colorFocused;
+    String colorEnabled;
+    String colorPressed;
+    String colorChecked;
+    String colorActivated;
+    String colorAccelerated;
+    String colorHovered;
+    String colorDragCanAccept;
+    String colorDragHovered;
+    String colorDefault;
+
+    ColorState(String colorWindowFocused, String colorSelected, String colorFocused,
+               String colorEnabled, String colorPressed, String colorChecked, String colorActivated,
+               String colorAccelerated, String colorHovered, String colorDragCanAccept,
+               String colorDragHovered, String colorDefault) {
+        this.colorWindowFocused = colorWindowFocused;
+        this.colorSelected = colorSelected;
+        this.colorFocused = colorFocused;
+        this.colorEnabled = colorEnabled;
+        this.colorPressed = colorPressed;
+        this.colorChecked = colorChecked;
+        this.colorActivated = colorActivated;
+        this.colorAccelerated = colorAccelerated;
+        this.colorHovered = colorHovered;
+        this.colorDragCanAccept = colorDragCanAccept;
+        this.colorDragHovered = colorDragHovered;
+        this.colorDefault = colorDefault;
+        this.onlyDefaultColor = TextUtils.isEmpty(colorWindowFocused)
+                && TextUtils.isEmpty(colorSelected)
+                && TextUtils.isEmpty(colorFocused)
+                && TextUtils.isEmpty(colorEnabled)
+                && TextUtils.isEmpty(colorPressed)
+                && TextUtils.isEmpty(colorChecked)
+                && TextUtils.isEmpty(colorActivated)
+                && TextUtils.isEmpty(colorAccelerated)
+                && TextUtils.isEmpty(colorHovered)
+                && TextUtils.isEmpty(colorDragCanAccept)
+                && TextUtils.isEmpty(colorDragHovered);
+        if (onlyDefaultColor) {
+            if (!colorDefault.startsWith("#")) {
+                throw new SkinCompatException("Default color cannot be a reference, when only default color is available!");
+            }
+        }
+    }
+
+    ColorState(String colorName, String colorDefault) {
+        this.colorName = colorName;
+        this.colorDefault = colorDefault;
+        this.onlyDefaultColor = true;
+        if (!colorDefault.startsWith("#")) {
+            throw new SkinCompatException("Default color cannot be a reference, when only default color is available!");
+        }
+    }
+
+    public boolean isOnlyDefaultColor() {
+        return onlyDefaultColor;
+    }
+
+    public String getColorName() {
+        return colorName;
+    }
+
+    public String getColorWindowFocused() {
+        return colorWindowFocused;
+    }
+
+    public String getColorSelected() {
+        return colorSelected;
+    }
+
+    public String getColorFocused() {
+        return colorFocused;
+    }
+
+    public String getColorEnabled() {
+        return colorEnabled;
+    }
+
+    public String getColorPressed() {
+        return colorPressed;
+    }
+
+    public String getColorChecked() {
+        return colorChecked;
+    }
+
+    public String getColorActivated() {
+        return colorActivated;
+    }
+
+    public String getColorAccelerated() {
+        return colorAccelerated;
+    }
+
+    public String getColorHovered() {
+        return colorHovered;
+    }
+
+    public String getColorDragCanAccept() {
+        return colorDragCanAccept;
+    }
+
+    public String getColorDragHovered() {
+        return colorDragHovered;
+    }
+
+    public String getColorDefault() {
+        return colorDefault;
+    }
+
+    ColorStateList parse() {
+        if (onlyDefaultColor) {
+            int defaultColor = Color.parseColor(colorDefault);
+            return ColorStateList.valueOf(defaultColor);
+        }
+        return parseAll();
+    }
+
+    private ColorStateList parseAll() {
+        int stateColorCount = 0;
+        List<int[]> stateSetList = new ArrayList<>();
+        List<Integer> stateColorList = new ArrayList<>();
+        if (!TextUtils.isEmpty(colorWindowFocused)) {
+            try {
+                String windowFocusedColorStr = getColorStr(colorWindowFocused);
+                if (!TextUtils.isEmpty(windowFocusedColorStr)) {
+                    int windowFocusedColorInt = Color.parseColor(windowFocusedColorStr);
+                    stateSetList.add(SkinCompatThemeUtils.WINDOW_FOCUSED_STATE_SET);
+                    stateColorList.add(windowFocusedColorInt);
+                    stateColorCount++;
+                }
+            } catch (Exception e) {
+            }
+        }
+        if (!TextUtils.isEmpty(colorSelected)) {
+            try {
+                String colorSelectedStr = getColorStr(colorSelected);
+                if (!TextUtils.isEmpty(colorSelectedStr)) {
+                    int selectedColorInt = Color.parseColor(colorSelectedStr);
+                    stateSetList.add(SkinCompatThemeUtils.SELECTED_STATE_SET);
+                    stateColorList.add(selectedColorInt);
+                    stateColorCount++;
+                }
+            } catch (Exception e) {
+            }
+        }
+        if (!TextUtils.isEmpty(colorFocused)) {
+            try {
+                String colorFocusedStr = getColorStr(colorFocused);
+                if (!TextUtils.isEmpty(colorFocusedStr)) {
+                    int focusedColorInt = Color.parseColor(colorFocusedStr);
+                    stateSetList.add(SkinCompatThemeUtils.FOCUSED_STATE_SET);
+                    stateColorList.add(focusedColorInt);
+                    stateColorCount++;
+                }
+            } catch (Exception e) {
+            }
+        }
+        if (!TextUtils.isEmpty(colorEnabled)) {
+            try {
+                String colorEnabledStr = getColorStr(colorEnabled);
+                if (!TextUtils.isEmpty(colorEnabledStr)) {
+                    int enabledColorInt = Color.parseColor(colorEnabledStr);
+                    stateSetList.add(SkinCompatThemeUtils.ENABLED_STATE_SET);
+                    stateColorList.add(enabledColorInt);
+                    stateColorCount++;
+                }
+            } catch (Exception e) {
+            }
+        }
+        if (!TextUtils.isEmpty(colorPressed)) {
+            try {
+                String colorPressedStr = getColorStr(colorPressed);
+                if (!TextUtils.isEmpty(colorPressedStr)) {
+                    int pressedColorInt = Color.parseColor(colorPressedStr);
+                    stateSetList.add(SkinCompatThemeUtils.PRESSED_STATE_SET);
+                    stateColorList.add(pressedColorInt);
+                    stateColorCount++;
+                }
+            } catch (Exception e) {
+            }
+        }
+        if (!TextUtils.isEmpty(colorChecked)) {
+            try {
+                String colorCheckedStr = getColorStr(colorChecked);
+                if (!TextUtils.isEmpty(colorCheckedStr)) {
+                    int checkedColorInt = Color.parseColor(colorCheckedStr);
+                    stateSetList.add(SkinCompatThemeUtils.CHECKED_STATE_SET);
+                    stateColorList.add(checkedColorInt);
+                    stateColorCount++;
+                }
+            } catch (Exception e) {
+            }
+        }
+        if (!TextUtils.isEmpty(colorActivated)) {
+            try {
+                String colorActivatedStr = getColorStr(colorActivated);
+                if (!TextUtils.isEmpty(colorActivatedStr)) {
+                    int activatedColorInt = Color.parseColor(colorActivatedStr);
+                    stateSetList.add(SkinCompatThemeUtils.ACTIVATED_STATE_SET);
+                    stateColorList.add(activatedColorInt);
+                    stateColorCount++;
+                }
+            } catch (Exception e) {
+            }
+        }
+        if (!TextUtils.isEmpty(colorAccelerated)) {
+            try {
+                String colorAcceleratedStr = getColorStr(colorAccelerated);
+                if (!TextUtils.isEmpty(colorAcceleratedStr)) {
+                    int acceleratedColorInt = Color.parseColor(colorAcceleratedStr);
+                    stateSetList.add(SkinCompatThemeUtils.ACCELERATED_STATE_SET);
+                    stateColorList.add(acceleratedColorInt);
+                    stateColorCount++;
+                }
+            } catch (Exception e) {
+            }
+        }
+        if (!TextUtils.isEmpty(colorHovered)) {
+            try {
+                String colorHoveredStr = getColorStr(colorHovered);
+                if (!TextUtils.isEmpty(colorHoveredStr)) {
+                    int hoveredColorInt = Color.parseColor(colorHoveredStr);
+                    stateSetList.add(SkinCompatThemeUtils.HOVERED_STATE_SET);
+                    stateColorList.add(hoveredColorInt);
+                    stateColorCount++;
+                }
+            } catch (Exception e) {
+            }
+        }
+        if (!TextUtils.isEmpty(colorDragCanAccept)) {
+            try {
+                String colorDragCanAcceptStr = getColorStr(colorDragCanAccept);
+                if (!TextUtils.isEmpty(colorDragCanAcceptStr)) {
+                    int dragCanAcceptColorInt = Color.parseColor(colorDragCanAcceptStr);
+                    stateSetList.add(SkinCompatThemeUtils.DRAG_CAN_ACCEPT_STATE_SET);
+                    stateColorList.add(dragCanAcceptColorInt);
+                    stateColorCount++;
+                }
+            } catch (Exception e) {
+            }
+        }
+        if (!TextUtils.isEmpty(colorDragHovered)) {
+            try {
+                String colorDragHoveredStr = getColorStr(colorDragHovered);
+                if (!TextUtils.isEmpty(colorDragHoveredStr)) {
+                    int dragHoveredColorInt = Color.parseColor(colorDragHoveredStr);
+                    stateSetList.add(SkinCompatThemeUtils.DRAG_HOVERED_STATE_SET);
+                    stateColorList.add(dragHoveredColorInt);
+                    stateColorCount++;
+                }
+            } catch (Exception e) {
+            }
+        }
+        try {
+            String colorDefaultStr = getColorStr(colorDefault);
+            if (!TextUtils.isEmpty(colorDefaultStr)) {
+                int baseColor = Color.parseColor(colorDefaultStr);
+                stateSetList.add(SkinCompatThemeUtils.EMPTY_STATE_SET);
+                stateColorList.add(baseColor);
+                stateColorCount++;
+            }
+
+            final int[][] states = new int[stateColorCount][];
+            final int[] colors = new int[stateColorCount];
+            for (int index = 0; index < stateColorCount; index++) {
+                states[index] = stateSetList.get(index);
+                colors[index] = stateColorList.get(index);
+            }
+            return new ColorStateList(states, colors);
+        } catch (Exception e) {
+            if (Slog.DEBUG) {
+                Slog.i(TAG, colorName + " parse failure.");
+            }
+            SkinCompatUserThemeManager.get().removeColorState(colorName);
+            return null;
+        }
+    }
+
+    private String getColorStr(String colorName) {
+        if (colorName.startsWith("#")) {
+            return colorName;
+        } else {
+            ColorState stateRef = SkinCompatUserThemeManager.get().getColorState(colorName);
+            if (stateRef != null) {
+                if (stateRef.isOnlyDefaultColor()) {
+                    return stateRef.colorDefault;
+                } else {
+                    if (Slog.DEBUG) {
+                        Slog.i(TAG, colorName + " cannot reference " + stateRef.colorName);
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    static boolean checkColorValid(String name, String color) {
+        // 不为空
+        boolean colorValid = !TextUtils.isEmpty(color)
+                // 不以#开始,说明是引用其他颜色值 或者以#开始,则长度必须为7或9
+                && (!color.startsWith("#") || color.length() == 7 || color.length() == 9);
+        if (Slog.DEBUG && !colorValid) {
+            Slog.i(TAG, "Invalid color -> " + name + ": " + color);
+        }
+        return colorValid;
+    }
+
+    static JSONObject toJSONObject(ColorState state) throws JSONException {
+        JSONObject object = new JSONObject();
+        if (state.onlyDefaultColor) {
+            object.putOpt("colorName", state.colorName)
+                    .putOpt("colorDefault", state.colorDefault)
+                    .putOpt("onlyDefaultColor", state.onlyDefaultColor);
+        } else {
+            object.putOpt("colorName", state.colorName)
+                    .putOpt("colorWindowFocused", state.colorWindowFocused)
+                    .putOpt("colorSelected", state.colorSelected)
+                    .putOpt("colorFocused", state.colorFocused)
+                    .putOpt("colorEnabled", state.colorEnabled)
+                    .putOpt("colorPressed", state.colorPressed)
+                    .putOpt("colorChecked", state.colorChecked)
+                    .putOpt("colorActivated", state.colorActivated)
+                    .putOpt("colorAccelerated", state.colorAccelerated)
+                    .putOpt("colorHovered", state.colorHovered)
+                    .putOpt("colorDragCanAccept", state.colorDragCanAccept)
+                    .putOpt("colorDragHovered", state.colorDragHovered)
+                    .putOpt("colorDefault", state.colorDefault)
+                    .putOpt("onlyDefaultColor", state.onlyDefaultColor);
+        }
+        return object;
+    }
+
+    static ColorState fromJSONObject(JSONObject jsonObject) {
+        if (jsonObject.has("colorName")
+                && jsonObject.has("colorDefault")
+                && jsonObject.has("onlyDefaultColor")) {
+            try {
+                boolean onlyDefaultColor = jsonObject.getBoolean("onlyDefaultColor");
+                String colorName = jsonObject.getString("colorName");
+                String colorDefault = jsonObject.getString("colorDefault");
+                if (onlyDefaultColor) {
+                    return new ColorState(colorName, colorDefault);
+                } else {
+                    ColorBuilder builder = new ColorBuilder();
+                    builder.setColorDefault(colorDefault);
+                    if (jsonObject.has("colorWindowFocused")) {
+                        builder.setColorWindowFocused(jsonObject.getString("colorWindowFocused"));
+                    }
+                    if (jsonObject.has("colorSelected")) {
+                        builder.setColorSelected(jsonObject.getString("colorSelected"));
+                    }
+                    if (jsonObject.has("colorFocused")) {
+                        builder.setColorFocused(jsonObject.getString("colorFocused"));
+                    }
+                    if (jsonObject.has("colorEnabled")) {
+                        builder.setColorEnabled(jsonObject.getString("colorEnabled"));
+                    }
+                    if (jsonObject.has("colorPressed")) {
+                        builder.setColorPressed(jsonObject.getString("colorPressed"));
+                    }
+                    if (jsonObject.has("colorChecked")) {
+                        builder.setColorChecked(jsonObject.getString("colorChecked"));
+                    }
+                    if (jsonObject.has("colorActivated")) {
+                        builder.setColorActivated(jsonObject.getString("colorActivated"));
+                    }
+                    if (jsonObject.has("colorAccelerated")) {
+                        builder.setColorAccelerated(jsonObject.getString("colorAccelerated"));
+                    }
+                    if (jsonObject.has("colorHovered")) {
+                        builder.setColorHovered(jsonObject.getString("colorHovered"));
+                    }
+                    if (jsonObject.has("colorDragCanAccept")) {
+                        builder.setColorDragCanAccept(jsonObject.getString("colorDragCanAccept"));
+                    }
+                    if (jsonObject.has("colorDragHovered")) {
+                        builder.setColorDragHovered(jsonObject.getString("colorDragHovered"));
+                    }
+                    ColorState state = builder.build();
+                    state.colorName = colorName;
+                    return state;
+                }
+            } catch (JSONException e) {
+                e.printStackTrace();
+            }
+        }
+        return null;
+    }
+
+    public static class ColorBuilder {
+        String colorWindowFocused;
+        String colorSelected;
+        String colorFocused;
+        String colorEnabled;
+        String colorPressed;
+        String colorChecked;
+        String colorActivated;
+        String colorAccelerated;
+        String colorHovered;
+        String colorDragCanAccept;
+        String colorDragHovered;
+        String colorDefault;
+
+        public ColorBuilder() {
+        }
+
+        public ColorBuilder(ColorState state) {
+            colorWindowFocused = state.colorWindowFocused;
+            colorSelected = state.colorSelected;
+            colorFocused = state.colorFocused;
+            colorEnabled = state.colorEnabled;
+            colorPressed = state.colorPressed;
+            colorChecked = state.colorChecked;
+            colorActivated = state.colorActivated;
+            colorAccelerated = state.colorAccelerated;
+            colorHovered = state.colorHovered;
+            colorDragCanAccept = state.colorDragCanAccept;
+            colorDragHovered = state.colorDragHovered;
+            colorDefault = state.colorDefault;
+        }
+
+        public ColorBuilder setColorWindowFocused(String colorWindowFocused) {
+            if (checkColorValid("colorWindowFocused", colorWindowFocused)) {
+                this.colorWindowFocused = colorWindowFocused;
+            }
+            return this;
+
+        }
+
+        public ColorBuilder setColorWindowFocused(Context context, @ColorRes int colorRes) {
+            this.colorWindowFocused = context.getResources().getResourceEntryName(colorRes);
+            return this;
+        }
+
+        public ColorBuilder setColorSelected(String colorSelected) {
+            if (checkColorValid("colorSelected", colorSelected)) {
+                this.colorSelected = colorSelected;
+            }
+            return this;
+        }
+
+        public ColorBuilder setColorSelected(Context context, @ColorRes int colorRes) {
+            this.colorSelected = context.getResources().getResourceEntryName(colorRes);
+            return this;
+        }
+
+        public ColorBuilder setColorFocused(String colorFocused) {
+            if (checkColorValid("colorFocused", colorFocused)) {
+                this.colorFocused = colorFocused;
+            }
+            return this;
+        }
+
+        public ColorBuilder setColorFocused(Context context, @ColorRes int colorRes) {
+            this.colorFocused = context.getResources().getResourceEntryName(colorRes);
+            return this;
+        }
+
+        public ColorBuilder setColorEnabled(String colorEnabled) {
+            if (checkColorValid("colorEnabled", colorEnabled)) {
+                this.colorEnabled = colorEnabled;
+            }
+            return this;
+        }
+
+        public ColorBuilder setColorEnabled(Context context, @ColorRes int colorRes) {
+            this.colorEnabled = context.getResources().getResourceEntryName(colorRes);
+            return this;
+        }
+
+        public ColorBuilder setColorChecked(String colorChecked) {
+            if (checkColorValid("colorChecked", colorChecked)) {
+                this.colorChecked = colorChecked;
+            }
+            return this;
+        }
+
+        public ColorBuilder setColorChecked(Context context, @ColorRes int colorRes) {
+            this.colorChecked = context.getResources().getResourceEntryName(colorRes);
+            return this;
+        }
+
+        public ColorBuilder setColorPressed(String colorPressed) {
+            if (checkColorValid("colorPressed", colorPressed)) {
+                this.colorPressed = colorPressed;
+            }
+            return this;
+        }
+
+        public ColorBuilder setColorPressed(Context context, @ColorRes int colorRes) {
+            this.colorPressed = context.getResources().getResourceEntryName(colorRes);
+            return this;
+        }
+
+        public ColorBuilder setColorActivated(String colorActivated) {
+            if (checkColorValid("colorActivated", colorActivated)) {
+                this.colorActivated = colorActivated;
+            }
+            return this;
+        }
+
+        public ColorBuilder setColorActivated(Context context, @ColorRes int colorRes) {
+            this.colorActivated = context.getResources().getResourceEntryName(colorRes);
+            return this;
+        }
+
+        public ColorBuilder setColorAccelerated(String colorAccelerated) {
+            if (checkColorValid("colorAccelerated", colorAccelerated)) {
+                this.colorAccelerated = colorAccelerated;
+            }
+            return this;
+        }
+
+        public ColorBuilder setColorAccelerated(Context context, @ColorRes int colorRes) {
+            this.colorAccelerated = context.getResources().getResourceEntryName(colorRes);
+            return this;
+        }
+
+        public ColorBuilder setColorHovered(String colorHovered) {
+            if (checkColorValid("colorHovered", colorHovered)) {
+                this.colorHovered = colorHovered;
+            }
+            return this;
+        }
+
+        public ColorBuilder setColorHovered(Context context, @ColorRes int colorRes) {
+            this.colorHovered = context.getResources().getResourceEntryName(colorRes);
+            return this;
+        }
+
+        public ColorBuilder setColorDragCanAccept(String colorDragCanAccept) {
+            if (checkColorValid("colorDragCanAccept", colorDragCanAccept)) {
+                this.colorDragCanAccept = colorDragCanAccept;
+            }
+            return this;
+        }
+
+        public ColorBuilder setColorDragCanAccept(Context context, @ColorRes int colorRes) {
+            this.colorDragCanAccept = context.getResources().getResourceEntryName(colorRes);
+            return this;
+        }
+
+        public ColorBuilder setColorDragHovered(String colorDragHovered) {
+            if (checkColorValid("colorDragHovered", colorDragHovered)) {
+                this.colorDragHovered = colorDragHovered;
+            }
+            return this;
+        }
+
+        public ColorBuilder setColorDragHovered(Context context, @ColorRes int colorRes) {
+            this.colorDragHovered = context.getResources().getResourceEntryName(colorRes);
+            return this;
+        }
+
+        public ColorBuilder setColorDefault(String colorDefault) {
+            if (checkColorValid("colorDefault", colorDefault)) {
+                this.colorDefault = colorDefault;
+            }
+            return this;
+        }
+
+        public ColorBuilder setColorDefault(Context context, @ColorRes int colorRes) {
+            this.colorDefault = context.getResources().getResourceEntryName(colorRes);
+            return this;
+        }
+
+        public ColorState build() {
+            if (TextUtils.isEmpty(colorDefault)) {
+                throw new SkinCompatException("Default color can not empty!");
+            }
+            return new ColorState(colorWindowFocused, colorSelected, colorFocused,
+                    colorEnabled, colorPressed, colorChecked, colorActivated, colorAccelerated,
+                    colorHovered, colorDragCanAccept, colorDragHovered, colorDefault);
+        }
+    }
+}

+ 754 - 0
skin-support/src/main/java/skin/support/content/res/SkinCompatDrawableManager.java

@@ -0,0 +1,754 @@
+package skin.support.content.res;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Drawable.ConstantState;
+import android.graphics.drawable.LayerDrawable;
+import android.os.Build;
+import android.support.annotation.ColorInt;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+import android.support.graphics.drawable.AnimatedVectorDrawableCompat;
+import android.support.graphics.drawable.VectorDrawableCompat;
+import android.support.v4.graphics.drawable.DrawableCompat;
+import android.support.v4.util.ArrayMap;
+import android.support.v4.util.LongSparseArray;
+import android.support.v4.util.LruCache;
+import android.support.v7.appcompat.R;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.TypedValue;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.lang.ref.WeakReference;
+import java.util.WeakHashMap;
+
+import static android.support.v4.graphics.ColorUtils.compositeColors;
+import static skin.support.content.res.SkinCompatThemeUtils.getDisabledThemeAttrColor;
+import static skin.support.content.res.SkinCompatThemeUtils.getThemeAttrColor;
+import static skin.support.content.res.SkinCompatThemeUtils.getThemeAttrColorStateList;
+
+final class SkinCompatDrawableManager {
+    private interface InflateDelegate {
+        Drawable createFromXmlInner(@NonNull Context context, @NonNull XmlPullParser parser,
+                                    @NonNull AttributeSet attrs, @Nullable Resources.Theme theme);
+    }
+
+    private static final String TAG = SkinCompatDrawableManager.class.getSimpleName();
+    private static final boolean DEBUG = false;
+    private static final PorterDuff.Mode DEFAULT_MODE = PorterDuff.Mode.SRC_IN;
+    private static final String SKIP_DRAWABLE_TAG = "appcompat_skip_skip";
+
+    private static final String PLATFORM_VD_CLAZZ = "android.graphics.drawable.VectorDrawable";
+
+    private static SkinCompatDrawableManager INSTANCE;
+
+    public static SkinCompatDrawableManager get() {
+        if (INSTANCE == null) {
+            INSTANCE = new SkinCompatDrawableManager();
+            installDefaultInflateDelegates(INSTANCE);
+        }
+        return INSTANCE;
+    }
+
+    private static void installDefaultInflateDelegates(@NonNull SkinCompatDrawableManager manager) {
+        // This sdk version check will affect src:appCompat code path.
+        // Although VectorDrawable exists in Android framework from Lollipop, AppCompat will use the
+        // VectorDrawableCompat before Nougat to utilize the bug fixes in VectorDrawableCompat.
+        if (Build.VERSION.SDK_INT < 24) {
+            manager.addDelegate("vector", new VdcInflateDelegate());
+            // AnimatedVectorDrawableCompat only works on API v11+
+            manager.addDelegate("animated-vector", new AvdcInflateDelegate());
+        }
+    }
+
+    private static final ColorFilterLruCache COLOR_FILTER_CACHE = new ColorFilterLruCache(6);
+
+    /**
+     * Drawables which should be tinted with the value of {@code R.attr.colorControlNormal},
+     * using the default mode using a raw color filter.
+     */
+    private static final int[] COLORFILTER_TINT_COLOR_CONTROL_NORMAL = {
+            R.drawable.abc_textfield_search_default_mtrl_alpha,
+            R.drawable.abc_textfield_default_mtrl_alpha,
+            R.drawable.abc_ab_share_pack_mtrl_alpha
+    };
+
+    /**
+     * Drawables which should be tinted with the value of {@code R.attr.colorControlNormal}, using
+     * {@link DrawableCompat}'s tinting functionality.
+     */
+    private static final int[] TINT_COLOR_CONTROL_NORMAL = {
+            R.drawable.abc_ic_commit_search_api_mtrl_alpha,
+            R.drawable.abc_seekbar_tick_mark_material,
+            R.drawable.abc_ic_menu_share_mtrl_alpha,
+            R.drawable.abc_ic_menu_copy_mtrl_am_alpha,
+            R.drawable.abc_ic_menu_cut_mtrl_alpha,
+            R.drawable.abc_ic_menu_selectall_mtrl_alpha,
+            R.drawable.abc_ic_menu_paste_mtrl_am_alpha
+    };
+
+    /**
+     * Drawables which should be tinted with the value of {@code R.attr.colorControlActivated},
+     * using a color filter.
+     */
+    private static final int[] COLORFILTER_COLOR_CONTROL_ACTIVATED = {
+            R.drawable.abc_textfield_activated_mtrl_alpha,
+            R.drawable.abc_textfield_search_activated_mtrl_alpha,
+            R.drawable.abc_cab_background_top_mtrl_alpha,
+            R.drawable.abc_text_cursor_material,
+            R.drawable.abc_text_select_handle_left_mtrl_dark,
+            R.drawable.abc_text_select_handle_middle_mtrl_dark,
+            R.drawable.abc_text_select_handle_right_mtrl_dark,
+            R.drawable.abc_text_select_handle_left_mtrl_light,
+            R.drawable.abc_text_select_handle_middle_mtrl_light,
+            R.drawable.abc_text_select_handle_right_mtrl_light
+    };
+
+    /**
+     * Drawables which should be tinted with the value of {@code android.R.attr.colorBackground},
+     * using the {@link PorterDuff.Mode#MULTIPLY} mode and a color filter.
+     */
+    private static final int[] COLORFILTER_COLOR_BACKGROUND_MULTIPLY = {
+            R.drawable.abc_popup_background_mtrl_mult,
+            R.drawable.abc_cab_background_internal_bg,
+            R.drawable.abc_menu_hardkey_panel_mtrl_mult
+    };
+
+    /**
+     * Drawables which should be tinted using a state list containing values of
+     * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated}
+     */
+    private static final int[] TINT_COLOR_CONTROL_STATE_LIST = {
+            R.drawable.abc_tab_indicator_material,
+            R.drawable.abc_textfield_search_material
+    };
+
+    /**
+     * Drawables which should be tinted using a state list containing values of
+     * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated} for the checked
+     * state.
+     */
+    private static final int[] TINT_CHECKABLE_BUTTON_LIST = {
+            R.drawable.abc_btn_check_material,
+            R.drawable.abc_btn_radio_material
+    };
+
+    private WeakHashMap<Context, SparseArray<ColorStateList>> mTintLists;
+    private ArrayMap<String, InflateDelegate> mDelegates;
+    private SparseArray<String> mKnownDrawableIdTags;
+
+    private final Object mDrawableCacheLock = new Object();
+    private final WeakHashMap<Context, LongSparseArray<WeakReference<ConstantState>>>
+            mDrawableCaches = new WeakHashMap<>(0);
+
+    private TypedValue mTypedValue;
+
+    private boolean mHasCheckedVectorDrawableSetup;
+
+    void clearCaches() {
+        mDrawableCaches.clear();
+        if (mKnownDrawableIdTags != null) {
+            mKnownDrawableIdTags.clear();
+        }
+        if (mTintLists != null) {
+            mTintLists.clear();
+        }
+        COLOR_FILTER_CACHE.evictAll();
+    }
+
+    public Drawable getDrawable(@NonNull Context context, @DrawableRes int resId) {
+        return getDrawable(context, resId, false);
+    }
+
+    Drawable getDrawable(@NonNull Context context, @DrawableRes int resId,
+                         boolean failIfNotKnown) {
+        checkVectorDrawableSetup(context);
+
+        Drawable drawable = loadDrawableFromDelegates(context, resId);
+        if (drawable == null) {
+            drawable = createDrawableIfNeeded(context, resId);
+        }
+        if (drawable == null) {
+            drawable = SkinCompatResources.getDrawable(context, resId);
+        }
+
+        if (drawable != null) {
+            // Tint it if needed
+            drawable = tintDrawable(context, resId, failIfNotKnown, drawable);
+        }
+        if (drawable != null) {
+            // See if we need to 'fix' the drawable
+            SkinCompatDrawableUtils.fixDrawable(drawable);
+        }
+        return drawable;
+    }
+
+    public void onConfigurationChanged(@NonNull Context context) {
+        synchronized (mDrawableCacheLock) {
+            LongSparseArray<WeakReference<ConstantState>> cache = mDrawableCaches.get(context);
+            if (cache != null) {
+                // Crude, but we'll just clear the cache when the configuration changes
+                cache.clear();
+            }
+        }
+    }
+
+    private static long createCacheKey(TypedValue tv) {
+        return (((long) tv.assetCookie) << 32) | tv.data;
+    }
+
+    private Drawable createDrawableIfNeeded(@NonNull Context context,
+                                            @DrawableRes final int resId) {
+        if (mTypedValue == null) {
+            mTypedValue = new TypedValue();
+        }
+        final TypedValue tv = mTypedValue;
+        SkinCompatResources.getValue(context, resId, tv, true);
+        final long key = createCacheKey(tv);
+
+        Drawable dr = getCachedDrawable(context, key);
+        if (dr != null) {
+            // If we got a cached drawable, return it
+            return dr;
+        }
+
+        // Else we need to try and create one...
+        if (resId == R.drawable.abc_cab_background_top_material) {
+            dr = new LayerDrawable(new Drawable[]{
+                    getDrawable(context, R.drawable.abc_cab_background_internal_bg),
+                    getDrawable(context, R.drawable.abc_cab_background_top_mtrl_alpha)
+            });
+        }
+
+        if (dr != null) {
+            dr.setChangingConfigurations(tv.changingConfigurations);
+            // If we reached here then we created a new drawable, add it to the cache
+            addDrawableToCache(context, key, dr);
+        }
+
+        return dr;
+    }
+
+    private Drawable tintDrawable(@NonNull Context context, @DrawableRes int resId,
+                                  boolean failIfNotKnown, @NonNull Drawable drawable) {
+        final ColorStateList tintList = getTintList(context, resId);
+        if (tintList != null) {
+            // First mutate the Drawable, then wrap it and set the tint list
+            if (SkinCompatDrawableUtils.canSafelyMutateDrawable(drawable)) {
+                drawable = drawable.mutate();
+            }
+            drawable = DrawableCompat.wrap(drawable);
+            DrawableCompat.setTintList(drawable, tintList);
+
+            // If there is a blending mode specified for the drawable, use it
+            final PorterDuff.Mode tintMode = getTintMode(resId);
+            if (tintMode != null) {
+                DrawableCompat.setTintMode(drawable, tintMode);
+            }
+        } else if (resId == R.drawable.abc_seekbar_track_material) {
+            LayerDrawable ld = (LayerDrawable) drawable;
+            setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.background),
+                    getThemeAttrColor(context, R.attr.colorControlNormal), DEFAULT_MODE);
+            setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.secondaryProgress),
+                    getThemeAttrColor(context, R.attr.colorControlNormal), DEFAULT_MODE);
+            setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.progress),
+                    getThemeAttrColor(context, R.attr.colorControlActivated), DEFAULT_MODE);
+        } else if (resId == R.drawable.abc_ratingbar_material
+                || resId == R.drawable.abc_ratingbar_indicator_material
+                || resId == R.drawable.abc_ratingbar_small_material) {
+            LayerDrawable ld = (LayerDrawable) drawable;
+            setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.background),
+                    getDisabledThemeAttrColor(context, R.attr.colorControlNormal),
+                    DEFAULT_MODE);
+            setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.secondaryProgress),
+                    getThemeAttrColor(context, R.attr.colorControlActivated), DEFAULT_MODE);
+            setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.progress),
+                    getThemeAttrColor(context, R.attr.colorControlActivated), DEFAULT_MODE);
+        } else {
+            final boolean tinted = tintDrawableUsingColorFilter(context, resId, drawable);
+            if (!tinted && failIfNotKnown) {
+                // If we didn't tint using a ColorFilter, and we're set to fail if we don't
+                // know the id, return null
+                drawable = null;
+            }
+        }
+        return drawable;
+    }
+
+    private Drawable loadDrawableFromDelegates(@NonNull Context context, @DrawableRes int resId) {
+        if (mDelegates != null && !mDelegates.isEmpty()) {
+            if (mKnownDrawableIdTags != null) {
+                final String cachedTagName = mKnownDrawableIdTags.get(resId);
+                if (SKIP_DRAWABLE_TAG.equals(cachedTagName)
+                        || (cachedTagName != null && mDelegates.get(cachedTagName) == null)) {
+                    // If we don't have a delegate for the drawable tag, or we've been set to
+                    // skip it, fail fast and return null
+                    if (DEBUG) {
+                        Log.d(TAG, "[loadDrawableFromDelegates] Skipping drawable: "
+                                + context.getResources().getResourceName(resId));
+                    }
+                    return null;
+                }
+            } else {
+                // Create an id cache as we'll need one later
+                mKnownDrawableIdTags = new SparseArray<>();
+            }
+
+            if (mTypedValue == null) {
+                mTypedValue = new TypedValue();
+            }
+            final TypedValue tv = mTypedValue;
+            SkinCompatResources.getValue(context, resId, tv, true);
+
+            final long key = createCacheKey(tv);
+
+            Drawable dr = getCachedDrawable(context, key);
+            if (dr != null) {
+                if (DEBUG) {
+                    Log.i(TAG, "[loadDrawableFromDelegates] Returning cached drawable: " +
+                            context.getResources().getResourceName(resId));
+                }
+                // We have a cached drawable, return it!
+                return dr;
+            }
+
+            if (tv.string != null && tv.string.toString().endsWith(".xml")) {
+                // If the resource is an XML file, let's try and parse it
+                try {
+                    final XmlPullParser parser = SkinCompatResources.getXml(context, resId);
+                    final AttributeSet attrs = Xml.asAttributeSet(parser);
+                    int type;
+                    while ((type = parser.next()) != XmlPullParser.START_TAG &&
+                            type != XmlPullParser.END_DOCUMENT) {
+                        // Empty loop
+                    }
+                    if (type != XmlPullParser.START_TAG) {
+                        throw new XmlPullParserException("No start tag found");
+                    }
+
+                    final String tagName = parser.getName();
+                    // Add the tag name to the cache
+                    mKnownDrawableIdTags.append(resId, tagName);
+
+                    // Now try and find a delegate for the tag name and inflate if found
+                    final InflateDelegate delegate = mDelegates.get(tagName);
+                    if (delegate != null) {
+                        dr = delegate.createFromXmlInner(context, parser, attrs, null);
+                    }
+                    if (dr != null) {
+                        // Add it to the drawable cache
+                        dr.setChangingConfigurations(tv.changingConfigurations);
+                        if (addDrawableToCache(context, key, dr) && DEBUG) {
+                            Log.i(TAG, "[loadDrawableFromDelegates] Saved drawable to cache: " +
+                                    context.getResources().getResourceName(resId));
+                        }
+                    }
+                } catch (Exception e) {
+                    Log.e(TAG, "Exception while inflating drawable", e);
+                }
+            }
+            if (dr == null) {
+                // If we reach here then the delegate inflation of the resource failed. Mark it as
+                // bad so we skip the id next time
+                mKnownDrawableIdTags.append(resId, SKIP_DRAWABLE_TAG);
+            }
+            return dr;
+        }
+
+        return null;
+    }
+
+    private Drawable getCachedDrawable(@NonNull final Context context, final long key) {
+        synchronized (mDrawableCacheLock) {
+            final LongSparseArray<WeakReference<ConstantState>> cache
+                    = mDrawableCaches.get(context);
+            if (cache == null) {
+                return null;
+            }
+
+            final WeakReference<ConstantState> wr = cache.get(key);
+            if (wr != null) {
+                // We have the key, and the secret
+                ConstantState entry = wr.get();
+                if (entry != null) {
+                    return entry.newDrawable(context.getResources());
+                } else {
+                    // Our entry has been purged
+                    cache.delete(key);
+                }
+            }
+        }
+        return null;
+    }
+
+    private boolean addDrawableToCache(@NonNull final Context context, final long key,
+                                       @NonNull final Drawable drawable) {
+        final ConstantState cs = drawable.getConstantState();
+        if (cs != null) {
+            synchronized (mDrawableCacheLock) {
+                LongSparseArray<WeakReference<ConstantState>> cache = mDrawableCaches.get(context);
+                if (cache == null) {
+                    cache = new LongSparseArray<>();
+                    mDrawableCaches.put(context, cache);
+                }
+                cache.put(key, new WeakReference<ConstantState>(cs));
+            }
+            return true;
+        }
+        return false;
+    }
+
+    static boolean tintDrawableUsingColorFilter(@NonNull Context context,
+                                                @DrawableRes final int resId, @NonNull Drawable drawable) {
+        PorterDuff.Mode tintMode = DEFAULT_MODE;
+        boolean colorAttrSet = false;
+        int colorAttr = 0;
+        int alpha = -1;
+
+        if (arrayContains(COLORFILTER_TINT_COLOR_CONTROL_NORMAL, resId)) {
+            colorAttr = R.attr.colorControlNormal;
+            colorAttrSet = true;
+        } else if (arrayContains(COLORFILTER_COLOR_CONTROL_ACTIVATED, resId)) {
+            colorAttr = R.attr.colorControlActivated;
+            colorAttrSet = true;
+        } else if (arrayContains(COLORFILTER_COLOR_BACKGROUND_MULTIPLY, resId)) {
+            colorAttr = android.R.attr.colorBackground;
+            colorAttrSet = true;
+            tintMode = PorterDuff.Mode.MULTIPLY;
+        } else if (resId == R.drawable.abc_list_divider_mtrl_alpha) {
+            colorAttr = android.R.attr.colorForeground;
+            colorAttrSet = true;
+            alpha = Math.round(0.16f * 255);
+        } else if (resId == R.drawable.abc_dialog_material_background) {
+            colorAttr = android.R.attr.colorBackground;
+            colorAttrSet = true;
+        }
+
+        if (colorAttrSet) {
+            if (SkinCompatDrawableUtils.canSafelyMutateDrawable(drawable)) {
+                drawable = drawable.mutate();
+            }
+
+            final int color = getThemeAttrColor(context, colorAttr);
+            drawable.setColorFilter(getPorterDuffColorFilter(color, tintMode));
+
+            if (alpha != -1) {
+                drawable.setAlpha(alpha);
+            }
+
+            if (DEBUG) {
+                Log.d(TAG, "[tintDrawableUsingColorFilter] Tinted "
+                        + context.getResources().getResourceName(resId) +
+                        " with color: #" + Integer.toHexString(color));
+            }
+            return true;
+        }
+        return false;
+    }
+
+    private void addDelegate(@NonNull String tagName, @NonNull InflateDelegate delegate) {
+        if (mDelegates == null) {
+            mDelegates = new ArrayMap<>();
+        }
+        mDelegates.put(tagName, delegate);
+    }
+
+    private void removeDelegate(@NonNull String tagName, @NonNull InflateDelegate delegate) {
+        if (mDelegates != null && mDelegates.get(tagName) == delegate) {
+            mDelegates.remove(tagName);
+        }
+    }
+
+    private static boolean arrayContains(int[] array, int value) {
+        for (int id : array) {
+            if (id == value) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    static PorterDuff.Mode getTintMode(final int resId) {
+        PorterDuff.Mode mode = null;
+
+        if (resId == R.drawable.abc_switch_thumb_material) {
+            mode = PorterDuff.Mode.MULTIPLY;
+        }
+
+        return mode;
+    }
+
+    ColorStateList getTintList(@NonNull Context context, @DrawableRes int resId) {
+        // Try the cache first (if it exists)
+        ColorStateList tint = getTintListFromCache(context, resId);
+
+        if (tint == null) {
+            // ...if the cache did not contain a color state list, try and create one
+            if (resId == R.drawable.abc_edit_text_material) {
+                tint = SkinCompatResources.getColorStateList(context, R.color.abc_tint_edittext);
+            } else if (resId == R.drawable.abc_switch_track_mtrl_alpha) {
+                tint = SkinCompatResources.getColorStateList(context, R.color.abc_tint_switch_track);
+            } else if (resId == R.drawable.abc_switch_thumb_material) {
+                tint = createSwitchThumbColorStateList(context);
+            } else if (resId == R.drawable.abc_btn_default_mtrl_shape) {
+                tint = createDefaultButtonColorStateList(context);
+            } else if (resId == R.drawable.abc_btn_borderless_material) {
+                tint = createBorderlessButtonColorStateList(context);
+            } else if (resId == R.drawable.abc_btn_colored_material) {
+                tint = createColoredButtonColorStateList(context);
+            } else if (resId == R.drawable.abc_spinner_mtrl_am_alpha
+                    || resId == R.drawable.abc_spinner_textfield_background_material) {
+                tint = SkinCompatResources.getColorStateList(context, R.color.abc_tint_spinner);
+            } else if (arrayContains(TINT_COLOR_CONTROL_NORMAL, resId)) {
+                tint = getThemeAttrColorStateList(context, R.attr.colorControlNormal);
+            } else if (arrayContains(TINT_COLOR_CONTROL_STATE_LIST, resId)) {
+                tint = SkinCompatResources.getColorStateList(context, R.color.abc_tint_default);
+            } else if (arrayContains(TINT_CHECKABLE_BUTTON_LIST, resId)) {
+                tint = SkinCompatResources.getColorStateList(context, R.color.abc_tint_btn_checkable);
+            } else if (resId == R.drawable.abc_seekbar_thumb_material) {
+                tint = SkinCompatResources.getColorStateList(context, R.color.abc_tint_seek_thumb);
+            }
+
+            if (tint != null) {
+                addTintListToCache(context, resId, tint);
+            }
+        }
+        return tint;
+    }
+
+    private ColorStateList getTintListFromCache(@NonNull Context context, @DrawableRes int resId) {
+        if (mTintLists != null) {
+            final SparseArray<ColorStateList> tints = mTintLists.get(context);
+            return tints != null ? tints.get(resId) : null;
+        }
+        return null;
+    }
+
+    private void addTintListToCache(@NonNull Context context, @DrawableRes int resId,
+                                    @NonNull ColorStateList tintList) {
+        if (mTintLists == null) {
+            mTintLists = new WeakHashMap<>();
+        }
+        SparseArray<ColorStateList> themeTints = mTintLists.get(context);
+        if (themeTints == null) {
+            themeTints = new SparseArray<>();
+            mTintLists.put(context, themeTints);
+        }
+        themeTints.append(resId, tintList);
+    }
+
+    private ColorStateList createDefaultButtonColorStateList(@NonNull Context context) {
+        return createButtonColorStateList(context,
+                getThemeAttrColor(context, R.attr.colorButtonNormal));
+    }
+
+    private ColorStateList createBorderlessButtonColorStateList(@NonNull Context context) {
+        // We ignore the custom tint for borderless buttons
+        return createButtonColorStateList(context, Color.TRANSPARENT);
+    }
+
+    private ColorStateList createColoredButtonColorStateList(@NonNull Context context) {
+        return createButtonColorStateList(context,
+                getThemeAttrColor(context, R.attr.colorAccent));
+    }
+
+    private ColorStateList createButtonColorStateList(@NonNull final Context context,
+                                                      @ColorInt final int baseColor) {
+        final int[][] states = new int[4][];
+        final int[] colors = new int[4];
+        int i = 0;
+
+        final int colorControlHighlight = getThemeAttrColor(context, R.attr.colorControlHighlight);
+        final int disabledColor = getDisabledThemeAttrColor(context, R.attr.colorButtonNormal);
+
+        // Disabled state
+        states[i] = SkinCompatThemeUtils.DISABLED_STATE_SET;
+        colors[i] = disabledColor;
+        i++;
+
+        states[i] = SkinCompatThemeUtils.PRESSED_STATE_SET;
+        colors[i] = compositeColors(colorControlHighlight, baseColor);
+        i++;
+
+        states[i] = SkinCompatThemeUtils.FOCUSED_STATE_SET;
+        colors[i] = compositeColors(colorControlHighlight, baseColor);
+        i++;
+
+        // Default enabled state
+        states[i] = SkinCompatThemeUtils.EMPTY_STATE_SET;
+        colors[i] = baseColor;
+        i++;
+
+        return new ColorStateList(states, colors);
+    }
+
+    private ColorStateList createSwitchThumbColorStateList(Context context) {
+        final int[][] states = new int[3][];
+        final int[] colors = new int[3];
+        int i = 0;
+
+        final ColorStateList thumbColor = SkinCompatThemeUtils.getThemeAttrColorStateList(context,
+                R.attr.colorSwitchThumbNormal);
+
+        if (thumbColor != null && thumbColor.isStateful()) {
+            // If colorSwitchThumbNormal is a valid ColorStateList, extract the default and
+            // disabled colors from it
+
+            // Disabled state
+            states[i] = SkinCompatThemeUtils.DISABLED_STATE_SET;
+            colors[i] = thumbColor.getColorForState(states[i], 0);
+            i++;
+
+            states[i] = SkinCompatThemeUtils.CHECKED_STATE_SET;
+            colors[i] = SkinCompatThemeUtils.getThemeAttrColor(context, R.attr.colorControlActivated);
+            i++;
+
+            // Default enabled state
+            states[i] = SkinCompatThemeUtils.EMPTY_STATE_SET;
+            colors[i] = thumbColor.getDefaultColor();
+            i++;
+        } else {
+            // Else we'll use an approximation using the default disabled alpha
+
+            // Disabled state
+            states[i] = SkinCompatThemeUtils.DISABLED_STATE_SET;
+            colors[i] = SkinCompatThemeUtils.getDisabledThemeAttrColor(context, R.attr.colorSwitchThumbNormal);
+            i++;
+
+            states[i] = SkinCompatThemeUtils.CHECKED_STATE_SET;
+            colors[i] = SkinCompatThemeUtils.getThemeAttrColor(context, R.attr.colorControlActivated);
+            i++;
+
+            // Default enabled state
+            states[i] = SkinCompatThemeUtils.EMPTY_STATE_SET;
+            colors[i] = SkinCompatThemeUtils.getThemeAttrColor(context, R.attr.colorSwitchThumbNormal);
+            i++;
+        }
+
+        return new ColorStateList(states, colors);
+    }
+
+    private static class ColorFilterLruCache extends LruCache<Integer, PorterDuffColorFilter> {
+
+        public ColorFilterLruCache(int maxSize) {
+            super(maxSize);
+        }
+
+        PorterDuffColorFilter get(int color, PorterDuff.Mode mode) {
+            return get(generateCacheKey(color, mode));
+        }
+
+        PorterDuffColorFilter put(int color, PorterDuff.Mode mode, PorterDuffColorFilter filter) {
+            return put(generateCacheKey(color, mode), filter);
+        }
+
+        private static int generateCacheKey(int color, PorterDuff.Mode mode) {
+            int hashCode = 1;
+            hashCode = 31 * hashCode + color;
+            hashCode = 31 * hashCode + mode.hashCode();
+            return hashCode;
+        }
+    }
+
+    private static PorterDuffColorFilter createTintFilter(ColorStateList tint,
+                                                          PorterDuff.Mode tintMode, final int[] state) {
+        if (tint == null || tintMode == null) {
+            return null;
+        }
+        final int color = tint.getColorForState(state, Color.TRANSPARENT);
+        return getPorterDuffColorFilter(color, tintMode);
+    }
+
+    public static PorterDuffColorFilter getPorterDuffColorFilter(int color, PorterDuff.Mode mode) {
+        // First, lets see if the cache already contains the color filter
+        PorterDuffColorFilter filter = COLOR_FILTER_CACHE.get(color, mode);
+
+        if (filter == null) {
+            // Cache miss, so create a color filter and add it to the cache
+            filter = new PorterDuffColorFilter(color, mode);
+            COLOR_FILTER_CACHE.put(color, mode, filter);
+        }
+
+        return filter;
+    }
+
+    private static void setPorterDuffColorFilter(Drawable d, int color, PorterDuff.Mode mode) {
+        if (SkinCompatDrawableUtils.canSafelyMutateDrawable(d)) {
+            d = d.mutate();
+        }
+        d.setColorFilter(getPorterDuffColorFilter(color, mode == null ? DEFAULT_MODE : mode));
+    }
+
+    private void checkVectorDrawableSetup(@NonNull Context context) {
+        if (mHasCheckedVectorDrawableSetup) {
+            // We've already checked so return now...
+            return;
+        }
+        // Here we will check that a known Vector drawable resource inside AppCompat can be
+        // correctly decoded
+        mHasCheckedVectorDrawableSetup = true;
+        final Drawable d = getDrawable(context, R.drawable.abc_vector_test);
+        if (d == null || !isVectorDrawable(d)) {
+            mHasCheckedVectorDrawableSetup = false;
+            throw new IllegalStateException("This app has been built with an incorrect "
+                    + "configuration. Please configure your build for VectorDrawableCompat.");
+        }
+    }
+
+    private static boolean isVectorDrawable(@NonNull Drawable d) {
+        return d instanceof VectorDrawableCompat
+                || PLATFORM_VD_CLAZZ.equals(d.getClass().getName());
+    }
+
+    private static class VdcInflateDelegate implements InflateDelegate {
+        VdcInflateDelegate() {
+        }
+
+        @SuppressLint("NewApi")
+        @Override
+        public Drawable createFromXmlInner(@NonNull Context context, @NonNull XmlPullParser parser,
+                                           @NonNull AttributeSet attrs, @Nullable Resources.Theme theme) {
+            try {
+                return VectorDrawableCompat
+                        .createFromXmlInner(context.getResources(), parser, attrs, theme);
+            } catch (Exception e) {
+                Log.e("VdcInflateDelegate", "Exception while inflating <vector>", e);
+                return null;
+            }
+        }
+    }
+
+    @RequiresApi(11)
+    @TargetApi(11)
+    private static class AvdcInflateDelegate implements InflateDelegate {
+        AvdcInflateDelegate() {
+        }
+
+        @SuppressLint("NewApi")
+        @Override
+        public Drawable createFromXmlInner(@NonNull Context context, @NonNull XmlPullParser parser,
+                                           @NonNull AttributeSet attrs, @Nullable Resources.Theme theme) {
+            try {
+                return AnimatedVectorDrawableCompat
+                        .createFromXmlInner(context, context.getResources(), parser, attrs, theme);
+            } catch (Exception e) {
+                Log.e("AvdcInflateDelegate", "Exception while inflating <animated-vector>", e);
+                return null;
+            }
+        }
+    }
+}

+ 88 - 0
skin-support/src/main/java/skin/support/content/res/SkinCompatDrawableUtils.java

@@ -0,0 +1,88 @@
+package skin.support.content.res;
+
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.DrawableContainer;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.InsetDrawable;
+import android.graphics.drawable.LayerDrawable;
+import android.graphics.drawable.ScaleDrawable;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.v7.graphics.drawable.DrawableWrapper;
+
+import skin.support.utils.SkinCompatVersionUtils;
+
+class SkinCompatDrawableUtils {
+
+    private static final String VECTOR_DRAWABLE_CLAZZ_NAME
+            = "android.graphics.drawable.VectorDrawable";
+
+    /**
+     * Attempt the fix any issues in the given drawable, usually caused by platform bugs in the
+     * implementation. This method should be call after retrieval from
+     * {@link android.content.res.Resources} or a {@link android.content.res.TypedArray}.
+     */
+    static void fixDrawable(@NonNull final Drawable drawable) {
+        if (Build.VERSION.SDK_INT == 21
+                && VECTOR_DRAWABLE_CLAZZ_NAME.equals(drawable.getClass().getName())) {
+            fixVectorDrawableTinting(drawable);
+        }
+    }
+
+    /**
+     * Some drawable implementations have problems with mutation. This method returns false if
+     * there is a known issue in the given drawable's implementation.
+     */
+    public static boolean canSafelyMutateDrawable(@NonNull Drawable drawable) {
+        if (Build.VERSION.SDK_INT < 15 && drawable instanceof InsetDrawable) {
+            return false;
+        } else if (Build.VERSION.SDK_INT < 15 && drawable instanceof GradientDrawable) {
+            // GradientDrawable has a bug pre-ICS which results in mutate() resulting
+            // in loss of color
+            return false;
+        } else if (Build.VERSION.SDK_INT < 17 && drawable instanceof LayerDrawable) {
+            return false;
+        }
+
+        if (drawable instanceof DrawableContainer) {
+            // If we have a DrawableContainer, let's traverse it's child array
+            final Drawable.ConstantState state = drawable.getConstantState();
+            if (state instanceof DrawableContainer.DrawableContainerState) {
+                final DrawableContainer.DrawableContainerState containerState =
+                        (DrawableContainer.DrawableContainerState) state;
+                for (final Drawable child : containerState.getChildren()) {
+                    if (!canSafelyMutateDrawable(child)) {
+                        return false;
+                    }
+                }
+            }
+        } else if (SkinCompatVersionUtils.isV4DrawableWrapper(drawable)) {
+            return canSafelyMutateDrawable(SkinCompatVersionUtils.getV4DrawableWrapperWrappedDrawable(drawable));
+        } else if (SkinCompatVersionUtils.isV4WrappedDrawable(drawable)) {
+            return canSafelyMutateDrawable(SkinCompatVersionUtils.getV4WrappedDrawableWrappedDrawable(drawable));
+        } else if (drawable instanceof DrawableWrapper) {
+            return canSafelyMutateDrawable(((DrawableWrapper) drawable).getWrappedDrawable());
+        } else if (drawable instanceof ScaleDrawable) {
+            return canSafelyMutateDrawable(((ScaleDrawable) drawable).getDrawable());
+        }
+
+        return true;
+    }
+
+    /**
+     * VectorDrawable has an issue on API 21 where it sometimes doesn't create its tint filter.
+     * Fixed by toggling it's state to force a filter creation.
+     */
+    private static void fixVectorDrawableTinting(final Drawable drawable) {
+        final int[] originalState = drawable.getState();
+        if (originalState == null || originalState.length == 0) {
+            // The drawable doesn't have a state, so set it to be checked
+            drawable.setState(SkinCompatThemeUtils.CHECKED_STATE_SET);
+        } else {
+            // Else the drawable does have a state, so clear it
+            drawable.setState(SkinCompatThemeUtils.EMPTY_STATE_SET);
+        }
+        // Now set the original state
+        drawable.setState(originalState);
+    }
+}

+ 274 - 0
skin-support/src/main/java/skin/support/content/res/SkinCompatResources.java

@@ -0,0 +1,274 @@
+package skin.support.content.res;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.AnyRes;
+import android.support.v7.app.AppCompatDelegate;
+import android.support.v7.content.res.AppCompatResources;
+import android.text.TextUtils;
+import android.util.TypedValue;
+
+import skin.support.SkinCompatManager;
+
+public class SkinCompatResources {
+    private static volatile SkinCompatResources sInstance;
+    private Resources mResources;
+    private String mSkinPkgName = "";
+    private String mSkinName = "";
+    private SkinCompatManager.SkinLoaderStrategy mStrategy;
+    private boolean isDefaultSkin = true;
+
+    private SkinCompatResources() {
+    }
+
+    public static SkinCompatResources getInstance() {
+        if (sInstance == null) {
+            synchronized (SkinCompatResources.class) {
+                if (sInstance == null) {
+                    sInstance = new SkinCompatResources();
+                }
+            }
+        }
+        return sInstance;
+    }
+
+    public void reset() {
+        reset(SkinCompatManager.getInstance().getStrategies().get(SkinCompatManager.SKIN_LOADER_STRATEGY_NONE));
+    }
+
+    public void reset(SkinCompatManager.SkinLoaderStrategy strategy) {
+        mResources = SkinCompatManager.getInstance().getContext().getResources();
+        mSkinPkgName = "";
+        mSkinName = "";
+        mStrategy = strategy;
+        isDefaultSkin = true;
+        SkinCompatUserThemeManager.get().clearCaches();
+        SkinCompatDrawableManager.get().clearCaches();
+    }
+
+    public void setupSkin(Resources resources, String pkgName, String skinName, SkinCompatManager.SkinLoaderStrategy strategy) {
+        if (resources == null || TextUtils.isEmpty(pkgName) || TextUtils.isEmpty(skinName)) {
+            reset(strategy);
+            return;
+        }
+        mResources = resources;
+        mSkinPkgName = pkgName;
+        mSkinName = skinName;
+        mStrategy = strategy;
+        isDefaultSkin = false;
+        SkinCompatUserThemeManager.get().clearCaches();
+        SkinCompatDrawableManager.get().clearCaches();
+    }
+
+    public Resources getSkinResources() {
+        return mResources;
+    }
+
+    public String getSkinPkgName() {
+        return mSkinPkgName;
+    }
+
+    public boolean isDefaultSkin() {
+        return isDefaultSkin;
+    }
+
+    @Deprecated
+    public int getColor(int resId) {
+        return getColor(SkinCompatManager.getInstance().getContext(), resId);
+    }
+
+    @Deprecated
+    public Drawable getDrawable(int resId) {
+        return getDrawable(SkinCompatManager.getInstance().getContext(), resId);
+    }
+
+    @Deprecated
+    public ColorStateList getColorStateList(int resId) {
+        return getColorStateList(SkinCompatManager.getInstance().getContext(), resId);
+    }
+
+    private int getTargetResId(Context context, int resId) {
+        try {
+            String resName = null;
+            if (mStrategy != null) {
+                resName = mStrategy.getTargetResourceEntryName(context, mSkinName, resId);
+            }
+            if (TextUtils.isEmpty(resName)) {
+                resName = context.getResources().getResourceEntryName(resId);
+            }
+            String type = context.getResources().getResourceTypeName(resId);
+            return mResources.getIdentifier(resName, type, mSkinPkgName);
+        } catch (Exception e) {
+            // 换肤失败不至于应用崩溃.
+            return 0;
+        }
+    }
+
+    private float getSkinDimension(Context context, int resId) {
+        if (!isDefaultSkin) {
+            int targetResId = getTargetResId(context, resId);
+            if (targetResId != 0) {
+                return mResources.getDimension(targetResId);
+            }
+        }
+        return context.getResources().getDimension(resId);
+    }
+    private int getSkinColor(Context context, int resId) {
+        if (!SkinCompatUserThemeManager.get().isColorEmpty()) {
+            ColorStateList colorStateList = SkinCompatUserThemeManager.get().getColorStateList(resId);
+            if (colorStateList != null) {
+                return colorStateList.getDefaultColor();
+            }
+        }
+        if (mStrategy != null) {
+            ColorStateList colorStateList = mStrategy.getColor(context, mSkinName, resId);
+            if (colorStateList != null) {
+                return colorStateList.getDefaultColor();
+            }
+        }
+        if (!isDefaultSkin) {
+            int targetResId = getTargetResId(context, resId);
+            if (targetResId != 0) {
+                return mResources.getColor(targetResId);
+            }
+        }
+        return context.getResources().getColor(resId);
+    }
+
+    private ColorStateList getSkinColorStateList(Context context, int resId) {
+        if (!SkinCompatUserThemeManager.get().isColorEmpty()) {
+            ColorStateList colorStateList = SkinCompatUserThemeManager.get().getColorStateList(resId);
+            if (colorStateList != null) {
+                return colorStateList;
+            }
+        }
+        if (mStrategy != null) {
+            ColorStateList colorStateList = mStrategy.getColorStateList(context, mSkinName, resId);
+            if (colorStateList != null) {
+                return colorStateList;
+            }
+        }
+        if (!isDefaultSkin) {
+            int targetResId = getTargetResId(context, resId);
+            if (targetResId != 0) {
+                return mResources.getColorStateList(targetResId);
+            }
+        }
+        return context.getResources().getColorStateList(resId);
+    }
+
+    private Drawable getSkinDrawable(Context context, int resId) {
+        if (!SkinCompatUserThemeManager.get().isColorEmpty()) {
+            ColorStateList colorStateList = SkinCompatUserThemeManager.get().getColorStateList(resId);
+            if (colorStateList != null) {
+                return new ColorDrawable(colorStateList.getDefaultColor());
+            }
+        }
+        if (!SkinCompatUserThemeManager.get().isDrawableEmpty()) {
+            Drawable drawable = SkinCompatUserThemeManager.get().getDrawable(resId);
+            if (drawable != null) {
+                return drawable;
+            }
+        }
+        if (mStrategy != null) {
+            Drawable drawable = mStrategy.getDrawable(context, mSkinName, resId);
+            if (drawable != null) {
+                return drawable;
+            }
+        }
+        if (!isDefaultSkin) {
+            int targetResId = getTargetResId(context, resId);
+            if (targetResId != 0) {
+                return mResources.getDrawable(targetResId);
+            }
+        }
+        return context.getResources().getDrawable(resId);
+    }
+
+    private Drawable getSkinDrawableCompat(Context context, int resId) {
+        if (AppCompatDelegate.isCompatVectorFromResourcesEnabled()) {
+            if (!isDefaultSkin) {
+                try {
+                    return SkinCompatDrawableManager.get().getDrawable(context, resId);
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+            // SkinCompatDrawableManager.get().getDrawable(context, resId) 中会调用getSkinDrawable等方法。
+            // 这里只需要拦截使用默认皮肤的情况。
+            if (!SkinCompatUserThemeManager.get().isColorEmpty()) {
+                ColorStateList colorStateList = SkinCompatUserThemeManager.get().getColorStateList(resId);
+                if (colorStateList != null) {
+                    return new ColorDrawable(colorStateList.getDefaultColor());
+                }
+            }
+            if (!SkinCompatUserThemeManager.get().isDrawableEmpty()) {
+                Drawable drawable = SkinCompatUserThemeManager.get().getDrawable(resId);
+                if (drawable != null) {
+                    return drawable;
+                }
+            }
+            if (mStrategy != null) {
+                Drawable drawable = mStrategy.getDrawable(context, mSkinName, resId);
+                if (drawable != null) {
+                    return drawable;
+                }
+            }
+            return AppCompatResources.getDrawable(context, resId);
+        } else {
+            return getSkinDrawable(context, resId);
+        }
+    }
+
+    private XmlResourceParser getSkinXml(Context context, int resId) {
+        if (!isDefaultSkin) {
+            int targetResId = getTargetResId(context, resId);
+            if (targetResId != 0) {
+                return mResources.getXml(targetResId);
+            }
+        }
+        return context.getResources().getXml(resId);
+    }
+
+    private void getSkinValue(Context context, @AnyRes int resId, TypedValue outValue, boolean resolveRefs) {
+        if (!isDefaultSkin) {
+            int targetResId = getTargetResId(context, resId);
+            if (targetResId != 0) {
+                mResources.getValue(targetResId, outValue, resolveRefs);
+                return;
+            }
+        }
+        context.getResources().getValue(resId, outValue, resolveRefs);
+    }
+
+    public static float getDimension(Context context, int resId) {
+        return getInstance().getSkinDimension(context, resId);
+    }
+    public static int getColor(Context context, int resId) {
+        return getInstance().getSkinColor(context, resId);
+    }
+
+    public static ColorStateList getColorStateList(Context context, int resId) {
+        return getInstance().getSkinColorStateList(context, resId);
+    }
+
+    public static Drawable getDrawable(Context context, int resId) {
+        return getInstance().getSkinDrawable(context, resId);
+    }
+
+    public static Drawable getDrawableCompat(Context context, int resId) {
+        return getInstance().getSkinDrawableCompat(context, resId);
+    }
+
+    public static XmlResourceParser getXml(Context context, int resId) {
+        return getInstance().getSkinXml(context, resId);
+    }
+
+    public static void getValue(Context context, @AnyRes int resId, TypedValue outValue, boolean resolveRefs) {
+        getInstance().getSkinValue(context, resId, outValue, resolveRefs);
+    }
+}

+ 141 - 0
skin-support/src/main/java/skin/support/content/res/SkinCompatThemeUtils.java

@@ -0,0 +1,141 @@
+package skin.support.content.res;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.os.Build;
+import android.support.annotation.RequiresApi;
+import android.support.v4.graphics.ColorUtils;
+import android.util.TypedValue;
+
+import static skin.support.widget.SkinCompatHelper.INVALID_ID;
+
+/**
+ * Created by ximsfei on 2017/3/25.
+ */
+
+public class SkinCompatThemeUtils {
+
+    private static final ThreadLocal<TypedValue> TL_TYPED_VALUE = new ThreadLocal<>();
+
+    static final int[] DISABLED_STATE_SET = new int[]{-android.R.attr.state_enabled};
+    static final int[] ENABLED_STATE_SET = new int[]{android.R.attr.state_enabled};
+    static final int[] WINDOW_FOCUSED_STATE_SET = new int[]{android.R.attr.state_window_focused};
+    static final int[] FOCUSED_STATE_SET = new int[]{android.R.attr.state_focused};
+    static final int[] ACTIVATED_STATE_SET = new int[]{android.R.attr.state_activated};
+    static final int[] ACCELERATED_STATE_SET = new int[]{android.R.attr.state_accelerated};
+    static final int[] HOVERED_STATE_SET = new int[]{android.R.attr.state_hovered};
+    static final int[] DRAG_CAN_ACCEPT_STATE_SET = new int[]{android.R.attr.state_drag_can_accept};
+    static final int[] DRAG_HOVERED_STATE_SET = new int[]{android.R.attr.state_drag_hovered};
+    static final int[] PRESSED_STATE_SET = new int[]{android.R.attr.state_pressed};
+    static final int[] CHECKED_STATE_SET = new int[]{android.R.attr.state_checked};
+    static final int[] SELECTED_STATE_SET = new int[]{android.R.attr.state_selected};
+    static final int[] NOT_PRESSED_OR_FOCUSED_STATE_SET = new int[]{
+            -android.R.attr.state_pressed, -android.R.attr.state_focused};
+    static final int[] EMPTY_STATE_SET = new int[0];
+
+    private static final int[] TEMP_ARRAY = new int[1];
+
+    private static final int[] APPCOMPAT_COLOR_PRIMARY_ATTRS = {
+            android.support.v7.appcompat.R.attr.colorPrimary
+    };
+    private static final int[] APPCOMPAT_COLOR_PRIMARY_DARK_ATTRS = {
+            android.support.v7.appcompat.R.attr.colorPrimaryDark
+    };
+    private static final int[] APPCOMPAT_COLOR_ACCENT_ATTRS = {
+            android.support.v7.appcompat.R.attr.colorAccent
+    };
+
+    public static int getColorPrimaryResId(Context context) {
+        return getResId(context, APPCOMPAT_COLOR_PRIMARY_ATTRS);
+    }
+
+    public static int getColorPrimaryDarkResId(Context context) {
+        return getResId(context, APPCOMPAT_COLOR_PRIMARY_DARK_ATTRS);
+    }
+
+    public static int getColorAccentResId(Context context) {
+        return getResId(context, APPCOMPAT_COLOR_ACCENT_ATTRS);
+    }
+
+    public static int getTextColorPrimaryResId(Context context) {
+        return getResId(context, new int[]{android.R.attr.textColorPrimary});
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+    public static int getStatusBarColorResId(Context context) {
+        return getResId(context, new int[]{android.R.attr.statusBarColor});
+    }
+
+    public static int getWindowBackgroundResId(Context context) {
+        return getResId(context, new int[]{android.R.attr.windowBackground});
+    }
+
+    private static int getResId(Context context, int[] attrs) {
+        TypedArray a = context.obtainStyledAttributes(attrs);
+        final int resId = a.getResourceId(0, INVALID_ID);
+        a.recycle();
+        return resId;
+    }
+
+    public static int getThemeAttrColor(Context context, int attr) {
+        TEMP_ARRAY[0] = attr;
+        TypedArray a = context.obtainStyledAttributes(null, TEMP_ARRAY);
+        try {
+            int resId = a.getResourceId(0, 0);
+            if (resId != 0) {
+                return SkinCompatResources.getColor(context, resId);
+            }
+            return 0;
+        } finally {
+            a.recycle();
+        }
+    }
+
+    public static ColorStateList getThemeAttrColorStateList(Context context, int attr) {
+        TEMP_ARRAY[0] = attr;
+        TypedArray a = context.obtainStyledAttributes(null, TEMP_ARRAY);
+        try {
+            int resId = a.getResourceId(0, 0);
+            if (resId != 0) {
+                return SkinCompatResources.getColorStateList(context, resId);
+            }
+            return null;
+        } finally {
+            a.recycle();
+        }
+    }
+
+    public static int getDisabledThemeAttrColor(Context context, int attr) {
+        final ColorStateList csl = getThemeAttrColorStateList(context, attr);
+        if (csl != null && csl.isStateful()) {
+            // If the CSL is stateful, we'll assume it has a disabled state and use it
+            return csl.getColorForState(DISABLED_STATE_SET, csl.getDefaultColor());
+        } else {
+            // Else, we'll generate the color using disabledAlpha from the theme
+
+            final TypedValue tv = getTypedValue();
+            // Now retrieve the disabledAlpha value from the theme
+            context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, tv, true);
+            final float disabledAlpha = tv.getFloat();
+
+            return getThemeAttrColor(context, attr, disabledAlpha);
+        }
+    }
+
+    private static TypedValue getTypedValue() {
+        TypedValue typedValue = TL_TYPED_VALUE.get();
+        if (typedValue == null) {
+            typedValue = new TypedValue();
+            TL_TYPED_VALUE.set(typedValue);
+        }
+        return typedValue;
+    }
+
+    static int getThemeAttrColor(Context context, int attr, float alpha) {
+        final int color = getThemeAttrColor(context, attr);
+        final int originalAlpha = Color.alpha(color);
+        return ColorUtils.setAlphaComponent(color, Math.round(originalAlpha * alpha));
+    }
+}

+ 397 - 0
skin-support/src/main/java/skin/support/content/res/SkinCompatUserThemeManager.java

@@ -0,0 +1,397 @@
+package skin.support.content.res;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.ColorRes;
+import android.support.annotation.DrawableRes;
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.WeakHashMap;
+
+import skin.support.SkinCompatManager;
+import skin.support.utils.ImageUtils;
+import skin.support.utils.SkinPreference;
+import skin.support.utils.Slog;
+
+import static skin.support.content.res.ColorState.checkColorValid;
+import static skin.support.content.res.ColorState.toJSONObject;
+
+public class SkinCompatUserThemeManager {
+    private static final String TAG = "SkinCompatUserThemeManager";
+    private static final String KEY_TYPE = "type";
+    private static final String KEY_TYPE_COLOR = "color";
+    private static final String KEY_TYPE_DRAWABLE = "drawable";
+    private static final String KEY_DRAWABLE_NAME = "drawableName";
+    private static final String KEY_DRAWABLE_PATH_AND_ANGLE = "drawablePathAndAngle";
+
+    private static SkinCompatUserThemeManager INSTANCE = new SkinCompatUserThemeManager();
+
+    private final HashMap<String, ColorState> mColorNameStateMap = new HashMap<>();
+    private final Object mColorCacheLock = new Object();
+    private final WeakHashMap<Integer, WeakReference<ColorStateList>> mColorCaches = new WeakHashMap<>();
+    private boolean mColorEmpty;
+
+    private final HashMap<String, String> mDrawablePathAndAngleMap = new HashMap<>();
+    private final Object mDrawableCacheLock = new Object();
+    private final WeakHashMap<Integer, WeakReference<Drawable>> mDrawableCaches = new WeakHashMap<>();
+    private boolean mDrawableEmpty;
+
+    private SkinCompatUserThemeManager() {
+        try {
+            startLoadFromSharedPreferences();
+        } catch (JSONException e) {
+            mColorNameStateMap.clear();
+            mDrawablePathAndAngleMap.clear();
+            if (Slog.DEBUG) {
+                Slog.i(TAG, "startLoadFromSharedPreferences error: " + e);
+            }
+        }
+    }
+
+    private void startLoadFromSharedPreferences() throws JSONException {
+        String colors = SkinPreference.getInstance().getUserTheme();
+        if (!TextUtils.isEmpty(colors)) {
+            JSONArray jsonArray = new JSONArray(colors);
+            if (Slog.DEBUG) {
+                Slog.i(TAG, "startLoadFromSharedPreferences: " + jsonArray.toString());
+            }
+            int count = jsonArray.length();
+            for (int i = 0; i < count; i++) {
+                JSONObject jsonObject = jsonArray.getJSONObject(i);
+                if (jsonObject.has(KEY_TYPE)) {
+                    String type = jsonObject.getString(KEY_TYPE);
+                    if (KEY_TYPE_COLOR.equals(type)) {
+                        ColorState state = ColorState.fromJSONObject(jsonObject);
+                        if (state != null) {
+                            mColorNameStateMap.put(state.colorName, state);
+                        }
+                    } else if (KEY_TYPE_DRAWABLE.equals(type)) {
+                        String drawableName = jsonObject.getString(KEY_DRAWABLE_NAME);
+                        String drawablePathAndAngle = jsonObject.getString(KEY_DRAWABLE_PATH_AND_ANGLE);
+                        if (!TextUtils.isEmpty(drawableName) && !TextUtils.isEmpty(drawablePathAndAngle)) {
+                            mDrawablePathAndAngleMap.put(drawableName, drawablePathAndAngle);
+                        }
+                    }
+                }
+            }
+            mColorEmpty = mColorNameStateMap.isEmpty();
+            mDrawableEmpty = mDrawablePathAndAngleMap.isEmpty();
+        }
+    }
+
+    public void apply() {
+        JSONArray jsonArray = new JSONArray();
+        for (String colorName : mColorNameStateMap.keySet()) {
+            ColorState state = mColorNameStateMap.get(colorName);
+            if (state != null) {
+                try {
+                    jsonArray.put(toJSONObject(state).putOpt(KEY_TYPE, KEY_TYPE_COLOR));
+                } catch (JSONException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        for (String drawableName : mDrawablePathAndAngleMap.keySet()) {
+            JSONObject object = new JSONObject();
+            try {
+                jsonArray.put(object.putOpt(KEY_TYPE, KEY_TYPE_DRAWABLE)
+                        .putOpt(KEY_DRAWABLE_NAME, drawableName)
+                        .putOpt(KEY_DRAWABLE_PATH_AND_ANGLE, mDrawablePathAndAngleMap.get(drawableName)));
+            } catch (JSONException e) {
+                e.printStackTrace();
+            }
+        }
+        if (Slog.DEBUG) {
+            Slog.i(TAG, "Apply user theme: " + jsonArray.toString());
+        }
+        SkinPreference.getInstance().setUserTheme(jsonArray.toString()).commitEditor();
+        SkinCompatManager.getInstance().notifyUpdateSkin();
+    }
+
+    public static SkinCompatUserThemeManager get() {
+        return INSTANCE;
+    }
+
+    public void addColorState(@ColorRes int colorRes, ColorState state) {
+        String entry = getEntryName(colorRes, KEY_TYPE_COLOR);
+        if (!TextUtils.isEmpty(entry) && state != null) {
+            state.colorName = entry;
+            mColorNameStateMap.put(entry, state);
+            removeColorInCache(colorRes);
+            mColorEmpty = false;
+        }
+    }
+
+    public void addColorState(@ColorRes int colorRes, String colorDefault) {
+        if (!checkColorValid("colorDefault", colorDefault)) {
+            return;
+        }
+        String entry = getEntryName(colorRes, KEY_TYPE_COLOR);
+        if (!TextUtils.isEmpty(entry)) {
+            mColorNameStateMap.put(entry, new ColorState(entry, colorDefault));
+            removeColorInCache(colorRes);
+            mColorEmpty = false;
+        }
+    }
+
+    public void removeColorState(@ColorRes int colorRes) {
+        String entry = getEntryName(colorRes, KEY_TYPE_COLOR);
+        if (!TextUtils.isEmpty(entry)) {
+            mColorNameStateMap.remove(entry);
+            removeColorInCache(colorRes);
+            mColorEmpty = mColorNameStateMap.isEmpty();
+        }
+    }
+
+    void removeColorState(String colorName) {
+        if (!TextUtils.isEmpty(colorName)) {
+            mColorNameStateMap.remove(colorName);
+            mColorEmpty = mColorNameStateMap.isEmpty();
+        }
+    }
+
+    public ColorState getColorState(String colorName) {
+        return mColorNameStateMap.get(colorName);
+    }
+
+    public ColorState getColorState(@ColorRes int colorRes) {
+        String entry = getEntryName(colorRes, KEY_TYPE_COLOR);
+        if (!TextUtils.isEmpty(entry)) {
+            return mColorNameStateMap.get(entry);
+        }
+        return null;
+    }
+
+    public ColorStateList getColorStateList(@ColorRes int colorRes) {
+        ColorStateList colorStateList = getCachedColor(colorRes);
+        if (colorStateList == null) {
+            String entry = getEntryName(colorRes, KEY_TYPE_COLOR);
+            if (!TextUtils.isEmpty(entry)) {
+                ColorState state = mColorNameStateMap.get(entry);
+                if (state != null) {
+                    colorStateList = state.parse();
+                    if (colorStateList != null) {
+                        addColorToCache(colorRes, colorStateList);
+                    }
+                }
+            }
+        }
+        return colorStateList;
+    }
+
+    public void addDrawablePath(@DrawableRes int drawableRes, String drawablePath) {
+        if (!checkPathValid(drawablePath)) {
+            return;
+        }
+        String entry = getEntryName(drawableRes, KEY_TYPE_DRAWABLE);
+        if (!TextUtils.isEmpty(entry)) {
+            int angle = ImageUtils.getImageRotateAngle(drawablePath);
+            String drawablePathAndAngle = drawablePath + ":" + String.valueOf(angle);
+            mDrawablePathAndAngleMap.put(entry, drawablePathAndAngle);
+            removeDrawableInCache(drawableRes);
+            mDrawableEmpty = false;
+        }
+    }
+
+    public void addDrawablePath(@DrawableRes int drawableRes, String drawablePath, int angle) {
+        if (!checkPathValid(drawablePath)) {
+            return;
+        }
+        String entry = getEntryName(drawableRes, KEY_TYPE_DRAWABLE);
+        if (!TextUtils.isEmpty(entry)) {
+            String drawablePathAndAngle = drawablePath + ":" + String.valueOf(angle);
+            mDrawablePathAndAngleMap.put(entry, drawablePathAndAngle);
+            removeDrawableInCache(drawableRes);
+            mDrawableEmpty = false;
+        }
+    }
+
+    public void removeDrawablePath(@DrawableRes int drawableRes) {
+        String entry = getEntryName(drawableRes, KEY_TYPE_DRAWABLE);
+        if (!TextUtils.isEmpty(entry)) {
+            mDrawablePathAndAngleMap.remove(entry);
+            removeDrawableInCache(drawableRes);
+            mDrawableEmpty = mDrawablePathAndAngleMap.isEmpty();
+        }
+    }
+
+    public String getDrawablePath(String drawableName) {
+        String drawablePathAndAngle = mDrawablePathAndAngleMap.get(drawableName);
+        if (!TextUtils.isEmpty(drawablePathAndAngle)) {
+            String[] splits = drawablePathAndAngle.split(":");
+            return splits[0];
+        }
+        return "";
+    }
+
+    public int getDrawableAngle(String drawableName) {
+        String drawablePathAndAngle = mDrawablePathAndAngleMap.get(drawableName);
+        if (!TextUtils.isEmpty(drawablePathAndAngle)) {
+            String[] splits = drawablePathAndAngle.split(":");
+            if (splits.length == 2) {
+                return Integer.valueOf(splits[1]);
+            }
+        }
+        return 0;
+    }
+
+    public Drawable getDrawable(@DrawableRes int drawableRes) {
+        Drawable drawable = getCachedDrawable(drawableRes);
+        if (drawable == null) {
+            String entry = getEntryName(drawableRes, KEY_TYPE_DRAWABLE);
+            if (!TextUtils.isEmpty(entry)) {
+                String drawablePathAndAngle = mDrawablePathAndAngleMap.get(entry);
+                if (!TextUtils.isEmpty(drawablePathAndAngle)) {
+                    String[] splits = drawablePathAndAngle.split(":");
+                    String path = splits[0];
+                    int angle = 0;
+                    if (splits.length == 2) {
+                        angle = Integer.valueOf(splits[1]);
+                    }
+                    if (checkPathValid(path)) {
+                        if (angle == 0) {
+                            drawable = Drawable.createFromPath(path);
+                        } else {
+                            Matrix m = new Matrix();
+                            m.postRotate(angle);
+                            Bitmap bitmap = BitmapFactory.decodeFile(path);
+                            bitmap = Bitmap.createBitmap(bitmap, 0, 0,
+                                    bitmap.getWidth(), bitmap.getHeight(), m, true);
+                            drawable = new BitmapDrawable(null, bitmap);
+                        }
+                        if (drawable != null) {
+                            addDrawableToCache(drawableRes, drawable);
+                        }
+                    }
+                }
+            }
+        }
+        return drawable;
+    }
+
+    public void clearColors() {
+        mColorNameStateMap.clear();
+        clearColorCaches();
+        mColorEmpty = true;
+        apply();
+    }
+
+    public void clearDrawables() {
+        mDrawablePathAndAngleMap.clear();
+        clearDrawableCaches();
+        mDrawableEmpty = true;
+        apply();
+    }
+
+    boolean isColorEmpty() {
+        return mColorEmpty;
+    }
+
+    boolean isDrawableEmpty() {
+        return mDrawableEmpty;
+    }
+
+    void clearCaches() {
+        clearColorCaches();
+        clearDrawableCaches();
+    }
+
+    private void clearColorCaches() {
+        synchronized (mColorCacheLock) {
+            mColorCaches.clear();
+        }
+    }
+
+    private void clearDrawableCaches() {
+        synchronized (mDrawableCacheLock) {
+            mDrawableCaches.clear();
+        }
+    }
+
+    private ColorStateList getCachedColor(@ColorRes int colorRes) {
+        synchronized (mColorCacheLock) {
+            WeakReference<ColorStateList> colorRef = mColorCaches.get(colorRes);
+            if (colorRef != null) {
+                ColorStateList colorStateList = colorRef.get();
+                if (colorStateList != null) {
+                    return colorStateList;
+                } else {
+                    mColorCaches.remove(colorRes);
+                }
+            }
+        }
+        return null;
+    }
+
+    private void addColorToCache(@ColorRes int colorRes, ColorStateList colorStateList) {
+        if (colorStateList != null) {
+            synchronized (mColorCacheLock) {
+                mColorCaches.put(colorRes, new WeakReference<>(colorStateList));
+            }
+        }
+    }
+
+    private void removeColorInCache(@ColorRes int colorRes) {
+        synchronized (mColorCacheLock) {
+            mColorCaches.remove(colorRes);
+        }
+    }
+
+    private Drawable getCachedDrawable(@DrawableRes int drawableRes) {
+        synchronized (mDrawableCacheLock) {
+            WeakReference<Drawable> drawableRef = mDrawableCaches.get(drawableRes);
+            if (drawableRef != null) {
+                Drawable drawable = drawableRef.get();
+                if (drawable != null) {
+                    return drawable;
+                } else {
+                    mDrawableCaches.remove(drawableRes);
+                }
+            }
+        }
+        return null;
+    }
+
+    private void addDrawableToCache(@DrawableRes int drawableRes, Drawable drawable) {
+        if (drawable != null) {
+            synchronized (mDrawableCacheLock) {
+                mDrawableCaches.put(drawableRes, new WeakReference<>(drawable));
+            }
+        }
+    }
+
+    private void removeDrawableInCache(@DrawableRes int drawableRes) {
+        synchronized (mDrawableCacheLock) {
+            mDrawableCaches.remove(drawableRes);
+        }
+    }
+
+    private String getEntryName(int resId, String entryType) {
+        Context context = SkinCompatManager.getInstance().getContext();
+        String type = context.getResources().getResourceTypeName(resId);
+        if (entryType.equalsIgnoreCase(type)) {
+            return context.getResources().getResourceEntryName(resId);
+        }
+        return null;
+    }
+
+    private static boolean checkPathValid(String path) {
+        boolean valid = !TextUtils.isEmpty(path) && new File(path).exists();
+        if (Slog.DEBUG && !valid) {
+            Slog.i(TAG, "Invalid drawable path : " + path);
+        }
+        return valid;
+    }
+}

+ 7 - 0
skin-support/src/main/java/skin/support/exception/SkinCompatException.java

@@ -0,0 +1,7 @@
+package skin.support.exception;
+
+public class SkinCompatException extends RuntimeException {
+    public SkinCompatException(String message) {
+        super(message);
+    }
+}

+ 49 - 0
skin-support/src/main/java/skin/support/load/SkinAssetsLoader.java

@@ -0,0 +1,49 @@
+package skin.support.load;
+
+import android.content.Context;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import skin.support.SkinCompatManager;
+import skin.support.utils.SkinConstants;
+import skin.support.utils.SkinFileUtils;
+
+public class SkinAssetsLoader extends SkinSDCardLoader {
+    @Override
+    protected String getSkinPath(Context context, String skinName) {
+        return copySkinFromAssets(context, skinName);
+    }
+
+    @Override
+    public String getTargetResourceEntryName(Context context, String skinName, int resId) {
+        return null;
+    }
+
+    @Override
+    public int getType() {
+        return SkinCompatManager.SKIN_LOADER_STRATEGY_ASSETS;
+    }
+
+    private String copySkinFromAssets(Context context, String name) {
+        String skinPath = new File(SkinFileUtils.getSkinDir(context), name).getAbsolutePath();
+        try {
+            InputStream is = context.getAssets().open(
+                    SkinConstants.SKIN_DEPLOY_PATH + File.separator + name);
+            OutputStream os = new FileOutputStream(skinPath);
+            int byteCount;
+            byte[] bytes = new byte[1024];
+            while ((byteCount = is.read(bytes)) != -1) {
+                os.write(bytes, 0, byteCount);
+            }
+            os.close();
+            is.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return skinPath;
+    }
+}

+ 46 - 0
skin-support/src/main/java/skin/support/load/SkinBuildInLoader.java

@@ -0,0 +1,46 @@
+package skin.support.load;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.drawable.Drawable;
+
+import skin.support.SkinCompatManager;
+import skin.support.SkinCompatManager.SkinLoaderStrategy;
+import skin.support.content.res.SkinCompatResources;
+
+public class SkinBuildInLoader implements SkinLoaderStrategy {
+    @Override
+    public String loadSkinInBackground(Context context, String skinName) {
+        SkinCompatResources.getInstance().setupSkin(
+                context.getResources(),
+                context.getPackageName(),
+                skinName,
+                this);
+        return skinName;
+    }
+
+    @Override
+    public String getTargetResourceEntryName(Context context, String skinName, int resId) {
+        return context.getResources().getResourceEntryName(resId) + "_" + skinName;
+    }
+
+    @Override
+    public ColorStateList getColor(Context context, String skinName, int resId) {
+        return null;
+    }
+
+    @Override
+    public ColorStateList getColorStateList(Context context, String skinName, int resId) {
+        return null;
+    }
+
+    @Override
+    public Drawable getDrawable(Context context, String skinName, int resId) {
+        return null;
+    }
+
+    @Override
+    public int getType() {
+        return SkinCompatManager.SKIN_LOADER_STRATEGY_BUILD_IN;
+    }
+}

+ 40 - 0
skin-support/src/main/java/skin/support/load/SkinNoneLoader.java

@@ -0,0 +1,40 @@
+package skin.support.load;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.drawable.Drawable;
+
+import skin.support.SkinCompatManager;
+import skin.support.SkinCompatManager.SkinLoaderStrategy;
+
+public class SkinNoneLoader implements SkinLoaderStrategy {
+    @Override
+    public String loadSkinInBackground(Context context, String skinName) {
+        return "";
+    }
+
+    @Override
+    public String getTargetResourceEntryName(Context context, String skinName, int resId) {
+        return "";
+    }
+
+    @Override
+    public ColorStateList getColor(Context context, String skinName, int resId) {
+        return null;
+    }
+
+    @Override
+    public ColorStateList getColorStateList(Context context, String skinName, int resId) {
+        return null;
+    }
+
+    @Override
+    public Drawable getDrawable(Context context, String skinName, int resId) {
+        return null;
+    }
+
+    @Override
+    public int getType() {
+        return SkinCompatManager.SKIN_LOADER_STRATEGY_NONE;
+    }
+}

+ 46 - 0
skin-support/src/main/java/skin/support/load/SkinPrefixBuildInLoader.java

@@ -0,0 +1,46 @@
+package skin.support.load;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.drawable.Drawable;
+
+import skin.support.SkinCompatManager;
+import skin.support.SkinCompatManager.SkinLoaderStrategy;
+import skin.support.content.res.SkinCompatResources;
+
+public class SkinPrefixBuildInLoader implements SkinLoaderStrategy {
+    @Override
+    public String loadSkinInBackground(Context context, String skinName) {
+        SkinCompatResources.getInstance().setupSkin(
+                context.getResources(),
+                context.getPackageName(),
+                skinName,
+                this);
+        return skinName;
+    }
+
+    @Override
+    public String getTargetResourceEntryName(Context context, String skinName, int resId) {
+        return skinName + "_" + context.getResources().getResourceEntryName(resId);
+    }
+
+    @Override
+    public ColorStateList getColor(Context context, String skinName, int resId) {
+        return null;
+    }
+
+    @Override
+    public ColorStateList getColorStateList(Context context, String skinName, int resId) {
+        return null;
+    }
+
+    @Override
+    public Drawable getDrawable(Context context, String skinName, int resId) {
+        return null;
+    }
+
+    @Override
+    public int getType() {
+        return SkinCompatManager.SKIN_LOADER_STRATEGY_PREFIX_BUILD_IN;
+    }
+}

+ 57 - 0
skin-support/src/main/java/skin/support/load/SkinSDCardLoader.java

@@ -0,0 +1,57 @@
+package skin.support.load;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+
+import skin.support.SkinCompatManager;
+import skin.support.SkinCompatManager.SkinLoaderStrategy;
+import skin.support.content.res.SkinCompatResources;
+import skin.support.utils.SkinFileUtils;
+
+public abstract class SkinSDCardLoader implements SkinLoaderStrategy {
+    @Override
+    public String loadSkinInBackground(Context context, String skinName) {
+        if (TextUtils.isEmpty(skinName)) {
+            return skinName;
+        }
+        String skinPkgPath = getSkinPath(context, skinName);
+        if (SkinFileUtils.isFileExists(skinPkgPath)) {
+            String pkgName = SkinCompatManager.getInstance().getSkinPackageName(skinPkgPath);
+            Resources resources = SkinCompatManager.getInstance().getSkinResources(skinPkgPath);
+            if (resources != null && !TextUtils.isEmpty(pkgName)) {
+                SkinCompatResources.getInstance().setupSkin(
+                        resources,
+                        pkgName,
+                        skinName,
+                        this);
+                return skinName;
+            }
+        }
+        return null;
+    }
+
+    protected abstract String getSkinPath(Context context, String skinName);
+
+    @Override
+    public String getTargetResourceEntryName(Context context, String skinName, int resId) {
+        return null;
+    }
+
+    @Override
+    public ColorStateList getColor(Context context, String skinName, int resId) {
+        return null;
+    }
+
+    @Override
+    public ColorStateList getColorStateList(Context context, String skinName, int resId) {
+        return null;
+    }
+
+    @Override
+    public Drawable getDrawable(Context context, String skinName, int resId) {
+        return null;
+    }
+}

+ 50 - 0
skin-support/src/main/java/skin/support/observe/SkinObservable.java

@@ -0,0 +1,50 @@
+package skin.support.observe;
+
+import java.util.ArrayList;
+
+/**
+ * Created by ximsfei on 2017/1/10.
+ */
+
+public class SkinObservable {
+    private final ArrayList<SkinObserver> observers;
+
+    public SkinObservable() {
+        observers = new ArrayList<>();
+    }
+
+    public synchronized void addObserver(SkinObserver o) {
+        if (o == null)
+            throw new NullPointerException();
+        if (!observers.contains(o)) {
+            observers.add(o);
+        }
+    }
+
+    public synchronized void deleteObserver(SkinObserver o) {
+        observers.remove(o);
+    }
+
+    public void notifyUpdateSkin() {
+        notifyUpdateSkin(null);
+    }
+
+    public void notifyUpdateSkin(Object arg) {
+        SkinObserver[] arrLocal;
+
+        synchronized (this) {
+            arrLocal = observers.toArray(new SkinObserver[observers.size()]);
+        }
+
+        for (int i = arrLocal.length-1; i>=0; i--)
+            arrLocal[i].updateSkin(this, arg);
+    }
+
+    public synchronized void deleteObservers() {
+        observers.clear();
+    }
+
+    public synchronized int countObservers() {
+        return observers.size();
+    }
+}

+ 9 - 0
skin-support/src/main/java/skin/support/observe/SkinObserver.java

@@ -0,0 +1,9 @@
+package skin.support.observe;
+
+/**
+ * Created by ximsfei on 2017/1/10.
+ */
+
+public interface SkinObserver {
+    void updateSkin(SkinObservable observable, Object o);
+}

+ 37 - 0
skin-support/src/main/java/skin/support/utils/ImageUtils.java

@@ -0,0 +1,37 @@
+package skin.support.utils;
+
+import android.media.ExifInterface;
+
+import java.io.IOException;
+
+public class ImageUtils {
+    public static int getImageRotateAngle(String filePath) {
+        ExifInterface exif;
+        try {
+            exif = new ExifInterface(filePath);
+        } catch (IOException e) {
+            e.printStackTrace();
+            exif = null;
+
+        }
+        int angle = 0;
+        if (exif != null) {
+            int ori = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
+            switch (ori) {
+                case ExifInterface.ORIENTATION_ROTATE_90:
+                    angle = 90;
+                    break;
+                case ExifInterface.ORIENTATION_ROTATE_180:
+                    angle = 180;
+                    break;
+                case ExifInterface.ORIENTATION_ROTATE_270:
+                    angle = 270;
+                    break;
+                default:
+                    angle = 0;
+                    break;
+            }
+        }
+        return angle;
+    }
+}

+ 151 - 0
skin-support/src/main/java/skin/support/utils/SkinCompatVersionUtils.java

@@ -0,0 +1,151 @@
+package skin.support.utils;
+
+import android.graphics.drawable.Drawable;
+
+import java.lang.reflect.Method;
+
+public final class SkinCompatVersionUtils {
+    private static final String TAG = "SkinCompatUtils";
+    // 27.1.0后删除
+    private static Class<?> sV4DrawableWrapperClass;
+    private static Method sV4DrawableWrapperGetM;
+    private static Method sV4DrawableWrapperSetM;
+    // 27.1.0后增加
+    private static Class<?> sV4WrappedDrawableClass;
+    private static Method sV4WrappedDrawableGetM;
+    private static Method sV4WrappedDrawableSetM;
+
+    static {
+        try {
+            sV4WrappedDrawableClass = Class.forName("android.support.v4.graphics.drawable.WrappedDrawable");
+        } catch (ClassNotFoundException e) {
+            if (Slog.DEBUG) {
+                Slog.i(TAG, "hasWrappedDrawable = false");
+            }
+        }
+        try {
+            sV4DrawableWrapperClass = Class.forName("android.support.v4.graphics.drawable.DrawableWrapper");
+        } catch (ClassNotFoundException e) {
+            if (Slog.DEBUG) {
+                Slog.i(TAG, "hasDrawableWrapper = false");
+            }
+        }
+    }
+
+    public static boolean hasV4WrappedDrawable() {
+        return sV4WrappedDrawableClass != null;
+    }
+
+    public static boolean isV4WrappedDrawable(Drawable drawable) {
+        return sV4WrappedDrawableClass != null
+                && sV4WrappedDrawableClass.isAssignableFrom(drawable.getClass());
+    }
+
+    public static Drawable getV4WrappedDrawableWrappedDrawable(Drawable drawable) {
+        if (sV4WrappedDrawableClass != null) {
+            if (sV4WrappedDrawableGetM == null) {
+                try {
+                    sV4WrappedDrawableGetM = sV4WrappedDrawableClass.getDeclaredMethod("getWrappedDrawable");
+                    sV4WrappedDrawableGetM.setAccessible(true);
+                } catch (Exception e) {
+                    if (Slog.DEBUG) {
+                        Slog.i(TAG, "getV4WrappedDrawableWrappedDrawable No Such Method");
+                    }
+                }
+            }
+            if (sV4WrappedDrawableGetM != null) {
+                try {
+                    return (Drawable) sV4WrappedDrawableGetM.invoke(drawable);
+                } catch (Exception e) {
+                    if (Slog.DEBUG) {
+                        Slog.i(TAG, "getV4WrappedDrawableWrappedDrawable invoke error: " + e);
+                    }
+                }
+            }
+        }
+        return drawable;
+    }
+
+    public static void setV4WrappedDrawableWrappedDrawable(Drawable drawable, Drawable inner) {
+        if (sV4WrappedDrawableClass != null) {
+            if (sV4WrappedDrawableSetM == null) {
+                try {
+                    sV4WrappedDrawableSetM = sV4WrappedDrawableClass.getDeclaredMethod("setWrappedDrawable", Drawable.class);
+                    sV4WrappedDrawableSetM.setAccessible(true);
+                } catch (Exception e) {
+                    if (Slog.DEBUG) {
+                        Slog.i(TAG, "setV4WrappedDrawableWrappedDrawable No Such Method");
+                    }
+                }
+            }
+            if (sV4WrappedDrawableSetM != null) {
+                try {
+                    sV4WrappedDrawableSetM.invoke(drawable, inner);
+                } catch (Exception e) {
+                    if (Slog.DEBUG) {
+                        Slog.i(TAG, "setV4WrappedDrawableWrappedDrawable invoke error: " + e);
+                    }
+                }
+            }
+        }
+    }
+
+    public static boolean hasV4DrawableWrapper() {
+        return sV4DrawableWrapperClass != null;
+    }
+
+    public static boolean isV4DrawableWrapper(Drawable drawable) {
+        return sV4DrawableWrapperClass != null
+                && sV4DrawableWrapperClass.isAssignableFrom(drawable.getClass());
+    }
+
+    public static Drawable getV4DrawableWrapperWrappedDrawable(Drawable drawable) {
+        if (sV4DrawableWrapperClass != null) {
+            if (sV4DrawableWrapperGetM == null) {
+                try {
+                    sV4DrawableWrapperGetM = sV4DrawableWrapperClass.getDeclaredMethod("getWrappedDrawable");
+                    sV4DrawableWrapperGetM.setAccessible(true);
+                } catch (Exception e) {
+                    if (Slog.DEBUG) {
+                        Slog.i(TAG, "getV4DrawableWrapperWrappedDrawable No Such Method");
+                    }
+                }
+            }
+            if (sV4DrawableWrapperGetM != null) {
+                try {
+                    return (Drawable) sV4DrawableWrapperGetM.invoke(drawable);
+                } catch (Exception e) {
+                    if (Slog.DEBUG) {
+                        Slog.i(TAG, "getV4DrawableWrapperWrappedDrawable invoke error: " + e);
+                    }
+                }
+            }
+        }
+        return drawable;
+    }
+
+    public static void setV4DrawableWrapperWrappedDrawable(Drawable drawable, Drawable inner) {
+        if (sV4DrawableWrapperClass != null) {
+            if (sV4DrawableWrapperSetM == null) {
+                try {
+                    sV4DrawableWrapperSetM = sV4DrawableWrapperClass.getDeclaredMethod("setWrappedDrawable", Drawable.class);
+                    sV4DrawableWrapperSetM.setAccessible(true);
+                } catch (Exception e) {
+                    if (Slog.DEBUG) {
+                        Slog.i(TAG, "setV4DrawableWrapperWrappedDrawable No Such Method");
+                    }
+                }
+            }
+            if (sV4DrawableWrapperSetM != null) {
+                try {
+                    sV4DrawableWrapperSetM.invoke(drawable, inner);
+                } catch (Exception e) {
+                    if (Slog.DEBUG) {
+                        Slog.i(TAG, "setV4DrawableWrapperWrappedDrawable invoke error: " + e);
+                    }
+                }
+            }
+        }
+    }
+
+}

+ 9 - 0
skin-support/src/main/java/skin/support/utils/SkinConstants.java

@@ -0,0 +1,9 @@
+package skin.support.utils;
+
+/**
+ * Created by ximsfei on 17-1-9.
+ */
+
+public class SkinConstants {
+    public static final String SKIN_DEPLOY_PATH = "skins";
+}

+ 37 - 0
skin-support/src/main/java/skin/support/utils/SkinFileUtils.java

@@ -0,0 +1,37 @@
+package skin.support.utils;
+
+import android.content.Context;
+import android.os.Environment;
+import android.text.TextUtils;
+
+import java.io.File;
+
+/**
+ * Created by ximsfei on 17-1-10.
+ */
+
+public class SkinFileUtils {
+    public static String getSkinDir(Context context) {
+        File skinDir = new File(getCacheDir(context), SkinConstants.SKIN_DEPLOY_PATH);
+        if (!skinDir.exists()) {
+            skinDir.mkdirs();
+        }
+        return skinDir.getAbsolutePath();
+    }
+
+    private static String getCacheDir(Context context) {
+        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+            File cacheDir = context.getExternalCacheDir();
+            if (cacheDir != null && (cacheDir.exists() || cacheDir.mkdirs())) {
+                return cacheDir.getAbsolutePath();
+            }
+        }
+
+        return context.getCacheDir().getAbsolutePath();
+    }
+
+
+    public static boolean isFileExists(String path) {
+        return !TextUtils.isEmpty(path) && new File(path).exists();
+    }
+}

+ 73 - 0
skin-support/src/main/java/skin/support/utils/SkinPreference.java

@@ -0,0 +1,73 @@
+package skin.support.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import skin.support.SkinCompatManager;
+
+/**
+ * Created by ximsfei on 2017/1/10.
+ */
+
+public class SkinPreference {
+    private static final String FILE_NAME = "meta-data";
+
+    private static final String KEY_SKIN_NAME = "skin-name";
+    private static final String KEY_SKIN_STRATEGY = "skin-strategy";
+    private static final String KEY_SKIN_USER_THEME = "skin-user-theme-json";
+    private static SkinPreference sInstance;
+    private final Context mApp;
+    private final SharedPreferences mPref;
+    private final SharedPreferences.Editor mEditor;
+
+    public static void init(Context context) {
+        if (sInstance == null) {
+            synchronized (SkinPreference.class) {
+                if (sInstance == null) {
+                    sInstance = new SkinPreference(context.getApplicationContext());
+                }
+            }
+        }
+    }
+
+    public static SkinPreference getInstance() {
+        return sInstance;
+    }
+
+    private SkinPreference(Context applicationContext) {
+        mApp = applicationContext;
+        mPref = mApp.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE);
+        mEditor = mPref.edit();
+    }
+
+    public SkinPreference setSkinName(String skinName) {
+        mEditor.putString(KEY_SKIN_NAME, skinName);
+        return this;
+    }
+
+    public String getSkinName() {
+        return mPref.getString(KEY_SKIN_NAME, "");
+    }
+
+    public SkinPreference setSkinStrategy(int strategy) {
+        mEditor.putInt(KEY_SKIN_STRATEGY, strategy);
+        return this;
+    }
+
+    public int getSkinStrategy() {
+        return mPref.getInt(KEY_SKIN_STRATEGY, SkinCompatManager.SKIN_LOADER_STRATEGY_NONE);
+    }
+
+    public SkinPreference setUserTheme(String themeJson) {
+        mEditor.putString(KEY_SKIN_USER_THEME, themeJson);
+        return this;
+    }
+
+    public String getUserTheme() {
+        return mPref.getString(KEY_SKIN_USER_THEME, "");
+    }
+
+    public void commitEditor() {
+        mEditor.apply();
+    }
+}

+ 28 - 0
skin-support/src/main/java/skin/support/utils/Slog.java

@@ -0,0 +1,28 @@
+package skin.support.utils;
+
+import android.util.Log;
+
+public class Slog {
+    public static boolean DEBUG = false;
+    private static final String TAG = "skin-support";
+
+    public static void i(String msg) {
+        if (DEBUG) {
+            Log.i(TAG, msg);
+        }
+    }
+
+    public static void i(String subtag, String msg) {
+        if (DEBUG) {
+            Log.i(TAG, subtag + ": " + msg);
+        }
+    }
+
+    public static void r(String msg) {
+        Log.i(TAG, msg);
+    }
+
+    public static void r(String subtag, String msg) {
+        Log.i(TAG, subtag + ": " + msg);
+    }
+}

+ 116 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatAutoCompleteTextView.java

@@ -0,0 +1,116 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.DrawableRes;
+import android.support.v7.widget.AppCompatAutoCompleteTextView;
+import android.util.AttributeSet;
+
+import skin.support.R;
+import skin.support.content.res.SkinCompatResources;
+
+import static skin.support.widget.SkinCompatHelper.INVALID_ID;
+
+/**
+ * Created by ximsfei on 2017/1/13.
+ */
+
+public class SkinCompatAutoCompleteTextView extends AppCompatAutoCompleteTextView implements SkinCompatSupportable {
+    private static final int[] TINT_ATTRS = {
+            android.R.attr.popupBackground
+    };
+    private int mDropDownBackgroundResId = INVALID_ID;
+    private SkinCompatTextHelper mTextHelper;
+    private SkinCompatBackgroundHelper mBackgroundTintHelper;
+
+    public SkinCompatAutoCompleteTextView(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatAutoCompleteTextView(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.autoCompleteTextViewStyle);
+    }
+
+    public SkinCompatAutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        TypedArray a = context.obtainStyledAttributes(attrs, TINT_ATTRS, defStyleAttr, 0);
+        if (a.hasValue(0)) {
+            mDropDownBackgroundResId = a.getResourceId(0, INVALID_ID);
+        }
+        a.recycle();
+        applyDropDownBackgroundResource();
+        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
+        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
+        mTextHelper = SkinCompatTextHelper.create(this);
+        mTextHelper.loadFromAttributes(attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setDropDownBackgroundResource(@DrawableRes int resId) {
+        super.setDropDownBackgroundResource(resId);
+        mDropDownBackgroundResId = resId;
+        applyDropDownBackgroundResource();
+    }
+
+    private void applyDropDownBackgroundResource() {
+        mDropDownBackgroundResId = SkinCompatHelper.checkResourceId(mDropDownBackgroundResId);
+        if (mDropDownBackgroundResId != INVALID_ID) {
+            Drawable drawable = SkinCompatResources.getDrawableCompat(getContext(), mDropDownBackgroundResId);
+            if (drawable != null) {
+                setDropDownBackgroundDrawable(drawable);
+            }
+        }
+    }
+
+    @Override
+    public void setBackgroundResource(@DrawableRes int resId) {
+        super.setBackgroundResource(resId);
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.onSetBackgroundResource(resId);
+        }
+    }
+
+    @Override
+    public void setTextAppearance(int resId) {
+        setTextAppearance(getContext(), resId);
+    }
+
+    @Override
+    public void setTextAppearance(Context context, int resId) {
+        super.setTextAppearance(context, resId);
+        if (mTextHelper != null) {
+            mTextHelper.onSetTextAppearance(context, resId);
+        }
+    }
+
+    @Override
+    public void setCompoundDrawablesRelativeWithIntrinsicBounds(
+            @DrawableRes int start, @DrawableRes int top, @DrawableRes int end, @DrawableRes int bottom) {
+        super.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
+        if (mTextHelper != null) {
+            mTextHelper.onSetCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
+        }
+    }
+
+    @Override
+    public void setCompoundDrawablesWithIntrinsicBounds(
+            @DrawableRes int left, @DrawableRes int top, @DrawableRes int right, @DrawableRes int bottom) {
+        super.setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
+        if (mTextHelper != null) {
+            mTextHelper.onSetCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
+        }
+    }
+
+    @Override
+    public void applySkin() {
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.applySkin();
+        }
+        if (mTextHelper != null) {
+            mTextHelper.applySkin();
+        }
+        applyDropDownBackgroundResource();
+    }
+
+}

+ 59 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatBackgroundHelper.java

@@ -0,0 +1,59 @@
+package skin.support.widget;
+
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.support.v4.view.ViewCompat;
+import android.util.AttributeSet;
+import android.view.View;
+
+import skin.support.R;
+import skin.support.content.res.SkinCompatResources;
+
+/**
+ * Created by ximsfei on 2017/1/10.
+ */
+
+public class SkinCompatBackgroundHelper extends SkinCompatHelper {
+    private final View mView;
+
+    private int mBackgroundResId = INVALID_ID;
+
+    public SkinCompatBackgroundHelper(View view) {
+        mView = view;
+    }
+
+    public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
+        TypedArray a = mView.getContext().obtainStyledAttributes(attrs, R.styleable.SkinBackgroundHelper, defStyleAttr, 0);
+        try {
+            if (a.hasValue(R.styleable.SkinBackgroundHelper_android_background)) {
+                mBackgroundResId = a.getResourceId(
+                        R.styleable.SkinBackgroundHelper_android_background, INVALID_ID);
+            }
+        } finally {
+            a.recycle();
+        }
+        applySkin();
+    }
+
+    public void onSetBackgroundResource(int resId) {
+        mBackgroundResId = resId;
+        // Update the default background tint
+        applySkin();
+    }
+
+    public void applySkin() {
+        mBackgroundResId = checkResourceId(mBackgroundResId);
+        if (mBackgroundResId == INVALID_ID) {
+            return;
+        }
+        Drawable drawable = SkinCompatResources.getDrawableCompat(mView.getContext(), mBackgroundResId);
+        if (drawable != null) {
+            int paddingLeft = mView.getPaddingLeft();
+            int paddingTop = mView.getPaddingTop();
+            int paddingRight = mView.getPaddingRight();
+            int paddingBottom = mView.getPaddingBottom();
+            ViewCompat.setBackground(mView, drawable);
+            mView.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
+        }
+    }
+}

+ 81 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatButton.java

@@ -0,0 +1,81 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.support.annotation.DrawableRes;
+import android.support.v7.appcompat.R;
+import android.support.v7.widget.AppCompatButton;
+import android.util.AttributeSet;
+
+/**
+ * Created by ximsfei on 17-1-11.
+ */
+public class SkinCompatButton extends AppCompatButton implements SkinCompatSupportable {
+    private SkinCompatTextHelper mTextHelper;
+    private SkinCompatBackgroundHelper mBackgroundTintHelper;
+
+    public SkinCompatButton(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatButton(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.buttonStyle);
+    }
+
+    public SkinCompatButton(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
+        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
+        mTextHelper = SkinCompatTextHelper.create(this);
+        mTextHelper.loadFromAttributes(attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setBackgroundResource(@DrawableRes int resId) {
+        super.setBackgroundResource(resId);
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.onSetBackgroundResource(resId);
+        }
+    }
+
+    @Override
+    public void setTextAppearance(int resId) {
+        setTextAppearance(getContext(), resId);
+    }
+
+    @Override
+    public void setTextAppearance(Context context, int resId) {
+        super.setTextAppearance(context, resId);
+        if (mTextHelper != null) {
+            mTextHelper.onSetTextAppearance(context, resId);
+        }
+    }
+
+    @Override
+    public void setCompoundDrawablesRelativeWithIntrinsicBounds(
+            @DrawableRes int start, @DrawableRes int top, @DrawableRes int end, @DrawableRes int bottom) {
+        super.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
+        if (mTextHelper != null) {
+            mTextHelper.onSetCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
+        }
+    }
+
+    @Override
+    public void setCompoundDrawablesWithIntrinsicBounds(
+            @DrawableRes int left, @DrawableRes int top, @DrawableRes int right, @DrawableRes int bottom) {
+        super.setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
+        if (mTextHelper != null) {
+            mTextHelper.onSetCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
+        }
+    }
+
+    @Override
+    public void applySkin() {
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.applySkin();
+        }
+        if (mTextHelper != null) {
+            mTextHelper.applySkin();
+        }
+    }
+
+}

+ 97 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatCheckBox.java

@@ -0,0 +1,97 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.support.annotation.DrawableRes;
+import android.support.v7.widget.AppCompatCheckBox;
+import android.util.AttributeSet;
+
+import skin.support.R;
+
+/**
+ * Created by ximsfei on 17-1-14.
+ */
+
+public class SkinCompatCheckBox extends AppCompatCheckBox implements SkinCompatSupportable {
+    private SkinCompatCompoundButtonHelper mCompoundButtonHelper;
+    private SkinCompatTextHelper mTextHelper;
+    private SkinCompatBackgroundHelper mBackgroundTintHelper;
+
+    public SkinCompatCheckBox(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatCheckBox(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.checkboxStyle);
+    }
+
+    public SkinCompatCheckBox(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mCompoundButtonHelper = new SkinCompatCompoundButtonHelper(this);
+        mCompoundButtonHelper.loadFromAttributes(attrs, defStyleAttr);
+        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
+        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
+        mTextHelper = SkinCompatTextHelper.create(this);
+        mTextHelper.loadFromAttributes(attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setButtonDrawable(@DrawableRes int resId) {
+        super.setButtonDrawable(resId);
+        if (mCompoundButtonHelper != null) {
+            mCompoundButtonHelper.setButtonDrawable(resId);
+        }
+    }
+
+    @Override
+    public void setBackgroundResource(@DrawableRes int resId) {
+        super.setBackgroundResource(resId);
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.onSetBackgroundResource(resId);
+        }
+    }
+
+    @Override
+    public void setTextAppearance(int resId) {
+        setTextAppearance(getContext(), resId);
+    }
+
+    @Override
+    public void setTextAppearance(Context context, int resId) {
+        super.setTextAppearance(context, resId);
+        if (mTextHelper != null) {
+            mTextHelper.onSetTextAppearance(context, resId);
+        }
+    }
+
+    @Override
+    public void setCompoundDrawablesRelativeWithIntrinsicBounds(
+            @DrawableRes int start, @DrawableRes int top, @DrawableRes int end, @DrawableRes int bottom) {
+        super.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
+        if (mTextHelper != null) {
+            mTextHelper.onSetCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
+        }
+    }
+
+    @Override
+    public void setCompoundDrawablesWithIntrinsicBounds(
+            @DrawableRes int left, @DrawableRes int top, @DrawableRes int right, @DrawableRes int bottom) {
+        super.setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
+        if (mTextHelper != null) {
+            mTextHelper.onSetCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
+        }
+    }
+
+    @Override
+    public void applySkin() {
+        if (mCompoundButtonHelper != null) {
+            mCompoundButtonHelper.applySkin();
+        }
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.applySkin();
+        }
+        if (mTextHelper != null) {
+            mTextHelper.applySkin();
+        }
+    }
+
+}

+ 111 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatCheckedTextView.java

@@ -0,0 +1,111 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.support.annotation.DrawableRes;
+import android.support.v7.widget.AppCompatCheckedTextView;
+import android.util.AttributeSet;
+
+import skin.support.R;
+import skin.support.content.res.SkinCompatResources;
+
+import static skin.support.widget.SkinCompatHelper.INVALID_ID;
+
+/**
+ * Created by ximsfei on 17-1-14.
+ */
+
+public class SkinCompatCheckedTextView extends AppCompatCheckedTextView implements SkinCompatSupportable {
+
+    private static final int[] TINT_ATTRS = {
+            android.R.attr.checkMark
+    };
+    private int mCheckMarkResId = INVALID_ID;
+
+    private SkinCompatTextHelper mTextHelper;
+    private SkinCompatBackgroundHelper mBackgroundTintHelper;
+
+    public SkinCompatCheckedTextView(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatCheckedTextView(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.checkedTextViewStyle);
+    }
+
+    public SkinCompatCheckedTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
+        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
+        mTextHelper = SkinCompatTextHelper.create(this);
+        mTextHelper.loadFromAttributes(attrs, defStyleAttr);
+
+        TypedArray a = context.obtainStyledAttributes(attrs, TINT_ATTRS, defStyleAttr, 0);
+        mCheckMarkResId = a.getResourceId(0, INVALID_ID);
+        a.recycle();
+        applyCheckMark();
+    }
+
+    @Override
+    public void setCheckMarkDrawable(@DrawableRes int resId) {
+        mCheckMarkResId = resId;
+        applyCheckMark();
+    }
+
+    @Override
+    public void setBackgroundResource(@DrawableRes int resId) {
+        super.setBackgroundResource(resId);
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.onSetBackgroundResource(resId);
+        }
+    }
+
+    @Override
+    public void setTextAppearance(int resId) {
+        setTextAppearance(getContext(), resId);
+    }
+
+    @Override
+    public void setTextAppearance(Context context, int resId) {
+        super.setTextAppearance(context, resId);
+        if (mTextHelper != null) {
+            mTextHelper.onSetTextAppearance(context, resId);
+        }
+    }
+
+    @Override
+    public void setCompoundDrawablesRelativeWithIntrinsicBounds(
+            @DrawableRes int start, @DrawableRes int top, @DrawableRes int end, @DrawableRes int bottom) {
+        super.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
+        if (mTextHelper != null) {
+            mTextHelper.onSetCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
+        }
+    }
+
+    @Override
+    public void setCompoundDrawablesWithIntrinsicBounds(
+            @DrawableRes int left, @DrawableRes int top, @DrawableRes int right, @DrawableRes int bottom) {
+        super.setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
+        if (mTextHelper != null) {
+            mTextHelper.onSetCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
+        }
+    }
+
+    @Override
+    public void applySkin() {
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.applySkin();
+        }
+        if (mTextHelper != null) {
+            mTextHelper.applySkin();
+        }
+        applyCheckMark();
+    }
+
+    private void applyCheckMark() {
+        mCheckMarkResId = SkinCompatHelper.checkResourceId(mCheckMarkResId);
+        if (mCheckMarkResId != INVALID_ID) {
+            setCheckMarkDrawable(SkinCompatResources.getDrawableCompat(getContext(), mCheckMarkResId));
+        }
+    }
+}

+ 67 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatCompoundButtonHelper.java

@@ -0,0 +1,67 @@
+package skin.support.widget;
+
+import android.content.res.TypedArray;
+import android.support.v4.widget.CompoundButtonCompat;
+import android.util.AttributeSet;
+import android.widget.CompoundButton;
+
+import skin.support.R;
+import skin.support.content.res.SkinCompatResources;
+
+/**
+ * Created by ximsfei on 17-1-14.
+ */
+public class SkinCompatCompoundButtonHelper extends SkinCompatHelper {
+    private final CompoundButton mView;
+    private int mButtonResourceId = INVALID_ID;
+    private int mButtonTintResId = INVALID_ID;
+
+    public SkinCompatCompoundButtonHelper(CompoundButton view) {
+        mView = view;
+    }
+
+    void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
+        TypedArray a = mView.getContext().obtainStyledAttributes(attrs, R.styleable.CompoundButton,
+                defStyleAttr, INVALID_ID);
+        try {
+            if (a.hasValue(R.styleable.CompoundButton_android_button)) {
+                mButtonResourceId = a.getResourceId(
+                        R.styleable.CompoundButton_android_button, INVALID_ID);
+            }
+//                if (resourceId != 0) {
+//                    mView.setButtonDrawable(
+//                            AppCompatResources.getDrawable(mView.getContext(), resourceId));
+//                }
+//            }
+            if (a.hasValue(R.styleable.CompoundButton_buttonTint)) {
+                mButtonTintResId = a.getResourceId(R.styleable.CompoundButton_buttonTint, INVALID_ID);
+            }
+//            if (a.hasValue(R.styleable.CompoundButton_buttonTintMode)) {
+//                CompoundButtonCompat.setButtonTintMode(mView,
+//                        DrawableUtils.parseTintMode(
+//                                a.getInt(R.styleable.CompoundButton_buttonTintMode, -1),
+//                                null));
+//            }
+        } finally {
+            a.recycle();
+        }
+        applySkin();
+    }
+
+    public void setButtonDrawable(int resId) {
+        mButtonResourceId = resId;
+        applySkin();
+    }
+
+    @Override
+    public void applySkin() {
+        mButtonResourceId = SkinCompatHelper.checkResourceId(mButtonResourceId);
+        if (mButtonResourceId != INVALID_ID) {
+            mView.setButtonDrawable(SkinCompatResources.getDrawableCompat(mView.getContext(), mButtonResourceId));
+        }
+        mButtonTintResId = SkinCompatHelper.checkResourceId(mButtonTintResId);
+        if (mButtonTintResId != INVALID_ID) {
+            CompoundButtonCompat.setButtonTintList(mView, SkinCompatResources.getColorStateList(mView.getContext(), mButtonTintResId));
+        }
+    }
+}

+ 88 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatEditText.java

@@ -0,0 +1,88 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.support.annotation.DrawableRes;
+import android.support.v7.appcompat.R;
+import android.support.v7.widget.AppCompatEditText;
+import android.util.AttributeSet;
+
+import static skin.support.widget.SkinCompatHelper.INVALID_ID;
+
+/**
+ * Created by ximsfei on 2017/1/10.
+ */
+
+public class SkinCompatEditText extends AppCompatEditText implements SkinCompatSupportable {
+    private SkinCompatTextHelper mTextHelper;
+    private SkinCompatBackgroundHelper mBackgroundTintHelper;
+
+    public SkinCompatEditText(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatEditText(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.editTextStyle);
+    }
+
+    public SkinCompatEditText(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
+        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
+        mTextHelper = SkinCompatTextHelper.create(this);
+        mTextHelper.loadFromAttributes(attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setBackgroundResource(@DrawableRes int resId) {
+        super.setBackgroundResource(resId);
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.onSetBackgroundResource(resId);
+        }
+    }
+
+    @Override
+    public void setTextAppearance(int resId) {
+        setTextAppearance(getContext(), resId);
+    }
+
+    @Override
+    public void setTextAppearance(Context context, int resId) {
+        super.setTextAppearance(context, resId);
+        if (mTextHelper != null) {
+            mTextHelper.onSetTextAppearance(context, resId);
+        }
+    }
+
+    public int getTextColorResId() {
+        return mTextHelper != null ? mTextHelper.getTextColorResId() : INVALID_ID;
+    }
+
+    @Override
+    public void setCompoundDrawablesRelativeWithIntrinsicBounds(
+            @DrawableRes int start, @DrawableRes int top, @DrawableRes int end, @DrawableRes int bottom) {
+        super.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
+        if (mTextHelper != null) {
+            mTextHelper.onSetCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
+        }
+    }
+
+    @Override
+    public void setCompoundDrawablesWithIntrinsicBounds(
+            @DrawableRes int left, @DrawableRes int top, @DrawableRes int right, @DrawableRes int bottom) {
+        super.setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
+        if (mTextHelper != null) {
+            mTextHelper.onSetCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
+        }
+    }
+
+    @Override
+    public void applySkin() {
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.applySkin();
+        }
+        if (mTextHelper != null) {
+            mTextHelper.applySkin();
+        }
+    }
+
+}

+ 43 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatFrameLayout.java

@@ -0,0 +1,43 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+/**
+ * Created by pengfengwang on 2017/1/13.
+ */
+
+public class SkinCompatFrameLayout extends FrameLayout implements SkinCompatSupportable {
+    private SkinCompatBackgroundHelper mBackgroundTintHelper;
+
+    public SkinCompatFrameLayout(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatFrameLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public SkinCompatFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
+        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setBackgroundResource(int resId) {
+        super.setBackgroundResource(resId);
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.onSetBackgroundResource(resId);
+        }
+    }
+
+    @Override
+    public void applySkin() {
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.applySkin();
+        }
+    }
+
+}

+ 18 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatHelper.java

@@ -0,0 +1,18 @@
+package skin.support.widget;
+
+
+/**
+ * Created by ximsfei on 2017/1/13.
+ */
+
+public abstract class SkinCompatHelper {
+    protected static final String SYSTEM_ID_PREFIX = "1";
+    public static final int INVALID_ID = 0;
+
+    final static public int checkResourceId(int resId) {
+        String hexResId = Integer.toHexString(resId);
+        return hexResId.startsWith(SYSTEM_ID_PREFIX) ? INVALID_ID : resId;
+    }
+
+    abstract public void applySkin();
+}

+ 60 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatImageButton.java

@@ -0,0 +1,60 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.support.annotation.DrawableRes;
+import android.support.v7.appcompat.R;
+import android.support.v7.widget.AppCompatImageButton;
+import android.util.AttributeSet;
+
+/**
+ * Created by ximsfei on 17-1-13.
+ */
+
+public class SkinCompatImageButton extends AppCompatImageButton implements SkinCompatSupportable {
+    private SkinCompatBackgroundHelper mBackgroundTintHelper;
+    private SkinCompatImageHelper mImageHelper;
+
+    public SkinCompatImageButton(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatImageButton(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.imageButtonStyle);
+    }
+
+    public SkinCompatImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
+        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
+
+        mImageHelper = new SkinCompatImageHelper(this);
+        mImageHelper.loadFromAttributes(attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setBackgroundResource(@DrawableRes int resId) {
+        super.setBackgroundResource(resId);
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.onSetBackgroundResource(resId);
+        }
+    }
+
+    @Override
+    public void setImageResource(@DrawableRes int resId) {
+        // Intercept this call and instead retrieve the Drawable via the image helper
+        if (mImageHelper != null) {
+            mImageHelper.setImageResource(resId);
+        }
+    }
+
+    @Override
+    public void applySkin() {
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.applySkin();
+        }
+        if (mImageHelper != null) {
+            mImageHelper.applySkin();
+        }
+    }
+
+}

+ 61 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatImageHelper.java

@@ -0,0 +1,61 @@
+package skin.support.widget;
+
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+import skin.support.R;
+import skin.support.content.res.SkinCompatResources;
+
+/**
+ * Created by ximsfei on 2017/1/12.
+ */
+public class SkinCompatImageHelper extends SkinCompatHelper {
+    private static final String TAG = SkinCompatImageHelper.class.getSimpleName();
+    private final ImageView mView;
+    private int mSrcResId = INVALID_ID;
+    private int mSrcCompatResId = INVALID_ID;
+
+    public SkinCompatImageHelper(ImageView imageView) {
+        mView = imageView;
+    }
+
+    public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
+        TypedArray a = null;
+        try {
+            a = mView.getContext().obtainStyledAttributes(attrs, R.styleable.SkinCompatImageView, defStyleAttr, 0);
+            mSrcResId = a.getResourceId(R.styleable.SkinCompatImageView_android_src, INVALID_ID);
+            mSrcCompatResId = a.getResourceId(R.styleable.SkinCompatImageView_srcCompat, INVALID_ID);
+        } finally {
+            if (a != null) {
+                a.recycle();
+            }
+        }
+        applySkin();
+    }
+
+    public void setImageResource(int resId) {
+        mSrcResId = resId;
+        applySkin();
+    }
+
+    public void applySkin() {
+        mSrcCompatResId = checkResourceId(mSrcCompatResId);
+        if (mSrcCompatResId != INVALID_ID) {
+            Drawable drawable = SkinCompatResources.getDrawableCompat(mView.getContext(), mSrcCompatResId);
+            if (drawable != null) {
+                mView.setImageDrawable(drawable);
+            }
+        } else {
+            mSrcResId = checkResourceId(mSrcResId);
+            if (mSrcResId == INVALID_ID) {
+                return;
+            }
+            Drawable drawable = SkinCompatResources.getDrawableCompat(mView.getContext(), mSrcResId);
+            if (drawable != null) {
+                mView.setImageDrawable(drawable);
+            }
+        }
+    }
+}

+ 59 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatImageView.java

@@ -0,0 +1,59 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.support.annotation.DrawableRes;
+import android.support.v7.widget.AppCompatImageView;
+import android.util.AttributeSet;
+
+/**
+ * Created by ximsfei on 2017/1/10.
+ */
+
+public class SkinCompatImageView extends AppCompatImageView implements SkinCompatSupportable {
+    private SkinCompatBackgroundHelper mBackgroundTintHelper;
+    private SkinCompatImageHelper mImageHelper;
+
+    public SkinCompatImageView(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatImageView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public SkinCompatImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
+        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
+
+        mImageHelper = new SkinCompatImageHelper(this);
+        mImageHelper.loadFromAttributes(attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setBackgroundResource(@DrawableRes int resId) {
+        super.setBackgroundResource(resId);
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.onSetBackgroundResource(resId);
+        }
+    }
+
+    @Override
+    public void setImageResource(@DrawableRes int resId) {
+        // Intercept this call and instead retrieve the Drawable via the image helper
+        if (mImageHelper != null) {
+            mImageHelper.setImageResource(resId);
+        }
+    }
+
+    @Override
+    public void applySkin() {
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.applySkin();
+        }
+        if (mImageHelper != null) {
+            mImageHelper.applySkin();
+        }
+    }
+
+}

+ 43 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatLinearLayout.java

@@ -0,0 +1,43 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+
+/**
+ * Created by pengfengwang on 2017/1/13.
+ */
+
+public class SkinCompatLinearLayout extends LinearLayout implements SkinCompatSupportable {
+    private SkinCompatBackgroundHelper mBackgroundTintHelper;
+
+    public SkinCompatLinearLayout(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatLinearLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public SkinCompatLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
+        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setBackgroundResource(int resId) {
+        super.setBackgroundResource(resId);
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.onSetBackgroundResource(resId);
+        }
+    }
+
+    @Override
+    public void applySkin() {
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.applySkin();
+        }
+    }
+
+}

+ 116 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatMultiAutoCompleteTextView.java

@@ -0,0 +1,116 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.DrawableRes;
+import android.support.v7.widget.AppCompatMultiAutoCompleteTextView;
+import android.util.AttributeSet;
+
+import skin.support.R;
+import skin.support.content.res.SkinCompatResources;
+
+import static skin.support.widget.SkinCompatHelper.INVALID_ID;
+
+/**
+ * Created by ximsfei on 17-1-14.
+ */
+
+public class SkinCompatMultiAutoCompleteTextView extends AppCompatMultiAutoCompleteTextView implements SkinCompatSupportable {
+    private static final int[] TINT_ATTRS = {
+            android.R.attr.popupBackground
+    };
+    private int mDropDownBackgroundResId = INVALID_ID;
+    private SkinCompatTextHelper mTextHelper;
+    private SkinCompatBackgroundHelper mBackgroundTintHelper;
+
+    public SkinCompatMultiAutoCompleteTextView(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatMultiAutoCompleteTextView(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.editTextStyle);
+    }
+
+    public SkinCompatMultiAutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        TypedArray a = context.obtainStyledAttributes(attrs, TINT_ATTRS, defStyleAttr, 0);
+        if (a.hasValue(0)) {
+            mDropDownBackgroundResId = a.getResourceId(0, INVALID_ID);
+        }
+        a.recycle();
+        applyDropDownBackgroundResource();
+        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
+        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
+        mTextHelper = SkinCompatTextHelper.create(this);
+        mTextHelper.loadFromAttributes(attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setDropDownBackgroundResource(@DrawableRes int resId) {
+        super.setDropDownBackgroundResource(resId);
+        mDropDownBackgroundResId = resId;
+        applyDropDownBackgroundResource();
+    }
+
+    private void applyDropDownBackgroundResource() {
+        mDropDownBackgroundResId = SkinCompatHelper.checkResourceId(mDropDownBackgroundResId);
+        if (mDropDownBackgroundResId != INVALID_ID) {
+            Drawable drawable = SkinCompatResources.getDrawableCompat(getContext(), mDropDownBackgroundResId);
+            if (drawable != null) {
+                setDropDownBackgroundDrawable(drawable);
+            }
+        }
+    }
+
+    @Override
+    public void setBackgroundResource(@DrawableRes int resId) {
+        super.setBackgroundResource(resId);
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.onSetBackgroundResource(resId);
+        }
+    }
+
+    @Override
+    public void setTextAppearance(int resId) {
+        setTextAppearance(getContext(), resId);
+    }
+
+    @Override
+    public void setTextAppearance(Context context, int resId) {
+        super.setTextAppearance(context, resId);
+        if (mTextHelper != null) {
+            mTextHelper.onSetTextAppearance(context, resId);
+        }
+    }
+
+    @Override
+    public void setCompoundDrawablesRelativeWithIntrinsicBounds(
+            @DrawableRes int start, @DrawableRes int top, @DrawableRes int end, @DrawableRes int bottom) {
+        super.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
+        if (mTextHelper != null) {
+            mTextHelper.onSetCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
+        }
+    }
+
+    @Override
+    public void setCompoundDrawablesWithIntrinsicBounds(
+            @DrawableRes int left, @DrawableRes int top, @DrawableRes int right, @DrawableRes int bottom) {
+        super.setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
+        if (mTextHelper != null) {
+            mTextHelper.onSetCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
+        }
+    }
+
+    @Override
+    public void applySkin() {
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.applySkin();
+        }
+        if (mTextHelper != null) {
+            mTextHelper.applySkin();
+        }
+        applyDropDownBackgroundResource();
+    }
+
+}

+ 35 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatProgressBar.java

@@ -0,0 +1,35 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ProgressBar;
+
+/**
+ * Created by ximsfei on 2017/1/19.
+ */
+
+public class SkinCompatProgressBar extends ProgressBar implements SkinCompatSupportable {
+    private SkinCompatProgressBarHelper mSkinCompatProgressBarHelper;
+
+    public SkinCompatProgressBar(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatProgressBar(Context context, AttributeSet attrs) {
+        this(context, attrs, android.R.attr.progressBarStyle);
+    }
+
+    public SkinCompatProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mSkinCompatProgressBarHelper = new SkinCompatProgressBarHelper(this);
+        mSkinCompatProgressBarHelper.loadFromAttributes(attrs, defStyleAttr);
+    }
+
+    @Override
+    public void applySkin() {
+        if (mSkinCompatProgressBarHelper != null) {
+            mSkinCompatProgressBarHelper.applySkin();
+        }
+    }
+
+}

+ 168 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatProgressBarHelper.java

@@ -0,0 +1,168 @@
+package skin.support.widget;
+
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Shader;
+import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ClipDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.RoundRectShape;
+import android.graphics.drawable.shapes.Shape;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.widget.ProgressBar;
+
+import skin.support.R;
+import skin.support.content.res.SkinCompatResources;
+import skin.support.utils.SkinCompatVersionUtils;
+
+/**
+ * Created by ximsfei on 2017/1/20.
+ */
+
+public class SkinCompatProgressBarHelper extends SkinCompatHelper {
+
+    private final ProgressBar mView;
+
+    private Bitmap mSampleTile;
+    private int mIndeterminateDrawableResId = INVALID_ID;
+    private int mProgressDrawableResId = INVALID_ID;
+    private int mIndeterminateTintResId = INVALID_ID;
+
+    SkinCompatProgressBarHelper(ProgressBar view) {
+        mView = view;
+    }
+
+    void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
+        TypedArray a = mView.getContext().obtainStyledAttributes(attrs, R.styleable.SkinCompatProgressBar, defStyleAttr, 0);
+
+        mIndeterminateDrawableResId = a.getResourceId(R.styleable.SkinCompatProgressBar_android_indeterminateDrawable, INVALID_ID);
+        mProgressDrawableResId = a.getResourceId(R.styleable.SkinCompatProgressBar_android_progressDrawable, INVALID_ID);
+
+        a.recycle();
+        if (Build.VERSION.SDK_INT > 21) {
+            a = mView.getContext().obtainStyledAttributes(attrs, new int[]{android.R.attr.indeterminateTint}, defStyleAttr, 0);
+            mIndeterminateTintResId = a.getResourceId(0, INVALID_ID);
+            a.recycle();
+        }
+        applySkin();
+    }
+
+    /**
+     * Converts a drawable to a tiled version of itself. It will recursively
+     * traverse layer and state list drawables.
+     */
+    private Drawable tileify(Drawable drawable, boolean clip) {
+        if (SkinCompatVersionUtils.isV4WrappedDrawable(drawable)) {
+            Drawable inner = SkinCompatVersionUtils.getV4WrappedDrawableWrappedDrawable(drawable);
+            if (inner != null) {
+                inner = tileify(inner, clip);
+//                ((WrappedDrawable) drawable).setWrappedDrawable(inner);
+                SkinCompatVersionUtils.setV4WrappedDrawableWrappedDrawable(drawable, inner);
+            }
+        } else if (SkinCompatVersionUtils.isV4DrawableWrapper(drawable)) {
+            Drawable inner = SkinCompatVersionUtils.getV4DrawableWrapperWrappedDrawable(drawable);
+            if (inner != null) {
+                inner = tileify(inner, clip);
+//                ((DrawableWrapper) drawable).setWrappedDrawable(inner);
+                SkinCompatVersionUtils.setV4DrawableWrapperWrappedDrawable(drawable, inner);
+            }
+        } else if (drawable instanceof LayerDrawable) {
+            LayerDrawable background = (LayerDrawable) drawable;
+            final int N = background.getNumberOfLayers();
+            Drawable[] outDrawables = new Drawable[N];
+
+            for (int i = 0; i < N; i++) {
+                int id = background.getId(i);
+                outDrawables[i] = tileify(background.getDrawable(i),
+                        (id == android.R.id.progress || id == android.R.id.secondaryProgress));
+            }
+            LayerDrawable newBg = new LayerDrawable(outDrawables);
+
+            for (int i = 0; i < N; i++) {
+                newBg.setId(i, background.getId(i));
+            }
+
+            return newBg;
+
+        } else if (drawable instanceof BitmapDrawable) {
+            final BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
+            final Bitmap tileBitmap = bitmapDrawable.getBitmap();
+            if (mSampleTile == null) {
+                mSampleTile = tileBitmap;
+            }
+
+            final ShapeDrawable shapeDrawable = new ShapeDrawable(getDrawableShape());
+            final BitmapShader bitmapShader = new BitmapShader(tileBitmap,
+                    Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);
+            shapeDrawable.getPaint().setShader(bitmapShader);
+            shapeDrawable.getPaint().setColorFilter(bitmapDrawable.getPaint().getColorFilter());
+            return (clip) ? new ClipDrawable(shapeDrawable, Gravity.LEFT,
+                    ClipDrawable.HORIZONTAL) : shapeDrawable;
+        }
+
+        return drawable;
+    }
+
+    /**
+     * Convert a AnimationDrawable for use as a barberpole animation.
+     * Each frame of the animation is wrapped in a ClipDrawable and
+     * given a tiling BitmapShader.
+     */
+    private Drawable tileifyIndeterminate(Drawable drawable) {
+        if (drawable instanceof AnimationDrawable) {
+            AnimationDrawable background = (AnimationDrawable) drawable;
+            final int N = background.getNumberOfFrames();
+            AnimationDrawable newBg = new AnimationDrawable();
+            newBg.setOneShot(background.isOneShot());
+
+            for (int i = 0; i < N; i++) {
+                Drawable frame = tileify(background.getFrame(i), true);
+                frame.setLevel(10000);
+                newBg.addFrame(frame, background.getDuration(i));
+            }
+            newBg.setLevel(10000);
+            drawable = newBg;
+        }
+        return drawable;
+    }
+
+    private Shape getDrawableShape() {
+        final float[] roundedCorners = new float[]{5, 5, 5, 5, 5, 5, 5, 5};
+        return new RoundRectShape(roundedCorners, null, null);
+    }
+
+    @Override
+    public void applySkin() {
+        mIndeterminateDrawableResId = checkResourceId(mIndeterminateDrawableResId);
+        if (mIndeterminateDrawableResId != INVALID_ID) {
+            Drawable drawable = SkinCompatResources.getDrawableCompat(mView.getContext(), mIndeterminateDrawableResId);
+            drawable.setBounds(mView.getIndeterminateDrawable().getBounds());
+            mView.setIndeterminateDrawable(tileifyIndeterminate(drawable));
+        }
+
+        mProgressDrawableResId = checkProgressDrawableResId(mProgressDrawableResId);
+        if (mProgressDrawableResId != INVALID_ID) {
+            Drawable drawable = SkinCompatResources.getDrawableCompat(mView.getContext(), mProgressDrawableResId);
+            mView.setProgressDrawable(tileify(drawable, false));
+        }
+        if (Build.VERSION.SDK_INT > 21) {
+            mIndeterminateTintResId = checkResourceId(mIndeterminateTintResId);
+            if (mIndeterminateTintResId != INVALID_ID) {
+                mView.setIndeterminateTintList(SkinCompatResources.getColorStateList(mView.getContext(), mIndeterminateTintResId));
+            }
+        }
+    }
+
+    private int checkProgressDrawableResId(int mProgressDrawableResId) {
+        if (mProgressDrawableResId == R.drawable.abc_ratingbar_material) {
+            return INVALID_ID;
+        }
+        return checkResourceId(mProgressDrawableResId);
+    }
+}

+ 95 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatRadioButton.java

@@ -0,0 +1,95 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.support.annotation.DrawableRes;
+import android.support.v7.widget.AppCompatRadioButton;
+import android.util.AttributeSet;
+
+import skin.support.R;
+
+/**
+ * Created by ximsfei on 17-1-14.
+ */
+
+public class SkinCompatRadioButton extends AppCompatRadioButton implements SkinCompatSupportable {
+    private SkinCompatTextHelper mTextHelper;
+    private SkinCompatCompoundButtonHelper mCompoundButtonHelper;
+    private SkinCompatBackgroundHelper mBackgroundTintHelper;
+
+    public SkinCompatRadioButton(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatRadioButton(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.radioButtonStyle);
+    }
+
+    public SkinCompatRadioButton(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mCompoundButtonHelper = new SkinCompatCompoundButtonHelper(this);
+        mCompoundButtonHelper.loadFromAttributes(attrs, defStyleAttr);
+        mTextHelper = SkinCompatTextHelper.create(this);
+        mTextHelper.loadFromAttributes(attrs, defStyleAttr);
+        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
+        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setButtonDrawable(@DrawableRes int resId) {
+        super.setButtonDrawable(resId);
+        if (mCompoundButtonHelper != null) {
+            mCompoundButtonHelper.setButtonDrawable(resId);
+        }
+    }
+
+    @Override
+    public void setTextAppearance(int resId) {
+        setTextAppearance(getContext(), resId);
+    }
+
+    @Override
+    public void setTextAppearance(Context context, int resId) {
+        super.setTextAppearance(context, resId);
+        if (mTextHelper != null) {
+            mTextHelper.onSetTextAppearance(context, resId);
+        }
+    }
+
+    @Override
+    public void setCompoundDrawablesRelativeWithIntrinsicBounds(
+            @DrawableRes int start, @DrawableRes int top, @DrawableRes int end, @DrawableRes int bottom) {
+        super.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
+        if (mTextHelper != null) {
+            mTextHelper.onSetCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
+        }
+    }
+
+    @Override
+    public void setCompoundDrawablesWithIntrinsicBounds(
+            @DrawableRes int left, @DrawableRes int top, @DrawableRes int right, @DrawableRes int bottom) {
+        super.setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
+        if (mTextHelper != null) {
+            mTextHelper.onSetCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
+        }
+    }
+    public void setBackgroundResource(int resId) {
+        super.setBackgroundResource(resId);
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.onSetBackgroundResource(resId);
+        }
+    }
+
+    @Override
+    public void applySkin() {
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.applySkin();
+        }
+        if (mCompoundButtonHelper != null) {
+            mCompoundButtonHelper.applySkin();
+        }
+        if (mTextHelper != null) {
+            mTextHelper.applySkin();
+        }
+    }
+
+}

+ 37 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatRadioGroup.java

@@ -0,0 +1,37 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.RadioGroup;
+
+/**
+ * Created by ximsf on 2017/3/23.
+ */
+
+public class SkinCompatRadioGroup extends RadioGroup implements SkinCompatSupportable {
+    private SkinCompatBackgroundHelper mBackgroundTintHelper;
+    public SkinCompatRadioGroup(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatRadioGroup(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
+        mBackgroundTintHelper.loadFromAttributes(attrs, 0);
+    }
+
+    @Override
+    public void setBackgroundResource(int resId) {
+        super.setBackgroundResource(resId);
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.onSetBackgroundResource(resId);
+        }
+    }
+
+    @Override
+    public void applySkin() {
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.applySkin();
+        }
+    }
+}

+ 37 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatRatingBar.java

@@ -0,0 +1,37 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.support.v7.widget.AppCompatRatingBar;
+import android.util.AttributeSet;
+
+import skin.support.R;
+
+/**
+ * Created by ximsfei on 17-1-21.
+ */
+
+public class SkinCompatRatingBar extends AppCompatRatingBar implements SkinCompatSupportable {
+    private SkinCompatProgressBarHelper mSkinCompatProgressBarHelper;
+
+    public SkinCompatRatingBar(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatRatingBar(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.ratingBarStyle);
+    }
+
+    public SkinCompatRatingBar(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mSkinCompatProgressBarHelper = new SkinCompatProgressBarHelper(this);
+        mSkinCompatProgressBarHelper.loadFromAttributes(attrs, defStyleAttr);
+    }
+
+    @Override
+    public void applySkin() {
+        if (mSkinCompatProgressBarHelper != null) {
+            mSkinCompatProgressBarHelper.applySkin();
+        }
+    }
+
+}

+ 43 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatRelativeLayout.java

@@ -0,0 +1,43 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.RelativeLayout;
+
+/**
+ * Created by pengfengwang on 2017/1/13.
+ */
+
+public class SkinCompatRelativeLayout extends RelativeLayout implements SkinCompatSupportable {
+    private SkinCompatBackgroundHelper mBackgroundTintHelper;
+
+    public SkinCompatRelativeLayout(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatRelativeLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public SkinCompatRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
+        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setBackgroundResource(int resId) {
+        super.setBackgroundResource(resId);
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.onSetBackgroundResource(resId);
+        }
+    }
+
+    @Override
+    public void applySkin() {
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.applySkin();
+        }
+    }
+
+}

+ 43 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatScrollView.java

@@ -0,0 +1,43 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.support.annotation.DrawableRes;
+import android.util.AttributeSet;
+import android.widget.ScrollView;
+
+/**
+ * Created by Jungle68 on 2017/6/27.
+ */
+public class SkinCompatScrollView extends ScrollView implements SkinCompatSupportable {
+    private SkinCompatBackgroundHelper mBackgroundTintHelper;
+
+    public SkinCompatScrollView(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatScrollView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public SkinCompatScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
+        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setBackgroundResource(@DrawableRes int resId) {
+        super.setBackgroundResource(resId);
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.onSetBackgroundResource(resId);
+        }
+    }
+
+    @Override
+    public void applySkin() {
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.applySkin();
+        }
+    }
+
+}

+ 38 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatSeekBar.java

@@ -0,0 +1,38 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.support.v7.widget.AppCompatSeekBar;
+import android.util.AttributeSet;
+
+import skin.support.R;
+
+/**
+ * Created by ximsfei on 17-1-21.
+ */
+
+public class SkinCompatSeekBar extends AppCompatSeekBar implements SkinCompatSupportable {
+    private SkinCompatSeekBarHelper mSkinCompatSeekBarHelper;
+
+    public SkinCompatSeekBar(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatSeekBar(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.seekBarStyle);
+    }
+
+    public SkinCompatSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mSkinCompatSeekBarHelper = new SkinCompatSeekBarHelper(this);
+        mSkinCompatSeekBarHelper.loadFromAttributes(attrs, defStyleAttr);
+    }
+
+
+    @Override
+    public void applySkin() {
+        if (mSkinCompatSeekBarHelper != null) {
+            mSkinCompatSeekBarHelper.applySkin();
+        }
+    }
+
+}

+ 63 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatSeekBarHelper.java

@@ -0,0 +1,63 @@
+package skin.support.widget;
+
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.widget.SeekBar;
+
+import skin.support.R;
+import skin.support.content.res.SkinCompatResources;
+
+/**
+ * Created by ximsfei on 17-1-21.
+ */
+public class SkinCompatSeekBarHelper extends SkinCompatProgressBarHelper {
+    private final SeekBar mView;
+
+    private int mThumbResId = INVALID_ID;
+
+    public SkinCompatSeekBarHelper(SeekBar view) {
+        super(view);
+        mView = view;
+    }
+
+    @Override
+    void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
+        super.loadFromAttributes(attrs, defStyleAttr);
+
+        TypedArray a = mView.getContext().obtainStyledAttributes(attrs, R.styleable.AppCompatSeekBar, defStyleAttr, 0);
+        mThumbResId = a.getResourceId(R.styleable.AppCompatSeekBar_android_thumb, INVALID_ID);
+//        final Drawable drawable = a.getDrawableIfKnown(R.styleable.AppCompatSeekBar_android_thumb);
+//        if (drawable != null) {
+//            mView.setThumb(drawable);
+//        }
+
+//        mTickMarkResId = a.getResourceId(R.styleable.AppCompatSeekBar_tickMark, INVALID_ID);
+//        final Drawable tickMark = a.getDrawable(R.styleable.AppCompatSeekBar_tickMark);
+//        setTickMark(tickMark);
+
+//        if (a.hasValue(R.styleable.AppCompatSeekBar_tickMarkTintMode)) {
+//            mTickMarkTintMode = DrawableUtils.parseTintMode(a.getInt(
+//                    R.styleable.AppCompatSeekBar_tickMarkTintMode, -1), mTickMarkTintMode);
+//            mHasTickMarkTintMode = true;
+//        }
+
+//        if (a.hasValue(R.styleable.AppCompatSeekBar_tickMarkTint)) {
+//            mTickMarkTintList = a.getColorStateList(R.styleable.AppCompatSeekBar_tickMarkTint);
+//            mHasTickMarkTint = true;
+//        }
+
+        a.recycle();
+
+//        applyTickMarkTint();
+        applySkin();
+    }
+
+    @Override
+    public void applySkin() {
+        super.applySkin();
+        mThumbResId = checkResourceId(mThumbResId);
+        if (mThumbResId != INVALID_ID) {
+            mView.setThumb(SkinCompatResources.getDrawableCompat(mView.getContext(), mThumbResId));
+        }
+    }
+}

+ 116 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatSpinner.java

@@ -0,0 +1,116 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.os.Build;
+import android.support.annotation.DrawableRes;
+import android.support.v7.widget.AppCompatSpinner;
+import android.util.AttributeSet;
+import android.util.Log;
+
+import skin.support.R;
+import skin.support.content.res.SkinCompatResources;
+
+import static skin.support.widget.SkinCompatHelper.INVALID_ID;
+import static skin.support.widget.SkinCompatHelper.checkResourceId;
+
+/**
+ * Created by ximsfei on 17-1-21.
+ */
+
+public class SkinCompatSpinner extends AppCompatSpinner implements SkinCompatSupportable {
+    private static final String TAG = SkinCompatSpinner.class.getSimpleName();
+
+    private static final int[] ATTRS_ANDROID_SPINNERMODE = {android.R.attr.spinnerMode};
+
+    private static final int MODE_DIALOG = 0;
+    private static final int MODE_DROPDOWN = 1;
+    private static final int MODE_THEME = -1;
+
+    private SkinCompatBackgroundHelper mBackgroundTintHelper;
+    private int mPopupBackgroundResId = INVALID_ID;
+
+    public SkinCompatSpinner(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatSpinner(Context context, int mode) {
+        this(context, null, R.attr.spinnerStyle, mode);
+    }
+
+    public SkinCompatSpinner(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.spinnerStyle);
+    }
+
+    public SkinCompatSpinner(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, MODE_THEME);
+    }
+
+    public SkinCompatSpinner(Context context, AttributeSet attrs, int defStyleAttr, int mode) {
+        this(context, attrs, defStyleAttr, mode, null);
+    }
+
+    public SkinCompatSpinner(Context context, AttributeSet attrs, int defStyleAttr, int mode, Resources.Theme popupTheme) {
+        super(context, attrs, defStyleAttr, mode, popupTheme);
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Spinner, defStyleAttr, 0);
+
+        if (getPopupContext() != null) {
+            if (mode == MODE_THEME) {
+                if (Build.VERSION.SDK_INT >= 11) {
+                    // If we're running on API v11+ we will try and read android:spinnerMode
+                    TypedArray aa = null;
+                    try {
+                        aa = context.obtainStyledAttributes(attrs, ATTRS_ANDROID_SPINNERMODE,
+                                defStyleAttr, 0);
+                        if (aa.hasValue(0)) {
+                            mode = aa.getInt(0, MODE_DIALOG);
+                        }
+                    } catch (Exception e) {
+                        Log.i(TAG, "Could not read android:spinnerMode", e);
+                    } finally {
+                        if (aa != null) {
+                            aa.recycle();
+                        }
+                    }
+                } else {
+                    // Else, we use a default mode of dropdown
+                    mode = MODE_DROPDOWN;
+                }
+            }
+
+            if (mode == MODE_DROPDOWN) {
+                final TypedArray pa = getPopupContext().obtainStyledAttributes(attrs, R.styleable.Spinner, defStyleAttr, 0);
+                mPopupBackgroundResId = pa.getResourceId(R.styleable.Spinner_android_popupBackground, INVALID_ID);
+                pa.recycle();
+            }
+        }
+        a.recycle();
+
+        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
+        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setPopupBackgroundResource(@DrawableRes int resId) {
+        super.setPopupBackgroundResource(resId);
+        mPopupBackgroundResId = resId;
+        applyPopupBackground();
+    }
+
+    private void applyPopupBackground() {
+        mPopupBackgroundResId = checkResourceId(mPopupBackgroundResId);
+        if (mPopupBackgroundResId != INVALID_ID) {
+            setPopupBackgroundDrawable(SkinCompatResources.getDrawableCompat(getContext(), mPopupBackgroundResId));
+        }
+    }
+
+    @Override
+    public void applySkin() {
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.applySkin();
+        }
+        applyPopupBackground();
+    }
+
+}

+ 9 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatSupportable.java

@@ -0,0 +1,9 @@
+package skin.support.widget;
+
+/**
+ * Created by ximsfei on 2017/1/10.
+ */
+
+public interface SkinCompatSupportable {
+    void applySkin();
+}

+ 202 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatTextHelper.java

@@ -0,0 +1,202 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.support.annotation.DrawableRes;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.widget.TextView;
+
+import skin.support.R;
+import skin.support.content.res.SkinCompatResources;
+
+/**
+ * Created by ximsfei on 2017/1/10.
+ */
+
+public class SkinCompatTextHelper extends SkinCompatHelper {
+    private static final String TAG = SkinCompatTextHelper.class.getSimpleName();
+
+    public static SkinCompatTextHelper create(TextView textView) {
+        if (Build.VERSION.SDK_INT >= 17) {
+            return new SkinCompatTextHelperV17(textView);
+        }
+        return new SkinCompatTextHelper(textView);
+    }
+
+    final TextView mView;
+
+    private int mTextSizeResId = INVALID_ID;
+    private int mTextColorResId = INVALID_ID;
+    private int mTextColorHintResId = INVALID_ID;
+    protected int mDrawableBottomResId = INVALID_ID;
+    protected int mDrawableLeftResId = INVALID_ID;
+    protected int mDrawableRightResId = INVALID_ID;
+    protected int mDrawableTopResId = INVALID_ID;
+
+    public SkinCompatTextHelper(TextView view) {
+        mView = view;
+    }
+
+    public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
+        final Context context = mView.getContext();
+
+        // First read the TextAppearance style id
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SkinCompatTextHelper, defStyleAttr, 0);
+        final int ap = a.getResourceId(R.styleable.SkinCompatTextHelper_android_textAppearance, INVALID_ID);
+
+        if (a.hasValue(R.styleable.SkinCompatTextHelper_android_drawableLeft)) {
+            mDrawableLeftResId = a.getResourceId(R.styleable.SkinCompatTextHelper_android_drawableLeft, INVALID_ID);
+        }
+        if (a.hasValue(R.styleable.SkinCompatTextHelper_android_drawableTop)) {
+            mDrawableTopResId = a.getResourceId(R.styleable.SkinCompatTextHelper_android_drawableTop, INVALID_ID);
+        }
+        if (a.hasValue(R.styleable.SkinCompatTextHelper_android_drawableRight)) {
+            mDrawableRightResId = a.getResourceId(R.styleable.SkinCompatTextHelper_android_drawableRight, INVALID_ID);
+        }
+        if (a.hasValue(R.styleable.SkinCompatTextHelper_android_drawableBottom)) {
+            mDrawableBottomResId = a.getResourceId(R.styleable.SkinCompatTextHelper_android_drawableBottom, INVALID_ID);
+        }
+        a.recycle();
+
+        if (ap != INVALID_ID) {
+            a = context.obtainStyledAttributes(ap, R.styleable.SkinTextAppearance);
+            loadAttributes(a);
+        }
+
+        // Now read the style's values
+        a = context.obtainStyledAttributes(attrs, R.styleable.SkinTextAppearance, defStyleAttr, 0);
+        loadAttributes(a);
+        applySkin();
+    }
+
+    private void loadAttributes(TypedArray a) {
+        if (a.hasValue(R.styleable.SkinTextAppearance_android_textSize)) {
+            mTextSizeResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textSize, INVALID_ID);
+        }
+        if (a.hasValue(R.styleable.SkinTextAppearance_android_textColor)) {
+            mTextColorResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);
+        }
+        if (a.hasValue(R.styleable.SkinTextAppearance_android_textColorHint)) {
+            mTextColorHintResId = a.getResourceId(
+                    R.styleable.SkinTextAppearance_android_textColorHint, INVALID_ID);
+        }
+        a.recycle();
+    }
+
+    public void onSetTextAppearance(Context context, int resId) {
+        final TypedArray a = context.obtainStyledAttributes(resId, R.styleable.SkinTextAppearance);
+        loadAttributes(a);
+        applyTextColorResource();
+        applyTextColorHintResource();
+    }
+
+    private void applyTextColorHintResource() {
+        mTextColorHintResId = checkResourceId(mTextColorHintResId);
+        if (mTextColorHintResId == R.color.abc_hint_foreground_material_light) {
+            return;
+        }
+        if (mTextColorHintResId != INVALID_ID) {
+            // TODO: HTC_U-3u OS:8.0上调用framework的getColorStateList方法,有可能抛出异常,暂时没有找到更好的解决办法.
+            // issue: https://github.com/ximsfei/Android-skin-support/issues/110
+            try {
+                ColorStateList color = SkinCompatResources.getColorStateList(mView.getContext(), mTextColorHintResId);
+                mView.setHintTextColor(color);
+            } catch (Exception e) {
+            }
+        }
+    }
+
+    private void applyTextSizeResource() {
+        mTextSizeResId = checkResourceId(mTextSizeResId);
+//        if (mTextSizeResId == R.color.abc_primary_text_disable_only_material_light
+//                || mTextSizeResId == R.color.abc_secondary_text_material_light) {
+//            return;
+//        }
+        if (mTextSizeResId != INVALID_ID) {
+            try {
+                float dimension = SkinCompatResources.getDimension(mView.getContext(), mTextSizeResId);
+                mView.setTextSize(TypedValue.COMPLEX_UNIT_PX,dimension);
+            } catch (Exception e) {
+            }
+        }
+    }
+    private void applyTextColorResource() {
+        mTextColorResId = checkResourceId(mTextColorResId);
+        if (mTextColorResId == R.color.abc_primary_text_disable_only_material_light
+                || mTextColorResId == R.color.abc_secondary_text_material_light) {
+            return;
+        }
+        if (mTextColorResId != INVALID_ID) {
+            // TODO: HTC_U-3u OS:8.0上调用framework的getColorStateList方法,有可能抛出异常,暂时没有找到更好的解决办法.
+            // issue: https://github.com/ximsfei/Android-skin-support/issues/110
+            try {
+                ColorStateList color = SkinCompatResources.getColorStateList(mView.getContext(), mTextColorResId);
+                mView.setTextColor(color);
+            } catch (Exception e) {
+            }
+        }
+    }
+
+    public void onSetCompoundDrawablesRelativeWithIntrinsicBounds(
+            @DrawableRes int start, @DrawableRes int top, @DrawableRes int end, @DrawableRes int bottom) {
+        mDrawableLeftResId = start;
+        mDrawableTopResId = top;
+        mDrawableRightResId = end;
+        mDrawableBottomResId = bottom;
+        applyCompoundDrawablesRelativeResource();
+    }
+
+    public void onSetCompoundDrawablesWithIntrinsicBounds(
+            @DrawableRes int left, @DrawableRes int top, @DrawableRes int right, @DrawableRes int bottom) {
+        mDrawableLeftResId = left;
+        mDrawableTopResId = top;
+        mDrawableRightResId = right;
+        mDrawableBottomResId = bottom;
+        applyCompoundDrawablesResource();
+    }
+
+    protected void applyCompoundDrawablesRelativeResource() {
+        applyCompoundDrawablesResource();
+    }
+
+    protected void applyCompoundDrawablesResource() {
+        Drawable drawableLeft = null, drawableTop = null, drawableRight = null, drawableBottom = null;
+        mDrawableLeftResId = checkResourceId(mDrawableLeftResId);
+        if (mDrawableLeftResId != INVALID_ID) {
+            drawableLeft = SkinCompatResources.getDrawableCompat(mView.getContext(), mDrawableLeftResId);
+        }
+        mDrawableTopResId = checkResourceId(mDrawableTopResId);
+        if (mDrawableTopResId != INVALID_ID) {
+            drawableTop = SkinCompatResources.getDrawableCompat(mView.getContext(), mDrawableTopResId);
+        }
+        mDrawableRightResId = checkResourceId(mDrawableRightResId);
+        if (mDrawableRightResId != INVALID_ID) {
+            drawableRight = SkinCompatResources.getDrawableCompat(mView.getContext(), mDrawableRightResId);
+        }
+        mDrawableBottomResId = checkResourceId(mDrawableBottomResId);
+        if (mDrawableBottomResId != INVALID_ID) {
+            drawableBottom = SkinCompatResources.getDrawableCompat(mView.getContext(), mDrawableBottomResId);
+        }
+        if (mDrawableLeftResId != INVALID_ID
+                || mDrawableTopResId != INVALID_ID
+                || mDrawableRightResId != INVALID_ID
+                || mDrawableBottomResId != INVALID_ID) {
+            mView.setCompoundDrawablesWithIntrinsicBounds(drawableLeft, drawableTop, drawableRight, drawableBottom);
+        }
+    }
+
+    public int getTextColorResId() {
+        return mTextColorResId;
+    }
+
+    public void applySkin() {
+        applyCompoundDrawablesRelativeResource();
+        applyTextSizeResource();
+        applyTextColorResource();
+        applyTextColorHintResource();
+    }
+}

+ 97 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatTextHelperV17.java

@@ -0,0 +1,97 @@
+package skin.support.widget;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.RequiresApi;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+import skin.support.R;
+import skin.support.content.res.SkinCompatResources;
+
+/**
+ * Created by pengfengwang on 2017/3/8.
+ */
+
+@RequiresApi(17)
+@TargetApi(17)
+public class SkinCompatTextHelperV17 extends SkinCompatTextHelper {
+    private int mDrawableStartResId = INVALID_ID;
+    private int mDrawableEndResId = INVALID_ID;
+
+    public SkinCompatTextHelperV17(TextView view) {
+        super(view);
+    }
+
+    @Override
+    public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
+        final Context context = mView.getContext();
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SkinCompatTextHelper,
+                defStyleAttr, 0);
+        if (a.hasValue(R.styleable.SkinCompatTextHelper_android_drawableStart)) {
+            mDrawableStartResId = a.getResourceId(R.styleable.SkinCompatTextHelper_android_drawableStart, INVALID_ID);
+            mDrawableStartResId = SkinCompatHelper.checkResourceId(mDrawableStartResId);
+        }
+        if (a.hasValue(R.styleable.SkinCompatTextHelper_android_drawableEnd)) {
+            mDrawableEndResId = a.getResourceId(R.styleable.SkinCompatTextHelper_android_drawableEnd, INVALID_ID);
+            mDrawableEndResId = SkinCompatHelper.checkResourceId(mDrawableEndResId);
+        }
+        a.recycle();
+        super.loadFromAttributes(attrs, defStyleAttr);
+    }
+
+    public void onSetCompoundDrawablesRelativeWithIntrinsicBounds(
+            @DrawableRes int start, @DrawableRes int top, @DrawableRes int end, @DrawableRes int bottom) {
+        mDrawableStartResId = start;
+        mDrawableTopResId = top;
+        mDrawableEndResId = end;
+        mDrawableBottomResId = bottom;
+        applyCompoundDrawablesRelativeResource();
+    }
+
+    @Override
+    protected void applyCompoundDrawablesRelativeResource() {
+        Drawable drawableLeft = null, drawableTop = null, drawableRight = null, drawableBottom = null,
+                drawableStart = null, drawableEnd = null;
+        mDrawableLeftResId = checkResourceId(mDrawableLeftResId);
+        if (mDrawableLeftResId != INVALID_ID) {
+            drawableLeft = SkinCompatResources.getDrawableCompat(mView.getContext(), mDrawableLeftResId);
+        }
+        mDrawableTopResId = checkResourceId(mDrawableTopResId);
+        if (mDrawableTopResId != INVALID_ID) {
+            drawableTop = SkinCompatResources.getDrawableCompat(mView.getContext(), mDrawableTopResId);
+        }
+        mDrawableRightResId = checkResourceId(mDrawableRightResId);
+        if (mDrawableRightResId != INVALID_ID) {
+            drawableRight = SkinCompatResources.getDrawableCompat(mView.getContext(), mDrawableRightResId);
+        }
+        mDrawableBottomResId = checkResourceId(mDrawableBottomResId);
+        if (mDrawableBottomResId != INVALID_ID) {
+            drawableBottom = SkinCompatResources.getDrawableCompat(mView.getContext(), mDrawableBottomResId);
+        }
+        if (mDrawableStartResId != INVALID_ID) {
+            drawableStart = SkinCompatResources.getDrawableCompat(mView.getContext(), mDrawableStartResId);
+        }
+        if (drawableStart == null) {
+            drawableStart = drawableLeft;
+        }
+        if (mDrawableEndResId != INVALID_ID) {
+            drawableEnd = SkinCompatResources.getDrawableCompat(mView.getContext(), mDrawableEndResId);
+        }
+        if (drawableEnd == null) {
+            drawableEnd = drawableRight;
+        }
+        if (mDrawableLeftResId != INVALID_ID
+                || mDrawableTopResId != INVALID_ID
+                || mDrawableRightResId != INVALID_ID
+                || mDrawableBottomResId != INVALID_ID
+                || mDrawableStartResId != INVALID_ID
+                || mDrawableEndResId != INVALID_ID) {
+            mView.setCompoundDrawablesWithIntrinsicBounds(drawableStart, drawableTop, drawableEnd, drawableBottom);
+        }
+    }
+}

+ 81 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatTextView.java

@@ -0,0 +1,81 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.support.annotation.DrawableRes;
+import android.support.v7.widget.AppCompatTextView;
+import android.util.AttributeSet;
+
+/**
+ * Created by ximsfei on 2017/1/10.
+ */
+
+public class SkinCompatTextView extends AppCompatTextView implements SkinCompatSupportable {
+    private SkinCompatTextHelper mTextHelper;
+    private SkinCompatBackgroundHelper mBackgroundTintHelper;
+
+    public SkinCompatTextView(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatTextView(Context context, AttributeSet attrs) {
+        this(context, attrs, android.R.attr.textViewStyle);
+    }
+
+    public SkinCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
+        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
+        mTextHelper = SkinCompatTextHelper.create(this);
+        mTextHelper.loadFromAttributes(attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setBackgroundResource(@DrawableRes int resId) {
+        super.setBackgroundResource(resId);
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.onSetBackgroundResource(resId);
+        }
+    }
+
+    @Override
+    public void setTextAppearance(int resId) {
+        setTextAppearance(getContext(), resId);
+    }
+
+    @Override
+    public void setTextAppearance(Context context, int resId) {
+        super.setTextAppearance(context, resId);
+        if (mTextHelper != null) {
+            mTextHelper.onSetTextAppearance(context, resId);
+        }
+    }
+
+    @Override
+    public void setCompoundDrawablesRelativeWithIntrinsicBounds(
+            @DrawableRes int start, @DrawableRes int top, @DrawableRes int end, @DrawableRes int bottom) {
+        super.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
+        if (mTextHelper != null) {
+            mTextHelper.onSetCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
+        }
+    }
+
+    @Override
+    public void setCompoundDrawablesWithIntrinsicBounds(
+            @DrawableRes int left, @DrawableRes int top, @DrawableRes int right, @DrawableRes int bottom) {
+        super.setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
+        if (mTextHelper != null) {
+            mTextHelper.onSetCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
+        }
+    }
+
+    @Override
+    public void applySkin() {
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.applySkin();
+        }
+        if (mTextHelper != null) {
+            mTextHelper.applySkin();
+        }
+    }
+
+}

+ 113 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatToolbar.java

@@ -0,0 +1,113 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.Toolbar;
+import android.util.AttributeSet;
+
+import skin.support.R;
+import skin.support.content.res.SkinCompatResources;
+
+import static skin.support.widget.SkinCompatHelper.INVALID_ID;
+
+/**
+ * Created by ximsfei on 17-1-12.
+ */
+
+public class SkinCompatToolbar extends Toolbar implements SkinCompatSupportable {
+    private int mTitleTextColorResId = INVALID_ID;
+    private int mSubtitleTextColorResId = INVALID_ID;
+    private int mNavigationIconResId = INVALID_ID;
+    private SkinCompatBackgroundHelper mBackgroundTintHelper;
+
+    public SkinCompatToolbar(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatToolbar(Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, R.attr.toolbarStyle);
+    }
+
+    public SkinCompatToolbar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
+        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Toolbar, defStyleAttr, 0);
+        mNavigationIconResId = a.getResourceId(R.styleable.Toolbar_navigationIcon, INVALID_ID);
+
+        int titleAp = a.getResourceId(R.styleable.Toolbar_titleTextAppearance, INVALID_ID);
+        int subtitleAp = a.getResourceId(R.styleable.Toolbar_subtitleTextAppearance, INVALID_ID);
+        a.recycle();
+        if (titleAp != INVALID_ID) {
+            a = context.obtainStyledAttributes(titleAp, R.styleable.SkinTextAppearance);
+            mTitleTextColorResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);
+            a.recycle();
+        }
+        if (subtitleAp != INVALID_ID) {
+            a = context.obtainStyledAttributes(subtitleAp, R.styleable.SkinTextAppearance);
+            mSubtitleTextColorResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);
+            a.recycle();
+        }
+        a = context.obtainStyledAttributes(attrs, R.styleable.Toolbar, defStyleAttr, 0);
+        if (a.hasValue(R.styleable.Toolbar_titleTextColor)) {
+            mTitleTextColorResId = a.getResourceId(R.styleable.Toolbar_titleTextColor, INVALID_ID);
+        }
+        if (a.hasValue(R.styleable.Toolbar_subtitleTextColor)) {
+            mSubtitleTextColorResId = a.getResourceId(R.styleable.Toolbar_subtitleTextColor, INVALID_ID);
+        }
+        a.recycle();
+        applyTitleTextColor();
+        applySubtitleTextColor();
+        applyNavigationIcon();
+    }
+
+    private void applyTitleTextColor() {
+        mTitleTextColorResId = SkinCompatHelper.checkResourceId(mTitleTextColorResId);
+        if (mTitleTextColorResId != INVALID_ID) {
+            setTitleTextColor(SkinCompatResources.getColor(getContext(), mTitleTextColorResId));
+        }
+    }
+
+    private void applySubtitleTextColor() {
+        mSubtitleTextColorResId = SkinCompatHelper.checkResourceId(mSubtitleTextColorResId);
+        if (mSubtitleTextColorResId != INVALID_ID) {
+            setSubtitleTextColor(SkinCompatResources.getColor(getContext(), mSubtitleTextColorResId));
+        }
+    }
+
+    private void applyNavigationIcon() {
+        mNavigationIconResId = SkinCompatHelper.checkResourceId(mNavigationIconResId);
+        if (mNavigationIconResId != INVALID_ID) {
+            setNavigationIcon(SkinCompatResources.getDrawableCompat(getContext(), mNavigationIconResId));
+        }
+    }
+
+    @Override
+    public void setBackgroundResource(@DrawableRes int resId) {
+        super.setBackgroundResource(resId);
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.onSetBackgroundResource(resId);
+        }
+    }
+
+    @Override
+    public void setNavigationIcon(@DrawableRes int resId) {
+        super.setNavigationIcon(resId);
+        mNavigationIconResId = resId;
+        applyNavigationIcon();
+    }
+
+    @Override
+    public void applySkin() {
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.applySkin();
+        }
+        applyTitleTextColor();
+        applySubtitleTextColor();
+        applyNavigationIcon();
+    }
+
+}

+ 44 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatView.java

@@ -0,0 +1,44 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * Created by pengfengwang on 2017/1/13.
+ */
+
+public class SkinCompatView extends View implements SkinCompatSupportable {
+    private SkinCompatBackgroundHelper mBackgroundTintHelper;
+
+    public SkinCompatView(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public SkinCompatView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
+        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
+
+    }
+
+    @Override
+    public void setBackgroundResource(int resId) {
+        super.setBackgroundResource(resId);
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.onSetBackgroundResource(resId);
+        }
+    }
+
+    @Override
+    public void applySkin() {
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.applySkin();
+        }
+    }
+
+}

+ 40 - 0
skin-support/src/main/java/skin/support/widget/SkinCompatViewGroup.java

@@ -0,0 +1,40 @@
+package skin.support.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+
+public abstract class SkinCompatViewGroup extends ViewGroup implements SkinCompatSupportable {
+    private SkinCompatBackgroundHelper mBackgroundTintHelper;
+
+    public SkinCompatViewGroup(Context context) {
+        this(context, null);
+    }
+
+    public SkinCompatViewGroup(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public SkinCompatViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
+        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
+
+    }
+
+    @Override
+    public void setBackgroundResource(int resId) {
+        super.setBackgroundResource(resId);
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.onSetBackgroundResource(resId);
+        }
+    }
+
+    @Override
+    public void applySkin() {
+        if (mBackgroundTintHelper != null) {
+            mBackgroundTintHelper.applySkin();
+        }
+    }
+
+}