插件式换肤的实现

1. 概述


接着上篇文章来分析,目前我们能够做到的就是能够获取另一个本地没有安装的apk的资源文件,而按道理来讲,我们的apk是需要从网上去下载的,但是这里我们为了做演示,就直接把一个 demo打成apk包,然后命名为 red.skin文件手动复制到 手机存储目录中,如果不是很清楚这个换肤 代码实现的,可以先去看我之前的文章

插件式换肤示例代码演示

2. 实现换肤后的效果


1>:每一次打开的都是新的皮肤;
2>:换肤之后所有的Activity里面的View都要换肤;
3>:每次重新进入app也需要换肤;

3. 解决方案


由上边换肤后的效果可知,我们在换肤后需要实现上边的3个效果,解决方案如下:
方案1>:把每一个 activity中需要换肤的 View都找出来,然后调用代码换肤,这个是最死板的方法;
方案2>:获取Activity里边的 根布局,然后遍历根布局,不断的循环获取子View,通过解析 tag,然后进行换肤;
<Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/test_btn"
        android:text="换肤"
        android:tag="skin:background:blue_color@@textColor:black_color"
        />
方案3>:拦截View的创建,这个目前是最好的方法,分析图如下
拦截View的创建实现换肤.png
由上图可知:

第一步:比如说在一个Activity中有多个控件,这里暂且就定为有Button、TextView、ImageView等3个控件;
第二步:在右边定义一个集合,用于保存Activity中这3个控件的实例对象;第三步:只要点击换肤按钮,就去遍历集合中的所有对象,该修改颜色的修改颜色,该修改背景的修改背景,就ok;

那么既然我们需要使用第三种方案,拦截View的创建,那么我们就首先要知道这些Activity的页面是如何创建的,那么下篇文章我会带大家来分析setContentView的源码,我们一起来探索 Activity的页面到底是如何创建的。

4. 具体思路


自己在测试时,是直接打包了一个red.skin皮肤包,然后复制到手机存储目录,然后直接调用换肤的方法,然后把这个皮肤包的路径传递就ok,但是在实际开发中,我们是需要从服务器端去下载皮肤包,然后把直接把路径传递给 换肤的方法loadSkin(path)就ok。

注意

这里的 red.skin包是一个apk文件,然后以.skin结尾命名的,apk中没有任何代码,只是在drawable中有一个和项目中需要换肤的原图的名字一模一样的图片,目的就是解析资源,然后达到换肤。

5. 具体代码如下:

1>:SkinAttr:皮肤的属性 比如设置背景、设置文本

/**
 * Email: 2185134304@qq.com
 * Created by JackChen 2018/4/15 18:06
 * Version 1.0
 * Params:
 * Description:    皮肤的属性  比如设置背景、设置文本
*/
public class SkinAttr {

    // 资源的名称
    private String mResName ;

    // 皮肤的类型,是需要换皮肤还是换背景
    private SkinType mSkinType ;

    public SkinAttr(String resName, SkinType skinType) {
        this.mResName = resName ;
        this.mSkinType = skinType ;
    }

    public void skin(View view) {
        mSkinType.skin(view , mResName) ;
    }
}

2>:SkinType:皮肤的类型

/**
 * Email: 2185134304@qq.com
 * Created by JackChen 2018/4/15 18:07
 * Version 1.0
 * Params:
 * Description:    皮肤的类型
*/
public enum  SkinType {

