Android换肤技术

       所谓换肤技术,就是用户可以根据自己的喜好,选择自己喜欢的并且APP提供的颜色,背景图片,作为整个app的主题背景颜色,或者字体颜色等等...满足用户的需求。

APP换肤 主要分为2种:

1.内置换肤:就是将换肤所需要的图片资源,或者颜色资源,在打包的时候,打包到APK当中

2.皮肤包换肤:就是 当用户选择某一款皮肤的时候,动态从服务器下载打包好的皮肤包,然后解析出相应的图片资源,并替换出原有的图片,颜色,字体等等资源

优缺点

1.内置换肤:将所有皮肤包的资源打包到APK内部当中的话,如果皮肤包资源多,会增大APK包 的体积,所以预制在APK内部的的    皮肤包资源不能太多。

2.皮肤包换肤:可以动态改变下载或者删除皮肤包,不占用APK的大小,缺点就是第一次下载皮肤包的时候 需要下载,而且解析的时候 需要浪费时间,消耗流量

内置换肤

所谓的内置换肤,一般就是在app内部,内置几套皮肤包供用户选择,由于APK的体积有限,所以APK内部提供给用户选择的皮肤就不可能多,用户选择之后,将当前的Activity或者Fragment中各个需要替换的背景替换掉。对于内置换肤,由于内置皮肤包有限,一般开发人员会根据用户的选择,在代码中已经写好自己的switch case语句或者if else if语句来判断某个用户选择的那款内置的皮肤,根据选择修改即可。这里只做介绍不讲具体实现,因为很简单

例如 app中提供白天模式 和 夜间模式,这其实也是换肤的一种简单的实现方式。我们完全可以在app中设置某个全局变量,用户选择某个选项之后,去将背景变为白色或者黑色。

外置皮肤包换肤

      外置皮肤包换肤方式主要有两种实现方式,这里主要细讲第二种实现方式,因为我觉得第二种优于第一种。

实现方式一

     外置皮肤包换肤,就是当用户选择某一种皮肤之后,可以从服务器下载一个皮肤包,当皮肤包下载完成就立马更新当前Activity的背景或者相关图片,如果跳转到别的activity之后 也会展示最新皮肤包下的相关背景或者图片。

    主要思想就是利用SDK提供给我们的Application.ActivityLifecycleCallbacks接口可以检测各个Activity的生命周期:

public interface ActivityLifecycleCallbacks {
        void onActivityCreated(Activity activity, Bundle savedInstanceState);
        void onActivityStarted(Activity activity);
        void onActivityResumed(Activity activity);
        void onActivityPaused(Activity activity);
        void onActivityStopped(Activity activity);
        void onActivitySaveInstanceState(Activity activity, Bundle outState);
        void onActivityDestroyed(Activity activity);
    }

然后利用  自定义LayoutInflater.Factory2 的实现类,去修改拦截系统onCreateView方法,将当前Activity的所有控件记录下来,到需要换肤的时候去遍历所有控件的对应属性,并且去修改对应的属性值,达到换肤的目的。

     public void skinChange() {

            for (AttributeNameAndValue attributeNameAndValue : attributeNameAndValues) {
                switch (attributeNameAndValue.attrName) {
                    case "background":
                        Object background = SkinResources.getInstance().getBackground(attributeNameAndValue.attrValueInt);
                        if (background instanceof Integer) {
                            mView.setBackgroundColor((Integer) background);
                        } else {
                            // mView.setBackground((Drawable) background);
                            // 用兼容包的
                            ViewCompat.setBackground(mView, (Drawable) background);
                        }
                        break;

                    case "textColor":
                        TextView textView = (TextView) mView;
                        textView.setTextColor(SkinResources.getInstance().getColorStateList(attributeNameAndValue.attrValueInt));
                        break;

                    case "src":
                        Object src = SkinResources.getInstance().getBackground(attributeNameAndValue.attrValueInt);
                        if (src instanceof Integer) {
                            ((ImageView) mView).setImageDrawable(new ColorDrawable((Integer) src));
                        } else {
                            ((ImageView) mView).setImageDrawable((Drawable) src);
                        }
                    case "tint":
                        SkinResources skinRes = SkinResources.getInstance();
                        Log.d(TAG, "tint>>>>>>>>>>>" + attributeNameAndValue.attrValueInt + "  -- " + skinRes.getDefaultSkin());
                        break;
                }
            }
        }

