Android动态换肤框架实现

今天介绍一下Android 中的常用的换肤策略,同时动手实现一个动态换肤的框架

先上效果图:

 

换肤概念

 
换肤: 在android中是指 对 文字、 颜色、 图片 等的资源的更换。
: 对应于现实生活中,就是我们的 肤色 、 衣服 等的更换。
 
有什么好处或者说 目的是什么 ??
对应于我们android 中呢,就是 可以 满足用户的新鲜感,可以提高用户的留存率,当然 皮肤也可以进行收费。 这就好比我们生活中 去旅行时,有普通的酒店, 也有很多主题酒店,有各种各样的…, 你会去哪个呢?

 

换肤实现方式

在我们的开发中, 一般常用的有两种换肤的方式:
 

第一种: 静态换肤:

这种换肤的方式,也就是我们所说的内置换肤,这种方式我也用过很多次,就是在APP内部放置多套相同的资源。进行资源的切换(这种方式,我想你也用过,当然,我们今天并不是要说这种). 这种换肤的方式有很多缺点,比如, 灵活性差,只能更换内置的资源、apk体积太大,在我们的应用Apk中等一般图片文件能占到apk大小的一半左右。
 
当然了,这种方式也并不是一无是处, 任何事物的存在,必然有其理由。比如我们的应用内,只是普通的 日夜间模式 的切换,并不需要图片等的更换,只是更换颜色,那这样的方式就很实用。

 
啰嗦了半天… 是时候开始正式进入我们今天的主题.
 
~
~
~
Ok .

第二种:动态换肤

怎么个动态法呢? 即在运行时动态的加载皮肤包
 
这么说可能有点不是很好理解,大白话就是,当控件初始化完毕,需要设置内容(图片颜色 等)时,动态的去我们对应的皮肤包去找。
 
~ 我这么说你可能觉的这跟静态换肤没啥不同都是 需要去 set
 
我们这里呢,强调两点 :
1.动态 ,即灵活性比较高,可以随意的切换 , 当然这是基于第二点上 .

2.皮肤包,这个和apk是分离开来的,我们可以去网络,去服务器获取到皮肤包,想怎么换就怎么换。
 
是时候开始表演真正的技术了, 哈哈~~~
 
在开始之前,我们先重申一点: 我们今天要做的是框架。 所以我们要站在框架开发者的角度, 怎么让使用这更加方便、更加简洁的使用。
 
Ok, 开始 !
 
先总结一下我们的整体步骤:
1.采集控件 获取需要换肤的控件。(要换肤,你总得拿到或者知道要换哪些控件吧)
2.加载皮肤包 加载我们的外置皮肤包。
3.换肤 这一步呢,我们需要控件与皮肤包资源的匹配,进行换肤.
 

采集控件

我们需要去拿到每一个页面的 所有需要换肤的控件。你可能会想到在每个ActivityOnCreate()中去获取,然后保存到集合中。 stop! ~~~ ok 我们要做的是一个框架,要做到尽可能少的去 侵入用户的代码。 下面这个 api, 可以拿到Activity的回调。
 

application.registerActivityLifecycleCallbacks(参数)

 
这个方法可以获取到Activity的生命周期回调 ActivityLifecycleCallbacks。
 
ok,下面我们来点实际的。
 
我们新建一个类,用来做初始化,application 中初始化。
 


public class SkinChangeManager { 
   private static final String TAG = SkinChangeManager.class.getSimpleName(); 
   private static SkinChangeManager _Instance = null;  
   private static Application mContext;    
   private ActivityCallbacks mActivityCallBacks;   
   public static void init(Application context) {  
          mContext = context;       
         synchronized (SkinChangeManager.class) {   
             if (_Instance == null) {         
               _Instance = new SkinChangeManager();    
            }     
          } 
      }  
  public SkinChangeManager() {  
    SkinPreference.init(mContext); // sp 保存 
    SkinResources.init(mContext);  // 资源的获取    
    //注册activity的回调 此方法运行在 super.xxx 之前  
    mActivityCallBacks = new ActivityCallbacks();
    mContext.registerActivityLifecycleCallbacks(mActivityCallBacks);
    }    