    TEXT_COLOR("textColor") {
        @Override
        public void skin(View view, String resName) {
            SkinResource skinResource = getSkinResource();
            ColorStateList color = skinResource.getColorByName(resName);
            if (color == null){
                return;
            }
            TextView textView = (TextView) view;
            textView.setTextColor(color);

        }
    }, BACKGROUND("background") {
        @Override
        public void skin(View view, String resName) {

            // 背景可能是图片,也可能是颜色
            SkinResource skinResource = getSkinResource();
            Drawable drawable = skinResource.getDrawableByName(resName);
            if (drawable != null){
                // 背景可能是图片
                ImageView imageView = (ImageView) view;
                imageView.setBackground(drawable);
            }

            // 背景可能是颜色
            ColorStateList color = skinResource.getColorByName(resName);
            if (color != null){
                // 这里直接设置默认颜色
                view.setBackgroundColor(color.getDefaultColor());
            }


        }
    }, SRC("src") {
        @Override
        public void skin(View view, String resName) {
            // 获取资源设置
            SkinResource skinResource = getSkinResource();
            Drawable drawable = skinResource.getDrawableByName(resName);
            if (drawable != null){
                ImageView imageView = (ImageView) view;
                imageView.setImageDrawable(drawable);
                return;
            }
        }
    };

    public SkinResource getSkinResource() {
        return SkinManager.getInstance().getSkinResource() ;
    }


    // 会根据传递的名字调用对应的方法
    private String mResName ;
    SkinType(String resName){
        this.mResName = resName ;
    }
    public abstract void skin(View view, String resName) ;

    public String getResName() {
        return mResName;
    }
}

3>:SkinView:皮肤的 各种 View,比如Button、TextView、ImageView

/**
 * Email: 2185134304@qq.com
 * Created by JackChen 2018/4/15 18:05
 * Version 1.0
 * Params:
 * Description:    皮肤的 各种 View,比如Button、TextView、ImageView
*/

public class SkinView {
    private View mView ;
    private List<SkinAttr> mSkinAttrs ;

    public SkinView(View view, List<SkinAttr> skinAttrs) {
        this.mView = view ;
        this.mSkinAttrs = skinAttrs ;
    }

    public void skin(){
        for (SkinAttr attr : mSkinAttrs) {
            attr.skin(mView) ;
        }
    }
}

4>:ISkinChangeListener:换肤的回调接口

/**
 * Email: 2185134304@qq.com
 * Created by JackChen 2018/4/21 11:08
 * Version 1.0
 * Params:
 * Description:   换肤的回调接口
*/
public interface ISkinChangeListener {
    void changeSkin(SkinResource skinResource) ;
}

5>:SkinConfig:一些配置

/**
 * Email: 2185134304@qq.com
 * Created by JackChen 2018/4/21 10:00
 * Version 1.0
 * Params:
 * Description:
*/
public class SkinConfig {
    // SP的文件名称
    public static final String SKIN_INFO_NAME = "skinInfo";

    // 保存皮肤文件的路径的名称
    public static final String SKIN_PATH_NAME = "skinPath";

    // 不需要改变任何东西
    public static final int SKIN_CHANGE_NOTHING = -1;

    // 换肤成功
    public static final int SKIN_CHANGE_SUCCESS = 1;

    // 皮肤文件不存在
    public static final int SKIN_FILE_NOEXSIST = -2;

    // 皮肤文件有错误可能不是一个apk文件
    public static final int SKIN_FILE_ERROR = -3;
}

6>:SkinPreUtils:相关工具类

/**
 * Email: 2185134304@qq.com
 * Created by JackChen 2018/4/21 10:01
 * Version 1.0
 * Params:
 * Description:
*/
public class SkinPreUtils {

    private static SkinPreUtils mInstance;
    private Context mContext;


    /**
     * 这里的context必须调用 context.getApplicationContext() 否则内存泄露
     */
    private SkinPreUtils(Context context){
        this.mContext = context.getApplicationContext();
    }

    public static SkinPreUtils getInstance(Context context){
        if(mInstance == null){
            synchronized (SkinPreUtils.class){
                if(mInstance == null){

                }  mInstance = new SkinPreUtils(context);
            }
        }
        return mInstance;
    }


    /**
     * 保存当前皮肤路径
     * @param skinPath
     */
    public void saveSkinPath(String skinPath){
        mContext.getSharedPreferences(SkinConfig.SKIN_INFO_NAME,Context.MODE_PRIVATE)
                .edit().putString(SkinConfig.SKIN_PATH_NAME,skinPath).commit();
    }

