android之换肤原理解读

转载自:http://blog.csdn.net/zhongwn/article/details/52891902

如下是解读demo的链接,自行下载

https://github.com/fengjundev/Android-Skin-Loader

由于是开源的,而且对于想了解换肤功能的童鞋这个demo实在是通俗易懂,原理也很清晰,所以忍不住想要记录一下,

题外话:附上一篇换肤技术总结的博客,这是一篇动态换肤与本地换肤(传统的theme)换肤优劣势的详细比较,需要的童鞋,可以详细拜读,至少知道来源

http://blog.zhaiyifan.cn/2015/09/10/Android%E6%8D%A2%E8%82%A4%E6%8A%80%E6%9C%AF%E6%80%BB%E7%BB%93/


换肤功能用于公司的运营是常有的需求,毕竟皮肤对于app来说还是比较重要的;这个对于开发者来说并不关心, 我们只关心其技术原理。

一、换肤功能:

       解读的是一篇动态加载资源从而达到换肤 的效果,这也是换肤的一种潮流,行业上得换肤跟这个demo基本都大同小异,比较性能来说这个方案也是比较值得推荐

       既然是动态的,那就支持网络加载的资源、本地资源应该都要支持,而且是无缝隙,高性能的,废话不多说,先来看看设计者的思路


二、思路:

      前提:这demo只是用本地讲解 所以将要换肤的资源搞成apk,重新命名后缀为.skin,并保存在指定的sd目录中,方便加载,开发者也可通过网上下载保存到指定的sd目,方便支持线上换肤

       1、当点击换肤时,使用AsyncTask加载已保存在sd目录中的apk,并通过AssetManager反射添加资源路径的方法将apk的资源加载进去,然后通过new Resource将Assetmanager管理器注册,得到一个全新的资源管理者AssetManager,通过这个管理者与app原生的资源管理者作为区分,当加载资源的时候,就可以通过资源管理者的资源作为换肤,所以需要护肤时将这个新对象resource将其赋值给全局的resource,通过Resource对象实现换肤;若切换回默认app的皮肤时,就将默认app生成的resource赋值给resource,在其获取资源时,通过resource来控制是取得哪一套资源,从而实现换肤。

     2、当点击换肤,获取到resource之后,这里通过观察者模式去通知当前活动的页面进行换肤,而不是放在onResume实时监测,使用观察者就需要activity实现接口,这里通过在BaseActivity实现统一接口ISkinUpdate,统一进行注册,达到方便管理,方便换肤

     3、需要换肤就得知道哪些view需要换肤,通过设置inflate中的一个工厂Factory,这个工厂是用来创建一个view,有点类似hook,只要这个Factory返回一个view就不会再进行解析我们xml设置的view,每创建一个view之前factory都会执行一次,所以在这里通过设置自己自定义的实现接口Factory的SkinInflateFactory,就可以在其读取layout的xml文件生成view之前会执行onCreateView,通过hook这个点,即生成xml文件的view又可以满足我们所要读取需要换肤的view,并且判断当前view是否需要换肤,需要则直接设置相应的color或drawable。到此基本就这个思路

   

三、代码走读

   接下来一起看看代码走读:我的阅读习惯是从点击切换皮肤开始,然后一层层剥皮,需要用到的属性在哪里初始化,就跳转到哪里看看初始化的地方;读者在读demo的时候根据自己的习惯吧,这里就姑且按我的思维方式走。

  换肤嘛,当然是找到点击换肤的事件咯;

  找到如下类及其点击换肤响应事件的方法

public class SettingActivity extends BaseActivity {

   /**
    * Put this skin file on the root of sdcard
    * eg:
    * /mnt/sdcard/BlackFantacy.skin
    */
   private static final String SKIN_NAME = "BlackFantacy.skin";
   private static final String SKIN_DIR = Environment
         .getExternalStorageDirectory() + File.separator + SKIN_NAME;