上面这种方式就是每次一个新的Activity打开的时候,需要去遍历,记录当前Activity的所有控件,并且换肤的时候,有需要去遍历所有控件的对应的background,src,textColor....等属性,这里至少需要遍历2次,所以需要消耗性能,并且需要将Activity的内存都缓存起来,必然会增加内存。这种方式实现的Demo地址为 传送门 。

注意:这里,如果对于我们自定义的的View,如果我们不是系统的这些background,src属性值,这也那个这种方式就会难以实现,或者说对于这个switch ...  case .... 语句会由于开发人员越来越多,或者自定义控件越来越多,这个方法很难管理或者臃肿,不太友好

实现方式二

思路:换肤所需要的的目标皮肤包,其实就是一个Apk包。我们只需要将皮肤包中的用的图片的名称,颜色名称...与APP中所需要替换的图片资源,颜色资源的name相同就OK。正在运行的APP,通过Resources和AssetsManager可以得到当前控件中所使用到的颜色资源的name,背景图片的name,也可以得到这些资源对应的id ,例如R.drawable.***,R.color.***,,得到这些参数值之后,就可以通过插件的Resources和AssetsManager利用主app中获取到的name,id,将插件中的相同图片资源或者颜色资源取出来,最后更新目标控件的背景,颜色或者字体,就可以达到换肤的目的了。

先看换肤思想的由来,代码中涉及到的源码都是Android9.0(SDK28),所有思想都来源于源代码的阅读:

Activity.setContentView(layoutResID)-->getDelegate().setContentView(layoutResID)-->AppCompatDelegateImpl.setContentView

-->LayoutInflater.from(this.mContext).inflate(resId, contentParent)

-->PhoneLayoutInflater.inflate-->createViewFromTag(parent, name, context, attrs)-->

-->view = mFactory2.onCreateView(parent, name, context, attrs) 返回layoutResID中每一个view

-->思考如何mFactory2 是如何赋值的?

-->AppCompatActivity 的onCreate方法-->AppCompatDelegateImpl.installViewFactory();

-->LayoutInflaterCompat.setFactory2(layoutInflater, this); this就是AppCompatDelegateImpl 它实现了Factory2

-->那么上面的mFactory2 = AppCompatDelegateImpl

-->mFactory2.onCreateView = AppCompatDelegateImpl.onCreateView

-->AppCompatViewInflater.createView(...)

在AppCompatActivity中的onCreate方法中执行下面这个方法 赋值mFactory2
 public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(this.mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
        } else if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
            Log.i("AppCompatDelegate", "The Activity's LayoutInflater already has a Factory installed so we can not install AppCompat's");
        }

    }
 因此我们可以定义自己的Factory2 在onCreate方法之前调用,那么所有的view的创建都调用我们自己实现的factory2
 而Activity实现了Factory2接口,那么我们只需要在onCreate之前调用
 LayoutInflater layoutInflater = LayoutInflater.from(this);
 LayoutInflaterCompat.setFactory2(layoutInflater, this);
 再重写onCreateView方法,就可以拦截View的创建

上面的代码分析中我们需要定义自己的LayoutInflater来做和系统LayoutInflater加载xml,创建出不同的View。这里我们将自定义的LayoutInfalter 继承系统的 AppCompatViewInflater 这样可以不影响系统功能的情况下,实现自己的逻辑:

package com.android.skin.library.core;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatViewInflater;
import android.util.AttributeSet;
import android.view.View;

import com.android.skin.library.views.SkinnableButton;
import com.android.skin.library.views.SkinnableImageView;
import com.android.skin.library.views.SkinnableLinearLayout;
import com.android.skin.library.views.SkinnableRelativeLayout;
import com.android.skin.library.views.SkinnableTextView;

/**
 * 自定义控件加载器(可以考虑该类不被继承)
 */
public final class CustomAppCompatViewInflater extends AppCompatViewInflater {

    private String name; // 控件名
    private Context context; // 上下文
    private AttributeSet attrs; // 某控件对应所有属性