    /**
     * 获取皮肤的路径
     * @return  当前皮肤路径
     */
    public String getSkinPath(){
        return mContext.getSharedPreferences(SkinConfig.SKIN_INFO_NAME,Context.MODE_PRIVATE)
                .getString(SkinConfig.SKIN_PATH_NAME,"");
    }

    /**
     * 清空皮肤路径
     */
    public void clearSkinInfo() {
        saveSkinPath("");
    }
}

7>:SkinAppCompatViewInflater:这个是为了兼容 5.0的属性 ,如果不兼容,可能会有一些问题

/**
 * Email: 2185134304@qq.com
 * Created by JackChen 2018/4/15 18:39
 * Version 1.0
 * Params:
 * Description:    这个是为了兼容 5.0的属性 ,如果不兼容,可能会有一些问题
*/
import android.content.Context;
import android.content.ContextWrapper;
import android.content.res.TypedArray;
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.view.ContextThemeWrapper;
import android.support.v7.widget.AppCompatAutoCompleteTextView;
import android.support.v7.widget.AppCompatButton;
import android.support.v7.widget.AppCompatCheckBox;
import android.support.v7.widget.AppCompatCheckedTextView;
import android.support.v7.widget.AppCompatEditText;
import android.support.v7.widget.AppCompatImageButton;
import android.support.v7.widget.AppCompatImageView;
import android.support.v7.widget.AppCompatMultiAutoCompleteTextView;
import android.support.v7.widget.AppCompatRadioButton;
import android.support.v7.widget.AppCompatRatingBar;
import android.support.v7.widget.AppCompatSeekBar;
import android.support.v7.widget.AppCompatSpinner;
import android.support.v7.widget.AppCompatTextView;
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;

/**
 * This class is responsible for manually inflating our tinted widgets which are used on devices
 * running {@link android.os.Build.VERSION_CODES#KITKAT KITKAT} or below. As such, this class
 * should only be used when running on those devices.
 * <p>This class two main responsibilities: the first is to 'inject' our tinted views in place of
 * the framework versions in layout inflation; the second is backport the {@code android:theme}
 * functionality for any inflated widgets. This include theme inheritance from it's parent.
 */
public class SkinAppCompatViewInflater {

    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 LOG_TAG = "AppCompatViewInflater";

    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) {
        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);
        }

        View view = null;

        // We need to 'inject' our tint aware Views in place of the standard framework versions
        switch (name) {
            case "TextView":
                view = new AppCompatTextView(context, attrs);
                break;
            case "ImageView":
                view = new AppCompatImageView(context, attrs);
                break;
            case "Button":
                view = new AppCompatButton(context, attrs);
                break;
            case "EditText":
                view = new AppCompatEditText(context, attrs);
                break;
            case "Spinner":
                view = new AppCompatSpinner(context, attrs);
                break;
            case "ImageButton":
                view = new AppCompatImageButton(context, attrs);
                break;
            case "CheckBox":
                view = new AppCompatCheckBox(context, attrs);
                break;
            case "RadioButton":
                view = new AppCompatRadioButton(context, attrs);
                break;
            case "CheckedTextView":
                view = new AppCompatCheckedTextView(context, attrs);
                break;
            case "AutoCompleteTextView":
                view = new AppCompatAutoCompleteTextView(context, attrs);
                break;
            case "MultiAutoCompleteTextView":
                view = new AppCompatMultiAutoCompleteTextView(context, attrs);
                break;
            case "RatingBar":
                view = new AppCompatRatingBar(context, attrs);
                break;
            case "SeekBar":
                view = new AppCompatSeekBar(context, attrs);
                break;
        }

        if (view == null) {
            // If the original context does not equal our themed context, then we need to manually
            // inflate it using the name so that android:theme takes effect.
            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 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('.')) {
                // try the android.widget prefix first...
                return createView(context, name, "android.widget.");
            } 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 (!ViewCompat.hasOnClickListeners(view) || !(context instanceof ContextWrapper)) {
            // Skip our compat functionality if: the view doesn't have an onClickListener,
            // or the Context isn't a ContextWrapper
            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, android.support.v7.appcompat.R.styleable.View, 0, 0);
        int themeId = 0;
        if (useAndroidTheme) {
            // First try reading android:theme if enabled
            themeId = a.getResourceId(android.support.v7.appcompat.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(android.support.v7.appcompat.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);
        }
    }
}