   private void initView() {

      setNightSkinBtn.setOnClickListener(new OnClickListener() {

         @Override
         public void onClick(View v) {
            onSkinSetClick();
         }
      });

      setOfficalSkinBtn.setOnClickListener(new OnClickListener() {

         @Override
         public void onClick(View v) {
            onSkinResetClick();
         }
      });
   }


   private void onSkinSetClick() {
      if(!isOfficalSelected) return;

      File skin = new File(SKIN_DIR);

      if(skin == null || !skin.exists()){
         Toast.makeText(getApplicationContext(), "请检查" + SKIN_DIR + "是否存在", Toast.LENGTH_SHORT).show();
         return;
      }

      SkinManager.getInstance().load(skin.getAbsolutePath(),
            new ILoaderListener() {
               @Override
               public void onStart() {
                  L.e("startloadSkin");
               }

               @Override
               public void onSuccess() {
                  L.e("loadSkinSuccess");
                  Toast.makeText(getApplicationContext(), "切换成功", Toast.LENGTH_SHORT).show();
                  setNightSkinBtn.setText("黑色幻想(当前)");
                  setOfficalSkinBtn.setText("官方默认");
                  isOfficalSelected = false;
               }

               @Override
               public void onFailed() {
                  L.e("loadSkinFail");
                  Toast.makeText(getApplicationContext(), "切换失败", Toast.LENGTH_SHORT).show();
               }
            });
   }
}

然后在这个设置类里边,根据响应事件,我们看到了加载皮肤的调用处:

SkinManager.getInstance().load(skin.getAbsolutePath(),
当然路径是如下:先不管,将demo的BlackFanTancy.skin放到sd卡就行

private static final String SKIN_NAME = "BlackFantacy.skin";
   private static final String SKIN_DIR = Environment
         .getExternalStorageDirectory() + File.separator + SKIN_NAME;

接下来看看SkinManager这个单例皮肤管理类:其中load的方法,进入如下:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. public void load(String skinPackagePath, final ILoaderListener callback) {  
  2.   
  3.         new AsyncTask<String, Void, Resources>() {  
  4.   
  5.             protected void onPreExecute() {  
  6.                 if (callback != null) {  
  7.                     callback.onStart();  
  8.                 }  
  9.             };  
  10.   
  11.             @Override  
  12.             protected Resources doInBackground(String... params) {  
  13.                 try {  
  14.                     if (params.length == 1) {//加载皮肤包,并且将包的路径加到资源管理器中AssetManager  
  15.                         String skinPkgPath = params[0];  
  16.   
  17.                         File file = new File(skinPkgPath);  
  18.                         if (file == null || !file.exists()) {//  
  19.                             Log.d(TAG, "!file.exists() skinPkgPath= " + skinPkgPath);  
  20.                             Log.d(TAG, "!file.exists() = " + file.exists());  
  21.                             return null;  
  22.                         }  
  23.                         Log.d(TAG,"skinPkgPath = "+skinPkgPath);  
  24.                         PackageManager mPm = context.getPackageManager();  
  25.                         PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);  
  26.                         skinPackageName = mInfo.packageName;  
  27.   
  28.                         AssetManager assetManager = AssetManager.class.newInstance();  
  29.                         Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);  
  30.                         addAssetPath.invoke(assetManager, skinPkgPath);  
  31.   
  32.                         Resources superRes = context.getResources();  
  33.                         Resources skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());  
  34.   
  35.                         SkinConfig.saveSkinPath(context, skinPkgPath);  
  36.   
  37.                         skinPath = skinPkgPath;  
  38.                         isDefaultSkin = false;  
  39.                         return skinResource;  
  40.                     }  
  41.                     return null;  
  42.                 } catch (Exception e) {  
  43.                     e.printStackTrace();  
  44.                     return null;  
  45.                 }  
  46.             };  
  47.   
  48.             protected void onPostExecute(Resources result) {  
  49.                 mResources = result;  
  50.   
  51.                 if (mResources != null) {  
  52.                     if (callback != null) callback.onSuccess();  
  53.                     notifySkinUpdate();  
  54.                 }else{  
  55.                     isDefaultSkin = true;  
  56.                     if (callback != null) callback.onFailed();  
  57.                 }  
  58.             };  
  59.   
  60.         }.execute(skinPackagePath);  
  61.     }  