  public static SkinChangeManager getInstance() { 
       return _Instance;    
  }

 
ok, 在构造方法 的初始化中,我们 创建了 ActivityLifecycleCallbacks 的实例,并进行了注册 registerActivityLifecycleCallbacks .

 
下面是 ActivityCallbacks的代码:


@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {

}

@Override
public void onActivityStarted(Activity activity) {

}

@Override
public void onActivityResumed(Activity activity) {

}
    ...

 
你或许已经猜到了, 我们要搞点事的地方,没错,就是她 onActivityCreated

 
ok~~~,接下来我们要拿到所有的View 。这下才是重点。
 
需要拿到View,我们就要知道setContentView是干了什么事。

 
简单描述下流程:

setContentView -> window.setContentView()(实现类是PhoneWindow)->mLayoutInflater.inflate() -> inflate .. ->createViewFromTag().

 
下面这段是 系统 createViewFromTag() 中的部分代码:


    View view;
    if (mFactory2 != null) {
        view = mFactory2.onCreateView(parent, name, context, attrs);
    } else if (mFactory != null) {
        view = mFactory.onCreateView(name, context, attrs);
    } else {
        view = null;
    }

    if (view == null && mPrivateFactory != null) {
        view = mPrivateFactory.onCreateView(parent, name, context, attrs);
    }

    if (view == null) {
        final Object lastContext = mConstructorArgs[0];
        mConstructorArgs[0] = context;
        try {
            if (-1 == name.indexOf('.')) {
                view = onCreateView(parent, name, attrs);
            } else {
                view = createView(name, null, attrs);
            }
        } finally {
            mConstructorArgs[0] = lastContext;
        }
    }

return view;

 
ok,非常简单,首先判断了mFactory2mFactory 是否为null。然后,判断是否是系统View if (-1 ==
name.indexOf('.'))
那么 拼接前缀进行反射创建实例,如果是自定义的View ,则直接去创建了实例。
 
这里我们的思路是 View部分的创建我们自己来完成.
即给LayoutInflater 设置一个 Factory 我们模仿系统的实现来给系统返回一个View,当然我们也可以在中间做一些我们想做的操作。


@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {

    LayoutInflater inflater = LayoutInflater.from(activity);

    SkinViewFactory skinFactory = new SkinViewFactory(activity,mSkinTypeface);

    inflater.setFactory2(skinFactory);

}

 
ok, 我们来看 SkinFactory的实现:


public class SkinViewFactory implements LayoutInflater.Factory2{

    private Activity mActivity;
    private SkinAttribute mSkinAttrs;   // 此View 用来存储 所有我们需要换肤的view

    private static final Class<?>[] mConstructorSignature = new Class[]{
            Context.class, AttributeSet.class}; // 两个参数的签名


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

    public SkinViewFactory(Activity activity, Typeface mSkinTypeface) {
        this.mActivity = activity;
        mSkinAttrs = new SkinAttribute(mSkinTypeface);
    }

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

        //自定义view的情况  这里是全类名
        if (view == null) {
            view = createView(name, context, attrs);
        }

        //view 创建完毕了, 我们要从其中晒选出我们需要 换肤的view
        mSkinAttrs.load(view,attrs);
        return view;
    }

    private View createViewFromTag(String name, Context context, AttributeSet attrs) {

        View view = null;
        if (-1 != name.indexOf('.')) {  // 包含了. 自定义view
            return null;
        }

        for (int i = 0; i < mClassPrefixList.length; i++) {     // 拼接 然后 生成view
            view = createView(mClassPrefixList[i] + name, context, attrs);
            if (view != null) {
                break;
            }
        }
        return view;
    }

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