8>:SkinAttrSupport:解析皮肤属性的支持类

/**
 * Email: 2185134304@qq.com
 * Created by JackChen 2018/4/15 18:04
 * Version 1.0
 * Params:
 * Description:    解析皮肤属性的支持类
*/
public class SkinAttrSupport {


    /**
     * 获取SkinView的属性
     */
    public static List<SkinAttr> getSkinAttrs(Context context, AttributeSet attrs) {
        // 解析这3个属性  src、background、textColor
        List<SkinAttr> skinAttrs = new ArrayList<>() ;
        int attrLength = attrs.getAttributeCount();
        for (int i = 0; i < attrLength; i++) {

            // 比如 android:src="@drawable/image_src"
            // 获取名称,就是上边的src    比如是布局文件中的 id、src、style、layout_width、layout_height等等
            String attrName = attrs.getAttributeName(i);
            // 获取值 ,就是上边的 @drawable/image_src  #ffffff
            String attrValue = attrs.getAttributeValue(i) ;

            // 只获取重要的属性
            SkinType skinType = getSkinType(attrName) ;
            if (skinType != null){

                // 资源名称  目前只有 attrValue 并且是一个 @ 开头,int类型
                String resName = getResName(context , attrValue) ;

                if (TextUtils.isEmpty(resName)){
                    // 跳出本次循环
                    continue;
                }
                SkinAttr skinAttr = new SkinAttr(resName , skinType) ;
                skinAttrs.add(skinAttr) ;
            }
        }

        return skinAttrs;

    }


    /**
     * 获取资源的名称
     */
    private static String getResName(Context context, String attrValue) {

        // 这里需要判断 值 是否是以 "@" 符号开头
        if (attrValue.startsWith("@")){
            attrValue = attrValue.substring(1) ;

            int resId = Integer.parseInt(attrValue) ;

            return context.getResources().getResourceEntryName(resId) ;
        }
        return null;
    }


    /**
     * 通过名称获取 SkinType
     */
    private static SkinType getSkinType(String attrName) {
        SkinType[] skinTypes = SkinType.values();
        for (SkinType skinType : skinTypes) {

            if (skinType.getResName().equals(attrName)){
                return skinType ;
            }
        }

        return null;
    }
}

9>:SkinManager:皮肤的管理类

/**
 * Email: 2185134304@qq.com
 * Created by JackChen 2018/4/15 18:03
 * Version 1.0
 * Params:
 * Description:  皮肤的管理类
*/

public class SkinManager {

    private Context mContext ;

    private static Map<ISkinChangeListener , List<SkinView>> mSkinViews = new HashMap<>() ;

    private SkinResource mSkinResource ;


    private SkinManager(){}
    private volatile static SkinManager mInstance ;
    public static SkinManager getInstance() {
        if (mInstance == null){
            synchronized (SkinManager.class){
                if (mInstance == null){
                    mInstance = new SkinManager() ;
                }
            }
        }
        return mInstance ;
    }