这个方法传入了一个回调接口ILoadListner,以及皮肤绝对路径,随后通过AsyncTask异步执行加载路径上的skin资源

接下来看看其中三个方法

首先、onStart():没做什么工作,就是在加载之前判断callBack是否为空做一些初始化,我们这里没做什么初始化,只是打印而已

再次:doInBackground:重点的都在这个方法里边了:这个方法异步加载了资源,通过新创建Assetmanager与Resource建立对新加载的资源skin的管理,完成后通过SkinConfig.saveSkinPath();保存当前皮肤路径,以备下次再次打开app时默认加载皮肤还是上一次选中的。

最后:onPostExecut回到主线程处理更新皮肤;这里将新创建的resource对象保存到全局,由于callBack不为null,然后通过回调接口callBack.onSuccess()修改ui,以及调用notifySkinUpdate();猜测这个方法就是进行皮肤更新的方法。

到了这里这个线路基本完事儿;

细心好奇的你疑问肯定有两点:

1)、context是在哪儿赋值的

2)、notifySkinUpdate()到底做了什么工作


先来看看context到底在哪儿赋值的,当然是在当前类找了:你会发现下边这个方法

public void init(Context ctx){
   context = ctx.getApplicationContext();
}
这个很自然的看看其在哪儿调用的,快捷键ctrl+alt+H,只有一处方法调用,那就是Application,这是应用启动就初始化的如下

public class SkinApplication extends Application {
   
   public void onCreate() {
      super.onCreate();
      
      initSkinLoader();
   }

   /**
    * Must call init first
    */
   private void initSkinLoader() {
      SkinManager.getInstance().init(this);
      SkinManager.getInstance().load();
   }
}

看到这里,你会意外发现,咦,这里也有个load()那我们就会疑问这个load()是干啥用的,初始化的时候为何要调用它,这个load是在我们进入app时调用的,那就有理由猜想如下:

 1)、 上次登录app时我们还没切换过皮肤,还是默认皮肤,这个load是怎么工作的

 2)、上次登录app时我们切换皮肤了,那么这个load又是怎么工作的

带着这两个疑问,我们进入load()方法一探究竟呗

public void load(){
   String skin = SkinConfig.getCustomSkinPath(context);
   Log.d(TAG, "skin = " + skin);
   load(skin, null);
}
首先从sharePreference获取皮肤路径:分两步走

 <一>、没切换过皮肤,则skin得到的是默认

public  static final String     DEFALT_SKIN          =  "cn_feng_skin_default";

 <二>、切换了皮肤,则获取的是切换皮肤的路径:

然后往下走,神奇的发现调用了load(skin,null);  这个方法不就是前边我们分析过的吗,接下来我们再次看看这个方法,毕竟参数不一样了嘛:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. /** 
  2.      * Load resources from apk in asyc task 
  3.      * @param skinPackagePath path of skin apk 
  4.      * @param callback callback to notify user 
  5.      */  
  6.     public void load(String skinPackagePath, final ILoaderListener callback) {  
  7.   
  8.         new AsyncTask<String, Void, Resources>() {  
  9.   
  10.             protected void onPreExecute() {  
  11.                 if (callback != null) {  
  12.                     callback.onStart();  
  13.                 }  
  14.             };  
  15.   
  16.             @Override  
  17.             protected Resources doInBackground(String... params) {  
  18.                 try {  
  19.                     if (params.length == 1) {//加载皮肤包,并且将包的路径加到资源管理器中AssetManager  
  20.                         String skinPkgPath = params[0];  
  21.   
  22.                         File file = new File(skinPkgPath);  
  23.                         if (file == null || !file.exists()) {//  
  24.                             Log.d(TAG, "!file.exists() skinPkgPath= " + skinPkgPath);  
  25.                             Log.d(TAG, "!file.exists() = " + file.exists());  
  26.                             return null;  
  27.                         }  
  28.                         Log.d(TAG,"skinPkgPath = "+skinPkgPath);  
  29.                         PackageManager mPm = context.getPackageManager();  
  30.                         PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);  
  31.                         skinPackageName = mInfo.packageName;  
  32.   
  33.                         AssetManager assetManager = AssetManager.class.newInstance();  
  34.                         Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);  
  35.                         addAssetPath.invoke(assetManager, skinPkgPath);  
  36.   
  37.                         Resources superRes = context.getResources();  
  38.                         Resources skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());  
  39.   
  40.                         SkinConfig.saveSkinPath(context, skinPkgPath);  
  41.   
  42.                         skinPath = skinPkgPath;  
  43.                         isDefaultSkin = false;  
  44.                         return skinResource;  
  45.                     }  
  46.                     return null;  
  47.                 } catch (Exception e) {  
  48.                     e.printStackTrace();  
  49.                     return null;  
  50.                 }  
  51.             };  
  52.   
  53.             protected void onPostExecute(Resources result) {  
  54.                 mResources = result;  
  55.   
  56.                 if (mResources != null) {  
  57.                     if (callback != null) callback.onSuccess();  
  58.                     notifySkinUpdate();  
  59.                 }else{  
  60.                     isDefaultSkin = true;  
  61.                     if (callback != null) callback.onFailed();  
  62.                 }  
  63.             };  
  64.   
  65.         }.execute(skinPackagePath);  
  66.     }  

当然还是分两步走:由于callback为null,所以不在走callback回调,这里不做分析了

   1)默认皮肤时,skin为默认的

public  static final String     DEFALT_SKIN          =  "cn_feng_skin_default";

 由于这个是不存在的,所以当执行到doInBackground():

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. File file = new File(skinPkgPath);  
  2.                         if (file == null || !file.exists()) {//  
  3.                             Log.d(TAG, "!file.exists() skinPkgPath= " + skinPkgPath);  
  4.                             Log.d(TAG, "!file.exists() = " + file.exists());  
  5.                             return null;  
  6.                         }  
在这里会直接返回,不在加载皮肤;用的就是app中layout默认的颜色背景;到这里这个默认的分析完毕

  2)切换了皮肤:前边也分析保存了皮肤在sd中的绝对路径:所以这里获取的skin路径上一次皮肤的路径

       再往下走,除了callback不调用外,最后还是调用notifySkinUpdate()方法;