    public CustomAppCompatViewInflater(@NonNull Context context) {
        this.context = context;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAttrs(AttributeSet attrs) {
        this.attrs = attrs;
    }

    /**
     * @return 自动匹配控件名,并初始化控件对象
     */
    public View autoMatch() {
        View view = null;
        switch (name) {
            case "LinearLayout":
                // view = super.createTextView(context, attrs); // 源码写法
                view = new SkinnableLinearLayout(context, attrs);
                this.verifyNotNull(view, name);
                break;
            case "RelativeLayout":
                view = new SkinnableRelativeLayout(context, attrs);
                this.verifyNotNull(view, name);
                break;
            case "TextView":
                view = new SkinnableTextView(context, attrs);
                this.verifyNotNull(view, name);
                break;
            case "ImageView":
                view = new SkinnableImageView(context, attrs);
                this.verifyNotNull(view, name);
                break;
            case "Button":
                view = new SkinnableButton(context, attrs);
                this.verifyNotNull(view, name);
                break;
        }

        return view;
    }

    /**
     * 校验控件不为空(源码方法,由于private修饰,只能复制过来了。为了代码健壮,可有可无)
     *
     * @param view 被校验控件,如:AppCompatTextView extends TextView(v7兼容包,兼容是重点!!!)
     * @param name 控件名,如:"ImageView"
     */
    private void verifyNotNull(View view, String name) {
        if (view == null) {
            throw new IllegalStateException(this.getClass().getName() + " asked to inflate view for <" + name + ">, but returned null");
        }
    }
}

autoMatch中处理的方式和系统AppCompatViewInflater的 createView方法。

以其中一个自定义View SkinnableLinearLayout为例说明,如果想实现全部的activity或者控件换肤,我们就需要自定义对应的所有系统控件,这也是这种方法的弊端吧。

首先在attrs.xml文件中加入我们自定义的属性如下

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <!-- TextView控件属性 -->
    <declare-styleable name="SkinnableTextView">
        <attr name="android:background" />
        <attr name="android:textColor" />
        <attr name="custom_typeface"/>
    </declare-styleable>

    <!-- Button控件继承TextView,此处parent语法通过,但无效果,不像style.xml -->
    <declare-styleable name="SkinnableButton">
        <attr name="android:background" />
        <attr name="android:textColor" />
        <attr name="custom_typeface"/>
    </declare-styleable>

    <!-- ImageView控件特有属性 -->
    <declare-styleable name="SkinnableImageView">
        <attr name="android:src" />
    </declare-styleable>

    <!-- SkinnableLinearLayout控件 -->
    <declare-styleable name="SkinnableLinearLayout">
        <attr name="android:background" />
    </declare-styleable>

    <!-- SkinnableRelativeLayout控件 -->
    <declare-styleable name="SkinnableRelativeLayout">
        <attr name="android:background" />
    </declare-styleable>

    <!-- 后续可以自己拓展………………………………………………………………………… -->

</resources>

我们的思想就是在自定义LayoutInflator去加载xml文件创建出对应的控件的时候,去创建我们自定义的控件,自定义的控件完全继承系统控件(为了适配我们可以尽量继承Appcompact***的控件),只是实现了我们自定义的接口,这样我们就可以将关于background,src,textColor等属性解析出来,存放在自定义的实体类中保存,这样每个控件都保存着跟换肤相关的需要改变的 background,src,textColor 等属性,当换肤的时候,就直接替换这些属性的值就OK。具体的SkinnableLinearLayout的实现如下:

public class SkinnableLinearLayout extends LinearLayout implements ViewsMatch {

    private AttrsBean attrsBean;

    public SkinnableLinearLayout(Context context) {
        this(context, null);
    }

    public SkinnableLinearLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SkinnableLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        attrsBean = new AttrsBean();

        // 根据自定义属性,匹配控件属性的类型集合,如:background
        TypedArray typedArray = context.obtainStyledAttributes(attrs,
                R.styleable.SkinnableLinearLayout,
                defStyleAttr, 0);
        // 存储到临时JavaBean对象
        attrsBean.saveViewResource(typedArray, R.styleable.SkinnableLinearLayout);
        // 这一句回收非常重要!obtainStyledAttributes()有语法提示!!
        typedArray.recycle();
    }