        if (null != constructor) {
            try {
                return constructor.newInstance(context, attrs);

            } catch (InstantiationException e) {

            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

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

 
SkinFactory 中我们做了两件事:

  1. View的创建并返回,由系统来进行后续的操作
  2. 我们创建了一个实体 SkinAttribute 并调用了 mSkinAttrs.load(view,attrs); 来进行View的筛选。

我们接着来看筛选的逻辑部分, 即mSkinAttrs.load(view,attrs);


/**     
* 获取view 中的属性, 过滤出 需要换肤的View 以及 相关的属性 保存起来     
*     
* @param view  
* @param set     
*/    
public void load(View view, AttributeSet set) {     
    List<SkinPair> skinPairs = new ArrayList<>();        
    int attributeCount = set.getAttributeCount();        
    for (int i = 0; i < attributeCount; i++) {            
        String attrName = set.getAttributeName(i);            
        if (mAttributes.contains(attrName)) {                
            int resId;                
            String attributeValue = set.getAttributeValue(i);                
            Log.e(TAG, "attribute name  :" + attrName + " attribute value " + attributeValue);                
            if (attributeValue.startsWith("#")) {    
                // 如果写固定了,比如#111111 我们则不做操作                    
                continue;
            } else if (attributeValue.startsWith("?")) {
                // 主题中的资源                    
                int attrId = Integer.parseInt(attributeValue.substring(1)); 
                // 获取到attrId                    
                resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];                                                } else {                    
                //@1213432  格式                    
                resId = Integer.parseInt(attributeValue.substring(1));  
                // 剪切之前 类似 @2131165298                
            }                

            if (resId != 0) {    
               // 存储对应的 name - id    比如. drawable - 121321423                                                             SkinPair skinPair = new SkinPair(attrName, resId);                                                             skinPairs.add(skinPair);                
            }            
        }        
    }        
    if (!skinPairs.isEmpty()) {            
        SkinView skinView = new SkinView(view, skinPairs);                                                        skinView.applySkin(mSkinTypeface);            
        mSkinViews.add(skinView);        
    }    
}

 
在下面我们定义了一个 所要过滤View 属性的集合。 即满足该属性的View,都会被保存起来。当然,你也可以添加你想过滤的属性到里面。


    //用来记录所有需要更换皮肤的 属性    
private static final List<String> mAttributes = new ArrayList<>(); 
static {       
        mAttributes.add("background");        
        mAttributes.add("src");        
        mAttributes.add("textColor");        
        mAttributes.add("drawableLeft");        
        mAttributes.add("drawableTop");        
        mAttributes.add("drawableRight");        
        mAttributes.add("drawableBottom");    
    }

 
以上,我们就把 采集控件 的步骤完成了, 把需要换肤的控件 都 保存了起来,具体的代码我会在 文章放上链接。 大家可以下载下来详细的看流程。
 

加载皮肤包

即把皮肤包(外置的apk或者zip文件)加载进来。 下面是SkinChangeManager.java 中的代码。


/**
 * 加载皮肤包
 *
 * @param path
 */
public void loadSkin(String path) {
    if (TextUtils.isEmpty(path)) {
        SkinPreference.getInstance().setSkin("");
        SkinResources.getInstance().reset();
    } else {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            addAssetPath.setAccessible(true);
            addAssetPath.invoke(assetManager, path);

            Resources mAppResource = mContext.getResources();

            //获取外部Apk(皮肤包) 包名
            PackageManager mPm = mContext.getPackageManager();
            PackageInfo info = mPm.getPackageArchiveInfo(path, PackageManager
                    .GET_ACTIVITIES);
            String packageName = info.packageName;

            Resources mSkinResources = new Resources(assetManager, mAppResource.getDisplayMetrics(), mAppResource.getConfiguration());

            Log.e(TAG,"package Name : "+packageName);

            SkinResources.getInstance().applySkin(mSkinResources, packageName);

            SkinPreference.getInstance().setSkin(path);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 
以上的代码完成 加载皮肤包,并 得到皮肤包 对应的 Resources
然后交给了 SkinResources 来处理。就这么简单?
 
没错,皮肤包加载完了 ~~~
 
我们需要稍微提一下Android 加载资源的方式:
这里写图片描述

 
ok, 你理解了上面的图中的图片加载方式,就能理解SkinResources 的代码操作了.下面是部分 SkinResources的代码:


    public int getIdentifier(int resId) {        
        if (isDefaultSkin) {            
            return resId;        
        }        
          //在皮肤包中不一定就是 当前程序的 id        
          //获取对应id 在当前的名称 colorPrimary        
          //R.drawable.ic_launcher        
          String resName = mAppResources.getResourceEntryName(resId);//ic_launcher        
          String resType = mAppResources.getResourceTypeName(resId);//drawable        
        int skinId = mSkinResources.getIdentifier(resName, resType, mSkinPkgName);                                return skinId;    
    }    
    public int getColor(int resId) {        
        if (isDefaultSkin) {            
            return mAppResources.getColor(resId);        
        }        
        int skinId = getIdentifier(resId);        
        if (skinId == 0) {            
            return mAppResources.getColor(resId);        
        }        
        return mSkinResources.getColor(skinId);    
    }

 
稍微解释一下: 当你现在使用的是apk的默认皮肤,会直接 mAppResources.getColor(resId); 如果不是 默认皮肤,就会通过 resId 获取到对应的资源名资源类型 ,然后去皮肤包去取。
 
说了半天了,终于该到最后一步了.
 

换肤

在我们的 SkinAttribute 这个类中,有一个类 SkinView 用来记录换肤的View 和 View对应的属性 key(比如 textColor属性) 。
 
面向对象的思想,谁知道你该穿什么衣服? 当然是你自己咯 ~ 。
所以我们的 换肤方法 就在SkinView中 :


/**
 * 应用皮肤, 换肤 是View的 方法 所以我们在 View包装类 的内部进行 皮肤的切换
 * @param mSkinTypeface
 */
public void applySkin(Typeface mSkinTypeface) {
    for (SkinPair skinPair : skinPairs) {
        Drawable left = null, top = null, right = null, bottom = null;
        switch (skinPair.attributesName) {
            case "background":
                Object background = SkinResources.getInstance().getBackground(skinPair.resId);
                // Color
                if (background instanceof Integer) {
                    view.setBackgroundColor((Integer) background);
                } else {
                    view.setBackgroundDrawable((Drawable) background);
                }
                break;
            case "src":
                background = SkinResources.getInstance().getBackground(skinPair
                        .resId);
                if (background instanceof Integer) {
                    ((ImageView) view).setImageDrawable(new ColorDrawable((Integer)
                            background));
                } else {
                    ((ImageView) view).setImageDrawable((Drawable) background);
                }
                break;
            case "textColor":
                ((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList
                        (skinPair.resId));
                break;
            case "drawableLeft":
                left = SkinResources.getInstance().getDrawable(skinPair.resId);
                break;
            case "drawableTop":
                top = SkinResources.getInstance().getDrawable(skinPair.resId);
                break;
            case "drawableRight":
                right = SkinResources.getInstance().getDrawable(skinPair.resId);
                break;
            case "drawableBottom":
                bottom = SkinResources.getInstance().getDrawable(skinPair.resId);
                break;
            default:
                break;
        }
        if (null != left || null != right || null != top || null != bottom) {
            ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right,
                    bottom);
        }
    }
}

 

总结:

你知道动态的换肤需要几步么?那你知道把大象装进冰箱需要几步么?
 
没错,三步就够了。
 
第一步:采集所有需要换肤的控件,保存起来。
 
第二步:加载皮肤包。
 
第三步:换肤(让控件对应上指定的资源)

 
到这里,我们的换肤的整体思路与代码逻辑就结束,还有一些关于 字体状态栏自定义View 等的换肤,整体的思想是一致的,大家可以去参看完整的源码。

代码地址
https://download.csdn.net/download/mike_cui_ls/10311537

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值