那么这个方法到底做了什么,肯定就是我们的换肤方法了;好奇的你肯定会进入notifySkinUpdate()方法一探究竟,那我们就一起看看:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. @Override  
  2.     public void notifySkinUpdate() {  
  3.         if(skinObservers == nullreturn;  
  4.         for(ISkinUpdate observer : skinObservers){  
  5.             observer.onThemeUpdate();  
  6.         }  
  7.     }  

当打开app的时候,不管曾经是否换肤,由于skinObservers为null,所以直接返回

那么我们就要看看这个观察者skinObservers在哪儿初始化,哪儿订阅的了;

我们会发现初始化的方法、订阅的地方以及取消订阅的地方如下:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. @Override  
  2.     public void attach(ISkinUpdate observer) {  
  3.         if(skinObservers == null){  
  4.             skinObservers = new ArrayList<ISkinUpdate>();  
  5.         }  
  6.         if(!skinObservers.contains(skinObservers)){  
  7.             skinObservers.add(observer);  
  8.         }  
  9.     }  
  10.   
  11.     @Override  
  12.     public void detach(ISkinUpdate observer) {  
  13.         if(skinObservers == nullreturn;  
  14.         if(skinObservers.contains(observer)){  
  15.             skinObservers.remove(observer);  
  16.         }  
  17.     }  

这一看这里都是重写的方法,细心的你会发现notifySkinUpdat()方法也是重写的,那么我们就看看它实现的接口

public class SkinManager implements ISkinLoader{
然后再看看attach(ISkinUpdate observer)和detach( ISkinUpdate observer )在哪儿调用,在查看之前我们有理由猜想,BaseActivity肯定是作为观察者实现了ISkinUpdate,已实时监测换肤功能;

接下来查看attach(ISkinUpdate observer)和detach(ISkinUpdate observer)调用:发现确实是BaseActivity和BaseFragementActivity两个类中调用:如下

@Override
protected void onResume() {
   super.onResume();
   SkinManager.getInstance().attach(this);
}

@Override
protected void onDestroy() {
   super.onDestroy();
   SkinManager.getInstance().detach(this);
   mSkinInflaterFactory.clean();
}

传的是this,那么她们肯定实现了接口

public interface ISkinUpdate {
   void onThemeUpdate();
}

自然而然,我们就来看看onThemeUdapte做了什么,它就能更换皮肤了?

@Override
public void onThemeUpdate() {
   if(!isResponseOnSkinChanging){
      return;
   }
   mSkinInflaterFactory.applySkin();
}
isResponseOnSkinChanging
这个默认是true,也没地方改变它的默认值,我们先不管,直接跳到下面那行

   mSkinInflaterFactory.applySkin();
还是看看mSkinInflaterFactory到底是什么鬼,在哪儿初始化的,查看知道在oncreate方法中:

private SkinInflaterFactory mSkinInflaterFactory;

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   mSkinInflaterFactory = new SkinInflaterFactory();
   getLayoutInflater().setFactory(mSkinInflaterFactory);
}
这到底是什么鬼,每次进入activity都要设置这个Factory,这里我们部队Factory开讲,下次再进行对它的源码深究,我们只要知道它是一个生产view的工厂类,在inflate的时候,通过每遍历一个layout的每个组件view之前都会检测Factory是否为null,若不为null,则会调用onCreaterView();

扩展:Factory是否要生成view,如果生成view,则不会在创建layout遍历的那个组件,所以通过这个Factory也可以更改返回显示的view:比如layout布局其中一个组件是Imageview,而通过Factory可以生成TextView代替ImageView;

接下来看看SkinInFlaterFactory这个类的onCreateView,这个类肯定实现了Factory接口,否则setFactory()的,所以每次在inFlate时由于factory不为空,肯定都会检测是否要调用onCreateView;所以setFactory必须设置在setContentView()方法之前;因为setContentView实际上也是调用inflate;

那就看看其oncreateView()咯:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. @Override  
  2.     public View onCreateView(String name, Context context, AttributeSet attrs) {  
  3.         // if this is NOT enable to be skined , simplly skip it  
  4.         //在xml的节点中设置,设置为true表示是需要换肤的view,否则跳过这个view,因为这个view不需要换肤  
  5.         boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);  
  6.         if (!isSkinEnable){  
  7.                 return null;  
  8.         }  
  9.           
  10.         View view = createView(context, name, attrs);  
  11.           
  12.         if (view == null){  
  13.             return null;  
  14.         }  
  15.           
  16.         parseSkinAttr(context, attrs, view);  
  17.           
  18.         return view;  
  19.     }  

首先:就来看看这个isSkinEnable是什么:这个是我们在layout为组件设置的一个标志,标着这个组件是需要换肤的,如果不需要换肤的组件我们就不用往下走

其次:假设在组件中设置属性skin:enable=true,则就会往下执行 ,执行createView()以及parseSkinAttr(),以下分析这连个个方法:

  

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. private View createView(Context context, String name, AttributeSet attrs) {  
  2.         View view = null;  
  3.         try {  
  4.             Log.d("SkinInflaterFactory","name = "+name);  
  5.             if (-1 == name.indexOf('.')){//-1则不是自定义的view  
  6.                 if ("View".equals(name)) {  
  7.                     view = LayoutInflater.from(context).createView(name, "android.view.", attrs);  
  8.                 }   
  9.                 if (view == null) {  
  10.                     view = LayoutInflater.from(context).createView(name, "android.widget.", attrs);  
  11.                 }   
  12.                 if (view == null) {  
  13.                     view = LayoutInflater.from(context).createView(name, "android.webkit.", attrs);  
  14.                 }   
  15.             }else {  
  16.                 view = LayoutInflater.from(context).createView(name, null, attrs);  
  17.             }  
  18.   
  19.             L.i("about to create " + name);  
  20.   
  21.         } catch (Exception e) {   
  22.             L.e("error while create 【" + name + "】 : " + e.getMessage());  
  23.             view = null;  
  24.         }  
  25.         return view;  
  26.     }  
这里返回一个创建view,主要是判断:

这个组件view是否是用原生的还是自定义的:name是组件的名字:如TextView、ImageView,所示自定义的,则得到的是全名(包名+类名)


parseSkinAttr():

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. private void parseSkinAttr(Context context, AttributeSet attrs, View view) {  
  2.         List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();  
  3.           
  4.         for (int i = 0; i < attrs.getAttributeCount(); i++){  
  5.             String attrName = attrs.getAttributeName(i);  
  6.             String attrValue = attrs.getAttributeValue(i);  
  7.             Log.d(TAG,"attrName = "+attrName);//属性name比如layout_width或者自定义属性name比如本次用的enable 值是true  
  8.             Log.d(TAG,"attrValue = "+attrValue);//属性的值  
  9.             if(!AttrFactory.isSupportedAttr(attrName)){  
  10.                 continue;  
  11.             }  
  12.               
  13.             if(attrValue.startsWith("@")){  
  14.                 try {  
  15.                     int id = Integer.parseInt(attrValue.substring(1));  
  16.                     String entryName = context.getResources().getResourceEntryName(id);  
  17.                     String typeName = context.getResources().getResourceTypeName(id);  
  18.                     SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);  
  19.                     Log.d(TAG,"id = "+id);  
  20.                     Log.d(TAG,"getResourceEntryName = "+entryName);//color的name或drawable的name  
  21.                     Log.d(TAG,"getResourceTypeName = "+typeName);//比如:color,drawable  
  22.                     if (mSkinAttr != null) {  
  23.                         viewAttrs.add(mSkinAttr);  
  24.                     }  
  25.                 } catch (NumberFormatException e) {  
  26.                     e.printStackTrace();  
  27.                 } catch (NotFoundException e) {  
  28.                     e.printStackTrace();  
  29.                 }  
  30.             }  
  31.         }  
  32.           
  33.         if(!ListUtils.isEmpty(viewAttrs)){  
  34.             SkinItem skinItem = new SkinItem();  
  35.             skinItem.view = view;  
  36.             skinItem.attrs = viewAttrs;  
  37.   
  38.             mSkinItems.add(skinItem);  
  39.   
  40.             if (SkinManager.getInstance().isExternalSkin()) {  
  41.                 //这里是每次进入activity后fragment的时候都要判断是否需要换肤  
  42.                 skinItem.apply();  
  43.             }  
  44.         }  
  45.     }  