    @Override
    public void skinnableView() {
        // 根据自定义属性,获取styleable中的background属性
        int key = R.styleable.SkinnableLinearLayout[R.styleable.SkinnableLinearLayout_android_background];
        // 根据styleable获取控件某属性的resourceId
        int backgroundResourceId = attrsBean.getViewResource(key);
        if(backgroundResourceId>0){
            if(SkinManager.getInstance().getIsDefaultSkin()){
                Drawable drawable = ContextCompat.getDrawable(getContext(),backgroundResourceId);
                setBackground(drawable);
            }else{
                Object obj = SkinManager.getInstance().getBackground(backgroundResourceId);
                if(obj instanceof Integer){  //说明是颜色
                    int color = (int)obj;
                    setBackgroundColor(color);
                }else{
                    Drawable drawable = (Drawable)obj;
                    setBackground(drawable);
                }
            }
        }
    }
}

三个参数的构造方法中,使用attrBean.saveViewResource方法就是将上面自定义SkinnableLinearLayout的attr:background属性抽取出来存放在AttrBean中,然后在skinnableView中获取对应的属性,修改其属性值达到换肤的目的.AttrBean的结构如下:


/**
 * 临时JavaBean对象,用于存储控件的key、value
 * 如:key:android:textColor, value:@Color/xxx
 * <p>
 * 思考:动态加载的场景,键值对是否存储SharedPreferences呢?
 */
public class AttrsBean {

    private SparseIntArray resourcesMap;
    private static final int DEFAULT_VALUE = -1;

    public AttrsBean() {
        resourcesMap = new SparseIntArray();
    }

    /**
     * 储控件的key、value
     *
     * @param typedArray 控件属性的类型集合,如:background / textColor
     * @param styleable  自定义属性,参考value/attrs.xml
     */
    public void saveViewResource(TypedArray typedArray, int[] styleable) {
        for (int i = 0; i < typedArray.length(); i++) {
            int key = styleable[i];
            int resourceId = typedArray.getResourceId(i, DEFAULT_VALUE);
            resourcesMap.put(key, resourceId);
        }
    }

    /**
     * 获取控件某属性的resourceId
     *
     * @param styleable 自定义属性,参考value/attrs.xml
     * @return 某控件某属性的resourceId
     */
    public int getViewResource(int styleable) {
        return resourcesMap.get(styleable);
    }
}

在resourcesMap中存放的是控件的属性和 控件属性值的资源id,例如上面的SkinnableLinearLayout中的AttrBean 中存放的key为

R.styleable.SkinnableLinearLayout[R.styleable.SkinnableLinearLayout_android_background]

而value为该background下的颜色值或者图片值,这个值必须在color.xml里面定义,或者图片放在drawable文件夹下面,这样系统就会在.R文件中生成对应的资源id。如R.color.xxx  R.drawable.xxx。然后我们根据资源id,获取到资源的名称,去皮肤包中找到对应名称的图片,或者对应名称的颜色值。这样就可以达到换肤目的(必须在color.xlm文件中或者drawable文件加下,否则找不到对应的文件名或者颜色名字)

要实现换肤,并且拦截系统加载xml创建View的过程,刚刚说了,需要快系统一步将Factory2赋值,这样加载XML文件实例化View的时候,就会用我们自己的Factory2中的onCreateView方法,而我们的Activity本身就实现了Factory2接口,所以就近取材,将Activity的实例this赋值给mFactory2,重写Activity的onCreateView方法,我们就可以在Activity的onCreateView做文章,自定义的LayoutInflator来创建View.所以我们定义一个基础类SkinActivity来实现这一操作,后续需要换肤的所有类都继承自这个Activity。

/**
 * 换肤Activity父类
 *
 * 用法:
 * 1、继承此类
 * 2、重写openChangeSkin()方法
 */
public class SkinActivity extends AppCompatActivity {