    /**
     * 这里使用 getApplicationContext()防止内存泄露
     */
    public void init(Context context){
        this.mContext = context.getApplicationContext() ;

        // 每一次打开皮肤都会到这里来,防止皮肤被任意删除,需要做一些措施
        // 1. 获取当前皮肤的路径
        String currentSkinPath = SkinPreUtils.getInstance(context).getSkinPath() ;
        File file = new File(currentSkinPath) ;

        // 如果文件路径不存在,就清空皮肤
        if (!file.exists()){
            SkinPreUtils.getInstance(context).clearSkinInfo();
            return;
        }

        // 最好做一下,能否获取到包名
        String packageName = context.getPackageManager().getPackageArchiveInfo(currentSkinPath , PackageManager.GET_ACTIVITIES).packageName;
        if (TextUtils.isEmpty(packageName)){
            SkinPreUtils.getInstance(context).clearSkinInfo();
            return;
        }


        // 最好校验一下签名

        // 如果文件路径存在,就做一些初始化的工作
        mSkinResource  = new SkinResource(context , currentSkinPath) ;
    }


    /**
     * 加载皮肤
     */
    public int loadSkin(String skinPath) {


        // 判断1:如果皮肤不存在,就清空皮肤
        File file = new File(skinPath) ;
        if (!file.exists()){
            // 如果皮肤不存在,就清空皮肤
            return SkinConfig.SKIN_FILE_NOEXSIST;
        }

        // 判断2:最好做一下  能不能获取到包名
        String packageName = mContext.getPackageManager().getPackageArchiveInfo(
                skinPath, PackageManager.GET_ACTIVITIES).packageName;

        if(TextUtils.isEmpty(packageName)){
            return SkinConfig.SKIN_FILE_ERROR;
        }

        // 判读3. 判断当前的皮肤如果一样,就不要换肤
        String currentSkinPath = SkinPreUtils.getInstance(mContext).getSkinPath() ;

        if (skinPath.equals(currentSkinPath)){
            return SkinConfig.SKIN_CHANGE_NOTHING;
        }


        // 校验签名  在增量更新

        // 初始化资源管理
        mSkinResource = new SkinResource(mContext , skinPath) ;

        // 改变皮肤
        changeSkin() ;

        return SkinConfig.SKIN_CHANGE_SUCCESS ;
    }


    /**
     * 改变皮肤
     */
    private void changeSkin() {
        Set<ISkinChangeListener> keys = mSkinViews.keySet() ;
        for (ISkinChangeListener key : keys) {
            List<SkinView> skinViews = mSkinViews.get(key);
            for (SkinView skinView : skinViews) {
                skinView.skin();
            }

            // 通知Activity
            key.changeSkin(mSkinResource);
        }

    }


    /**
     * 保存当前皮肤状态
     */
    private void saveSkinStatus(String skinPath) {
        SkinPreUtils.getInstance(mContext).saveSkinPath(skinPath);
    }


    /**
     * 获取当前皮肤资源的管理
     */
    public SkinResource getSkinResource() {
        return mSkinResource;
    }

    /**
     * 恢复默认
     */
    public int restoredDefault() {

        // 判断当前有没有皮肤,如果没有皮肤,就不要往下边执行
        String currentSkinPath = SkinPreUtils.getInstance(mContext).getSkinPath() ;
        if (TextUtils.isEmpty(currentSkinPath)){
            return SkinConfig.SKIN_CHANGE_NOTHING;
        }

        // 当前手机运行好的app的apk路径
        String skinPath = mContext.getPackageResourcePath() ;
        // 初始化资源管理
        mSkinResource = new SkinResource(mContext , skinPath) ;


        // 每次加载皮肤后,就保存当前的皮肤,只需要保存传递的路径即可
        saveSkinStatus(skinPath) ;
        // 改变皮肤
        changeSkin();

        // 清空皮肤信息
        SkinPreUtils.getInstance(mContext).clearSkinInfo();
        return SkinConfig.SKIN_CHANGE_SUCCESS;
    }


    /**
     * 通过 Activity获取 SkinView
     */
    public List<SkinView> getSkinViews(Activity activity) {
        return mSkinViews.get(activity) ;
    }


    /**
     * 注册
     */
    public void register(ISkinChangeListener skinChangeListener, List<SkinView> skinViews) {
        mSkinViews.put(skinChangeListener , skinViews) ;
    }