这个方法是解析组件的所有属性, 并将得到的可以换肤的所有属性 color或drawable属性id和属性名 保存到一个viewAttrs,然后将viewAttrs 和view所有相关值 保存skinItem,随后将SkinItem缓存到mSkinItem集合中,接下来是判断当前是否需要换肤;假设是需要的,则我们来看看它是如何执行的

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. public void apply(){  
  2.         if(ListUtils.isEmpty(attrs)){  
  3.             return;  
  4.         }  
  5.         for(SkinAttr at : attrs){  
  6.             at.apply(view);  
  7.         }  
  8.     }  
由于parseSKinAttr解析式已经将attrs设置,所不会为空,所以会执行for循环
查看at.apply(View),那么我们发现SkinAttr是个抽象类,抽象方法为apply(view),于是回过头看看parseSKinAttr这个方法在哪里调用的:惊奇的发现:

SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
用的竟让是个工厂方式,进去一瞧咯:

public static SkinAttr get(String attrName, int attrValueRefId, String attrValueRefName, String typeName){
   
   SkinAttr mSkinAttr = null;
   
   if(BACKGROUND.equals(attrName)){ 
      mSkinAttr = new BackgroundAttr();
   }else if(TEXT_COLOR.equals(attrName)){ 
      mSkinAttr = new TextColorAttr();
   }else if(LIST_SELECTOR.equals(attrName)){ 
      mSkinAttr = new ListSelectorAttr();
   }else if(DIVIDER.equals(attrName)){ 
      mSkinAttr = new DividerAttr();
   }else{
      return null;
   }
   
   mSkinAttr.attrName = attrName;
   mSkinAttr.attrValueRefId = attrValueRefId;
   mSkinAttr.attrValueRefName = attrValueRefName;
   mSkinAttr.attrValueTypeName = typeName;
   return mSkinAttr;
}
很容易就知道,这是生成什么属性,支持哪些换肤,主要有四个,所以只拿第一个BackgroundAttr类作为分析,其它原理一样:
public class BackgroundAttr extends SkinAttr {