    private CustomAppCompatViewInflater viewInflater;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        LayoutInflater layoutInflater = LayoutInflater.from(this);
        LayoutInflaterCompat.setFactory2(layoutInflater, this);
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        if (openChangeSkin()) {
            if (viewInflater == null) {
                viewInflater = new CustomAppCompatViewInflater(context);
            }
            viewInflater.setName(name);
            viewInflater.setAttrs(attrs);
            View view = viewInflater.autoMatch();
            if(view !=null){
                return view;
            }
        }
        return super.onCreateView(parent, name, context, attrs);
    }

    /**
     * @return 是否开启换肤,增加此开关是为了避免开发者误继承此父类,导致未知bug
     */
    protected boolean openChangeSkin() {
        return false;
    }

    protected void updateSkin(String skinPath, int themeColorId) {
        SkinManager.getInstance().setSkinResource(skinPath);

        if (Build.VERSION.SDK_INT >= 21) {
            int themeColor = SkinManager.getInstance().getColor(themeColorId);
            // 换状态栏
            StatusBarUtils.forStatusBar(this,themeColor);
            // 换标题栏
            ActionBarUtils.forActionBar(this,themeColor);
            // 换底部导航栏
            NavigationUtils.forNavigation(this,themeColor);
        }

        View decorView = getWindow().getDecorView();
        applyDayNightForView(decorView);
    }

    protected void defaultSkin(String skinPath, int themeColorId){
        updateSkin(null,themeColorId);
    }

    /**
     * 回调接口 给具体控件换肤操作
     */
    protected void applyDayNightForView(View view) {
        if (view instanceof ViewsMatch) {
            ViewsMatch viewsMatch = (ViewsMatch) view;
            viewsMatch.skinnableView();
        }

        if (view instanceof ViewGroup) {
            ViewGroup parent = (ViewGroup) view;
            int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++) {
                applyDayNightForView(parent.getChildAt(i));
            }
        }
    }

在super.onCreate之前就设置mFactory2的值,mFactory2 = this = SkinActivity 重写onCreateView方法,在onCreateView中处理的办法就是,我们已经自定义的View,我们的CustomLayoutInflator能够处理的,就交给CustomLayoutInflator去处理,不能够处理的就交给super.onCretateView方法去处理,这样不会导致某些系统控件我们没重写,而显示不出来。在SkinActivity中还定义了一个openChangeSkin方法,用来控制当前页面是否需要换肤,用户可以继承SkinActivity自己来控制是否换肤。

准备工作都做好了,那么就是怎么去加载解析皮肤包的资源了,上面的分析,我们目前拿到了,每个View的对应属性的值,例如background属性值R.color.xxx或者R.drawable,现在需要把这些R.xxx.xxx代表的属性值,代表的图片名称,颜色name找出来,然后去皮肤包中找对应名称的图片,对应颜色name的value值,然后赋值给当前View的background。这样就大功告成了。具体实现在下面的SkinManager中,相关注释也很清晰:

public class SkinManager {
    private static final SkinManager ourInstance = new SkinManager();

    public static SkinManager getInstance() {
        return ourInstance;
    }

    private Application mContext;
    private volatile boolean isDefaultSkin = true;  //使用系统默认的皮肤
    private Resources mAppResource;
    private Resources mSkinResource;
    private String skinPackageName;   //皮肤包的包名
    private Map<String, SkinCache> resourcesMap = new HashMap<>();
    private SkinManager() {
    }

    public  void init(Application context){
        this.mContext = context;
        this.mAppResource = mContext.getResources();

    }