    /**
     * 检测是否需要换肤
     */
    public void checkChangeSkin(SkinView skinView) {
        // 如果当前有皮肤,也就是说保存了皮肤的路径,就去换一下皮肤
        String currentSkinPath = SkinPreUtils.getInstance(mContext).getSkinPath() ;
        if (!TextUtils.isEmpty(currentSkinPath)){
            // 如果当前皮肤路径不为空,就换肤
            skinView.skin();
        }
    }


    /**
     * 防止内存泄露
     */
    public void unregister(ISkinChangeListener skinChangeListener) {
        mSkinViews.remove(skinChangeListener) ;
    }
}

10>:SkinResource:皮肤的资源管理

/**
 * Email: 2185134304@qq.com
 * Created by JackChen 2018/4/15 18:04
 * Version 1.0
 * Params:
 * Description:    皮肤的资源管理
*/
public class SkinResource {


    // 所有的资源都是通过这个获取 , 主要获取的就是 textName、textColor
    private Resources mSkinResources;
    private String mPackageName;

    public SkinResource(Context context , String skinPath) {
        try {
                    // 读取本地的一个 .skin里面的资源
                    Resources superRes = context.getResources() ;

                    // 创建AssetsManager
                    // 不能直接 new AssetManager() ;
                    // 通过反射来创建 asset对象
                    AssetManager asset = AssetManager.class.newInstance() ;
                    // 添加本地下载好的 资源皮肤,就是复制到 手机目录中的 red.skin
                    // 不能直接调用 addAssetPath()方法,只能通过反射调用 该方法
                    // 参数1:表示方法名称  参数2:表示方法里边的参数类型 如果是String path -> String.class int path -> int.class 等等

                    Method method = AssetManager.class.getDeclaredMethod("addAssetPath" , String.class) ;
                    method.setAccessible(true);  // 设置权限,防止addAssetPath()方法是私有private的

                    /*// 反射执行addAssetPath()方法   File.separator就和 "/" 是一样的
                    method.invoke(asset , Environment.getExternalStorageDirectory().getAbsolutePath() +
                            File.separator + "red.skin") ;*/

            // 反射执行addAssetPath()方法   File.separator就和 "/" 是一样的  路径不能写死,只能是传递进来的path
            method.invoke(asset , skinPath) ;



            mSkinResources = new Resources(asset , superRes.getDisplayMetrics() , superRes.getConfiguration());


            // 获取包名
            mPackageName = context.getPackageManager().getPackageArchiveInfo(skinPath , PackageManager.GET_ACTIVITIES).packageName;

                } catch (Exception e) {
                    e.printStackTrace();
                }
    }


    /**
     * 通过名字获取 drawable图片
     * @param resName
     * @return
     */
    public Drawable getDrawableByName(String resName){
        try {
            // 参数1:资源名称  参数2:资源类型  参数3:包名
            int resId = mSkinResources.getIdentifier(resName , "drawable" , mPackageName) ;
            Drawable drawable = mSkinResources.getDrawable(resId) ;
            return drawable ;
        } catch (Exception e) {
            e.printStackTrace();
            return null ;
        }

    }


    /**
     * 通过名字获取颜色
     * @param resName
     * @return
     */
    public ColorStateList getColorByName(String resName){

        try {
            int resId = mSkinResources.getIdentifier(resName , "color" , mPackageName) ;
            ColorStateList color = mSkinResources.getColorStateList(resId) ;
            return color ;
        } catch (Exception e) {
            e.printStackTrace();
            return null ;
        }
    }
}

具体测试如下:

如果是在真正的开发中,皮肤包是直接从服务器中下载,然后把路径传递给loadSkin()方法即可,这里为了测试,就自己直接把 red.skin皮肤包放到手机存储目录下,然后就直接获取该皮肤包的一个apk的路径,然后直接传递给 loadSkin()方法,然后点击换肤就可以把red.skin包中drawable下的图片读取出来替换我们本地的图片,然后达到换肤的目的。


图片.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容