   @Override
   public void apply(View view) {
      
      if(RES_TYPE_NAME_COLOR.equals(attrValueTypeName)){
         view.setBackgroundColor(SkinManager.getInstance().getColor(attrValueRefId));
         Log.i("attr", "_________________________________________________________");
         Log.i("attr", "apply as color");
      }else if(RES_TYPE_NAME_DRAWABLE.equals(attrValueTypeName)){
         Drawable bg = SkinManager.getInstance().getDrawable(attrValueRefId);
         view.setBackground(bg);
         Log.i("attr", "_________________________________________________________");
         Log.i("attr", "apply as drawable");
         Log.i("attr", "bg.toString()  " + bg.toString());
         
         Log.i("attr", this.attrValueRefName + " 是否可变换状态? : " + bg.isStateful());
      }
   }
}
原来换肤最终的真相在这里:

view.setBackgroundColor(SkinManager.getInstance().getColor(attrValueRefId))
以及

 Drawable bg = SkinManager.getInstance().getDrawable(attrValueRefId);
         view.setBackground(bg);
这里边的

SkinManager.getInstance().getColor(attrValueRefId)
SkinManager.getInstance().getDrawable(attrValueRefId)

做了什么:进去瞧瞧就知道了:这里选择一个来分析吧,其它都一样的

public int getColor(int resId){
   int originColor = context.getResources().getColor(resId);
   if(mResources == null || isDefaultSkin){
      return originColor;
   }
   //通过默认的resId获取默认颜色的资源名,通过名字查找皮肤包一致的名字再获取生成的dstId
   String resName = context.getResources().getResourceEntryName(resId);

   int trueResId = mResources.getIdentifier(resName, "color", skinPackageName);
   int trueColor = 0;

   try{
      trueColor = mResources.getColor(trueResId);
   }catch(NotFoundException e){
      e.printStackTrace();
      trueColor = originColor;
   }

   return trueColor;
}

首先:通过app的context获取originColor,app的默认的颜色,若mResoutces=null(没切换皮肤)或isDefaultSkin=true(显示的是默认的),则直接返回显示默认color

否则:通过默认的resId获取默认颜色的资源名,通过名字查找皮肤包一致的名字再获取生成的dstId,得到dstId这里就是trueResId,这个id就是从新的Resource资源管理者获取的,就是换肤的皮肤颜色id,这样就能获得了皮肤,直接返回设置颜色就可以换肤成功了;

到这里终于结束了!!!,理解有误的地方,敬请指正!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值