    public void setSkinResource(String skinPath){
        if(TextUtils.isEmpty(skinPath) || !new File(skinPath).exists()){
            isDefaultSkin = true;
            return;
        }
        if(resourcesMap.containsKey(skinPath)){
            isDefaultSkin = false;
            SkinCache skinCache = resourcesMap.get(skinPath);
            if(null!=skinCache){
                mSkinResource = skinCache.getSkinResource();
                skinPackageName = skinCache.getSkinPkgName();
                return;
            }
        }
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
            addAssetPath.setAccessible(true);
            addAssetPath.invoke(assetManager,skinPath);
            mSkinResource = new Resources(assetManager,mAppResource.getDisplayMetrics(),mAppResource.getConfiguration());
            skinPackageName = mContext.getPackageManager().getPackageArchiveInfo(skinPath,PackageManager.GET_ACTIVITIES).packageName;
            isDefaultSkin = TextUtils.isEmpty(skinPackageName);
            if(!isDefaultSkin){
                resourcesMap.put(skinPath,new SkinCache(skinPackageName,mSkinResource));
            }
        } catch (Exception e) {
            e.printStackTrace();
            isDefaultSkin = true;
        }
    }

    /**
     * 根据当前包中的resourceId得到皮肤包中 对应的名称的资源文件的resourceId
     * @param resourceId
     * @return
     */
    public int getIdentifier(int resourceId){
        if(isDefaultSkin){
            return resourceId;
        }
        String resourceEntryName = mAppResource.getResourceEntryName(resourceId);   //ic_launcher
        String resourceType= mAppResource.getResourceTypeName(resourceId);    //mimap /drawable
        return mSkinResource.getIdentifier(resourceEntryName,resourceType,skinPackageName);
    }

    public int getColor(int resId) {
        if (isDefaultSkin) { // 如果没有皮肤,那就加载当前App运行的Apk资源
            return mAppResource.getColor(resId);
        }
        int skinId = getIdentifier(resId);
        if (skinId == 0) { // 如果为0,那就加载当前App运行的Apk资源
            return mAppResource.getColor(resId);
        }
        // skinId不等于0 ,就加载 本地存储的 xxx.skin皮肤包资源
        return mSkinResource.getColor(skinId);
    }

    public ColorStateList getColorStateList(int resId) {
        if (isDefaultSkin) { // 如果没有皮肤,那就加载当前App运行的Apk资源
            return mAppResource.getColorStateList(resId);
        }
        int skinId = getIdentifier(resId);
        if (skinId == 0) { // 如果为0,那就加载当前App运行的Apk资源
            return mAppResource.getColorStateList(resId);
        }
        // skinId不等于0 ,就加载 本地存储的 xxx.skin皮肤包资源
        return mSkinResource.getColorStateList(skinId);
    }

    public ColorStateList getColorStateList2(int resId, int attrValueInt) {
        if (isDefaultSkin) { // 如果没有皮肤,那就加载当前App运行的Apk资源
            return mAppResource.getColorStateList(resId);
        }
        int skinId = getIdentifier(attrValueInt);
        if (skinId == 0) { // 如果为0,那就加载当前App运行的Apk资源
            return mAppResource.getColorStateList(attrValueInt);
        }
        // skinId不等于0 ,就加载 本地存储的 xxx.skin皮肤包资源
        return mSkinResource.getColorStateList(skinId);
    }

    public Drawable getDrawable(int resId) {
        //如果有皮肤  isDefaultSkin false 没有就是true
        if (isDefaultSkin) {
            return mAppResource.getDrawable(resId);
        }
        int skinId = getIdentifier(resId);
        if (skinId == 0) {
            return mAppResource.getDrawable(resId);
        }
        // skinId不等于0 ,就加载 本地存储的 xxx.skin皮肤包资源
        return mSkinResource.getDrawable(skinId);
    }


    public String getString(int resId) {
        try {
            if (isDefaultSkin) {  // 如果没有皮肤,那就加载当前App运行的Apk资源
                return mAppResource.getString(resId);
            }
            int skinId = getIdentifier(resId);
            if (skinId == 0) { // 如果为0,那就加载当前App运行的Apk资源
                return mAppResource.getString(skinId);
            }
            // skinId不等于0 ,就加载 本地存储的 xxx.skin皮肤包资源
            return mSkinResource.getString(skinId);
        } catch (Resources.NotFoundException e) {

        }
        return null;
    }


    /**
     * 获取background 是特殊情况,因为:
     * 可能是color
     * 可能是drawable
     * 可能是mipmap
     * 所有得到当前属性的类型Resources.getResourceTypeName(resId); 进行判断
     * @return
     */
    public Object getBackground(int resId) {
        String resourceTypeName = mAppResource.getResourceTypeName(resId);
        if (resourceTypeName.equals("color")) {
            return getColor(resId);
        } else if (resourceTypeName.equals("drawable") || resourceTypeName.equals("mipmap")) {
            // drawable or mipmap
            return getDrawable(resId);
        }
        return getColorStateList(resId);
    }


    /**
     * 获得字体
     * @param resId
     * @return
     */
    public Typeface getTypeface(int resId) {
        /**
         * 获取到了字符串,可能是 本地xxx.skin皮肤包资源的字符串 还是 当前App运行的Apk资源的字符串,这个不关注
         * 暂停了 ...
         */
        String skinTypefacePath = getString(resId);
        if (TextUtils.isEmpty(skinTypefacePath)) {
            return Typeface.DEFAULT;
        }
        try {
            Typeface typeface;
            if (isDefaultSkin) {
                typeface = Typeface.createFromAsset(mAppResource.getAssets(), skinTypefacePath);
                return typeface;

            }
            typeface = Typeface.createFromAsset(mSkinResource.getAssets(), skinTypefacePath);
            return typeface;
        } catch (RuntimeException e) {
        }
        return Typeface.DEFAULT;
    }

    public boolean getIsDefaultSkin() {
        return isDefaultSkin;
    }

}

SkinManager写好之后,我们只需要在用户下载完对应的皮肤包之后,通过调用setSkinResource方法将对应皮肤包相关Resource解析得到,然后就可以很快的实现换肤了。正常情况下是从服务下载下来皮肤包,解析然后唤起换肤。这里只是用本地apk包替换而已。

这种方式实现的换肤,虽然可能需要定义很多自定义控件,但是其实结构单一 只需要实现接口,开发人员自己自定义的控件,自己管理自己控件里面的换肤逻辑就OK,我们可以实现 任何控件任何自定义属性的换肤,并且不同的开发人员还相互不影响。开发的目的不是为了代码少。而是为了逻辑清晰,简单,可扩展性比较好。这是方式二优于方式一的地方。如下面的例子:

public class CustomCircleView extends View implements ViewsMatch {

    private Paint mTextPain;
    private AttrsBean attrsBean;

    public CustomCircleView(Context context) {
        this(context, null);
    }

    public CustomCircleView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomCircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        attrsBean = new AttrsBean();

        // 根据自定义属性,匹配控件属性的类型集合,如:circleColor
        TypedArray typedArray = context.obtainStyledAttributes(attrs,
                R.styleable.CustomCircleView,
                defStyleAttr, 0);

        int corcleColorResId = typedArray.getResourceId(R.styleable.CustomCircleView_circleColor, 0);

        // 存储到临时JavaBean对象
        attrsBean.saveViewResource(typedArray, R.styleable.CustomCircleView);
        // 这一句回收非常重要!obtainStyledAttributes()有语法提示!!
        typedArray.recycle();

        mTextPain = new Paint();
        mTextPain.setColor(getResources().getColor(corcleColorResId));
        //开启抗锯齿,平滑文字和圆弧的边缘
        mTextPain.setAntiAlias(true);
        //设置文本位于相对于原点的中间
        mTextPain.setTextAlign(Paint.Align.CENTER);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 获取宽度一半
        int width = getWidth() / 2;
        // 获取高度一半
        int height = getHeight() / 2;
        // 设置半径为宽或者高的最小值(半径)
        int radius = Math.min(width, height);
        // 利用canvas画一个圆
        canvas.drawCircle(width, height, radius, mTextPain);
    }

    @Override
    public void skinnableView() {
        // 根据自定义属性,获取styleable中的circleColor属性
        int key = R.styleable.CustomCircleView[0]; // = R.styleable.CustomCircleView_circleColor
        int resourceId = attrsBean.getViewResource(key);
        if (resourceId > 0) {
            if (SkinManager.getInstance().getIsDefaultSkin()) {
                int color = ContextCompat.getColor(getContext(), resourceId);
                mTextPain.setColor(color);
            } else {
                int color = SkinManager.getInstance().getColor(resourceId);
                mTextPain.setColor(color);
            }
        }
        invalidate();
    }
}

所有的自定义控件需要实现 ViewsMatch 接口,重写skinnableView()方法之后,才能实现换肤,上面CustomCircleView也不例外,这里添加了一个自定义属性:

<!-- 自定义控件属性 -->
    <declare-styleable name="CustomCircleView">
        <attr name="circleColor" format="color|integer" />
    </declare-styleable>

我们只需要自己去解析 自己需要的属性,并且解析存放在AttrBean中,然后实现自己的换肤逻辑。

Demo传送门

 

 

 

 

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值