android 换肤模式总结

【android 换肤模式总结】将相本无种,男儿当自强。这篇文章主要讲述android 换肤模式总结相关的知识,希望能为你提供帮助。
由于android的设置中并没有夜间模式的选项,对于喜欢睡前玩手机的用户,只能简单的调节手机屏幕亮度来改善体验。目前越来越多的应用开始把夜间模式加到自家应用中,没准不久google也会把这项功能添加到Android系统中吧。
业内关于夜间模式的实现,有两种主流方案,各有其利弊,我较为推崇第三种方案:
1、通过切换theme来实现夜间模式。
2、通过修改uiMode来切换夜间模式。
3、通过插件方式切换夜间模式。
值得一提的是,上面提到的几种方案,都是资源内嵌在Apk中的方案,像新浪微博那种需要通过下载方式实现的夜间模式方案,网上有很多介绍,这里不去讨论。
下面简要描述下几种方案的实现原理:
1、通过切换theme来实现夜间模式。
首先在attrs.xml中,为需要随theme变化的内容定义属性

< ?xml  version="1.0"  encoding="utf-8"?>     < resources>             < attr  name="colorValue"  format="color"  />             < attr  name="floatValue"  format="float"  />             < attr  name="integerValue"  format="integer"  />             < attr  name="booleanValue"  format="boolean"  />             < attr  name="dimensionValue"  format="dimension"  />             < attr  name="stringValue"  format="string"  />             < attr  name="referenceValue"  format="color|reference"  />             < attr  name="imageValue"  format="reference"/>                 < attr  name="curVisibility">             < enum  name="show"  value="https://www.songbingjia.com/android/0"  />             < !--  Not  displayed,  but  taken  into  account  during  layout  (space  is  left  for  it).  -->             < enum  name="inshow"  value="https://www.songbingjia.com/android/1"  />             < !--  Completely  hidden,  as  if  the  view  had  not  been  added.  -->             < enum  name="hide"  value="https://www.songbingjia.com/android/2"  />             < /attr>     < /resources>

从上面的xml文件的内容可以看到,attr里可以定义各种属性类型,如color、float、integer、boolean、dimension(sp、dp/dip、px、pt...)、reference(指向本地资源),还有curVisibility是枚举属性,对应view的invisibility、visibility、gone。
其次在不同的theme中,对属性设置不同的值,在styles.xml中定义theme如下
< style  name="DayTheme"  parent="Theme.Sherlock.Light"> >             < item  name="colorValue"> @color/title< /item>             < item  name="floatValue"> 0.35< /item>             < item  name="integerValue"> 33< /item>             < item  name="booleanValue"> true< /item>             < item  name="dimensionValue"> 16dp< /item>             < !--  如果string类型不是填的引用而是直接放一个字符串,在布局文件中使用正常,但代码里获取的就有问题  -->             < item  name="stringValue"> @string/action_settings< /item>             < item  name="referenceValue"> @drawable/bg< /item>             < item  name="imageValue"> @drawable/launcher_icon< /item>             < item  name="curVisibility"> show< /item>     < /style>     < style  name="NightTheme"  parent="Theme.Sherlock.Light">             < item  name="colorValue"> @color/night_title< /item>             < item  name="floatValue"> 1.44< /item>             < item  name="integerValue"> 55< /item>             < item  name="booleanValue"> false< /item>             < item  name="dimensionValue"> 18sp< /item>             < item  name="stringValue"> @string/night_action_settings< /item>             < item  name="referenceValue"> @drawable/night_bg< /item>             < item  name="imageValue"> @drawable/night_launcher_icon< /item>             < item  name="curVisibility"> hide< /item>     < /style>

在布局文件中使用对应的值,通过?attr/属性名,来获取不同theme对应的值。
?xml  version="1.0"  encoding="utf-8"?>     < LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"            android:layout_width="match_parent"            android:layout_height="match_parent"              android:background="?attr/referenceValue"            android:orientation="vertical"            >             < TextView                    android:id="@+id/setting_Color"                    android:layout_width="wrap_content"                    android:layout_height="wrap_content"                    android:text="TextView"                    android:textColor="?attr/colorValue"  />             < CheckBox                                    android:id="@+id/setting_show_answer_switch"                                    android:layout_width="wrap_content"                                    android:layout_height="wrap_content"                                                                  android:checked="?attr/booleanValue"/>                   < TextView                    android:id="@+id/setting_Title"                    android:layout_width="wrap_content"                    android:layout_height="wrap_content"                      android:textSize="?attr/dimensionValue"                      android:text="@string/text_title"                    android:textColor="?attr/colorValue"  />               < TextView                    android:id="@+id/setting_Text"                    android:layout_width="wrap_content"                    android:layout_height="wrap_content"                        android:text="?attr/stringValue"  />                 < ImageView                    android:id="@+id/setting_Image"                    android:layout_width="wrap_content"                    android:layout_height="wrap_content"                            android:src="http://img.readke.com/220413/14134J023-0.jpg"  />                     < View  android:id="@+id/setting_line"                    android:layout_width="match_parent"                    android:layout_height="1dp"                    android:visibility="?attr/curVisibility"                    />       < /LinearLayout>

在Activity中调用如下changeTheme方法,其中isNightMode为一个全局变量用来标记当前是否为夜间模式,在设置完theme后,还需要调用restartActivity或者setContentView重新刷新UI。
@Override            protected  void  onCreate(Bundle  savedInstanceState)  {                                  super.onCreate(savedInstanceState);                     if(AppThemeManager.isLightMode()){                            this.setTheme(R.style.NightTheme);                     }else{                            this.setTheme(R.style.DayTheme);                     }                    setContentView(R.layout.setting);             }

到此即完成了一个夜间模式的简单实现,包括Google自家在内的很多应用都是采用此种方式实现夜间模式的,这应该也是Android官方推荐的方式。
但这种方式有一些不足,规模较大的应用,需要随theme变化的属性会很多,都需要逐一定义,有点麻烦,另外一个缺点是要使得新theme生效,一般需要restartActivity来切换UI,会导致切换主题时界面闪烁。
不过也可以通过调用自定义的updateTheme方法,重启Activity即可
public  static  void  updateTheme(Activity  activity,isNight) {     AppThemeManager.setNightMode(isNight);     activity.recreate();     /     *     *  activity.finish();       *  Intent  intent=new  Intent();       *  intent.setClass(context,  MainActivity.class);       *  context.startActivity(intent);     */ }

当然,潜在的问题也是存在的,比如,我们动态获取资源Resource,那么遇到这种情况的解决办法是自定义资源获取规则,并且在资源名称上下功夫
public  static  Drawable  getDrawable(Context  context,String  resName,boolean  isForce) {         int    resId;         if(AppThemeManager.isLightMode()  & &   isForce)  //这里使用isForce参数主要是为了一些主题切换时共用的图片被匹配         {         //约定,黑夜图片带_night               resId  =  context.getResources().getIdentifier(resName+"_night",  "drawable",  context.getPackageName());         }else{               resId  =  context.getResources().getIdentifier(resName,  "drawable",  context.getPackageName());         }                 return  context.getResources().getDrawable(resId); }public  static  Drawable  getDrawable(Context  context,int  resid,boolean  isForce) {         String  resName    =  context.getResources().getResourceEntryName(resid);         if(AppThemeManager.isLightMode()  & &   isForce)         {               resName  =  resName+"_night";         }         int    resId  =  context.getResources().getIdentifier(resName,  "drawable",  context.getPackageName());         return  context.getResources().getDrawable(resId); }//当然,获取string,dimens等资源也是这种方式,这里就不再论述

优点:可以匹配多套主题,并不局限于黑白模式
缺点:需要大量定义主题
2、通过修改uiMode来切换夜间模式。
修改uimode是修改Configuration,这种主题切换只限于黑白模式,没有其他模式,核心代码如下
Configuration  newConfig  =  new  Configuration(activity.getResources().getConfiguration()); newConfig.uiMode  & =  ~Configuration.UI_MODE_NIGHT_MASK; newConfig.uiMode  |=  uiNightMode; activity.getResources().updateConfiguration(newConfig,  null); activity.recreate();

但这种切换的前提是,我们的资源目录必须具备切换-night后缀,类似国际化语言的切换,如:
values-night/ drawable-night/ drawable-night-xxdpi/ .....

下面来一个开源的Helper
package  com.example.androidtestcase; import  android.app.Activity; import  android.content.SharedPreferences; import  android.content.res.Configuration; import  android.preference.PreferenceManager; import  java.lang.ref.WeakReference; public  class  NightModeHelper {        private  static  final  String  PREF_KEY  =  "nightModeState";         private  static  int  sUiNightMode  =  Configuration.UI_MODE_NIGHT_UNDEFINED;         private  WeakReference< Activity>   mActivity;         private  SharedPreferences  mPrefs;         public  NightModeHelper(Activity  activity)         {                int  currentMode  =  (activity.getResources().getConfiguration()                                 .uiMode  &   Configuration.UI_MODE_NIGHT_MASK);                 mPrefs  =  PreferenceManager.getDefaultSharedPreferences(activity);                 init(activity,  -1,  mPrefs.getInt(PREF_KEY,  currentMode));         }              public  NightModeHelper(Activity  activity,  int  theme)         {                int  currentMode  =  (activity.getResources().getConfiguration()                                 .uiMode  &   Configuration.UI_MODE_NIGHT_MASK);                 mPrefs  =  PreferenceManager.getDefaultSharedPreferences(activity);                 init(activity,  theme,  mPrefs.getInt(PREF_KEY,  currentMode));         }        public  NightModeHelper(Activity  activity,  int  theme,  int  defaultUiMode)         {                init(activity,  theme,  defaultUiMode);         }        private  void  init(Activity  activity,  int  theme,  int  defaultUiMode)         {                mActivity  =  new  WeakReference< Activity> (activity);                 if  (sUiNightMode  ==  Configuration.UI_MODE_NIGHT_UNDEFINED)                 {                         sUiNightMode  =  defaultUiMode;                 }                 updateConfig(sUiNightMode);                 if  (theme  !=  -1)                 {                         activity.setTheme(theme);                 }         }        private  void  updateConfig(int  uiNightMode)         {                Activity  activity  =  mActivity.get();                 if  (activity  ==  null)                 {                         throw  new  IllegalStateException("Activity  went  away?");                 }                 Configuration  newConfig  =  new  Configuration(activity.getResources().getConfiguration());                 newConfig.uiMode  & =  ~Configuration.UI_MODE_NIGHT_MASK;                 newConfig.uiMode  |=  uiNightMode;                 activity.getResources().updateConfiguration(newConfig,  null);                 sUiNightMode  =  uiNightMode;                 if  (mPrefs  !=  null)                 {                         mPrefs.edit()                                         .putInt(PREF_KEY,  sUiNightMode)                                         .apply();                 }         }        public  static  int  getUiNightMode()         {                return  sUiNightMode;         }        public  void  toggle()         {                if  (sUiNightMode  ==  Configuration.UI_MODE_NIGHT_YES)                 {                         notNight();                 }  else                 {                         night();                 }         }        public  void  notNight()         {                updateConfig(Configuration.UI_MODE_NIGHT_NO);                 System.gc();                 System.runFinalization();                   System.gc();                 mActivity.get().recreate();         }        public  void  night()         {                updateConfig(Configuration.UI_MODE_NIGHT_YES);                 System.gc();                 System.runFinalization();   //  added  in  https://github.com/android/platform_frameworks_base/commit/6f3a38f3afd79ed6dddcef5c83cb442d6749e2ff                 System.gc();                 mActivity.get().recreate();         } }

当然,Android也为这种过于冗杂的模式提供了UIModeManager,优点是我们再也不需要使用Perference手动保存并管理一些信息了。
UiModeManager  umm  =  (UiModeManager  )context.getSystemService(Context.UI_MODE_SERVICE); umm.getNightMode(UI_MODE_NIGHT_YES);

对于第二种方案,优缺点如下:
优点:
/res/xxx-night形式避免了切换中需要手动管理资源的问题,避免了代码手动管理夜间模式配置
缺点:
只能局限于2种主题。
 
3、通过插件方式切换夜间模式。
插件换肤具体请参考如下博客:
Android更换皮肤解决方案
参考  http://www.2cto.com/kf/201501/366859.html
本项目是以插件化开发思想进行的,主要工作和代码如下
 
资源文件,这里以color资源为例
1、首先我们需要准备一个皮肤包,这个皮肤包里面不会包含任何Activity,里面只有资源文件,这里我为了简单,仅仅加入一个color.xml(其实就相当于Android系统中的framework_res.apk)
< !--?xml  version="1.0"  encoding="utf-8"?--> < resources>         < color  name="main_btn_color"> #E61ABD< /color>         < color  name="main_background"> #38F709< /color>                   < color  name="second_btn_color"> #000000< /color>         < color  name="second_background"> #FFFFFF< /color>           < /resources>

2、将该资源打包成apk文件,放入sd卡中(实际项目你可以从我网络下载)
3、将需要换肤的Activity实现ISkinUpdate(这个可以自己随便定义名称)接口
public  class  MainActivity  extends  Activity  implements  ISkinUpdate,OnClickListener {         private  Button  btn_main;         private  View  main_view;           @Override         protected  void  onCreate(Bundle  savedInstanceState)  {                 super.onCreate(savedInstanceState);             this.setContentView(R.layout.activity_main);                                   SkinApplication.getInstance().mActivitys.add(this);                 btn_main=(Button)this.findViewById(R.id.btn_main);                 btn_main.setOnClickListener(this);                                   main_view=this.findViewById(R.id.main_view);                           }                           @Override         protected  void  onResume()  {             super.onResume();             if(SkinPackageManager.getInstance(this).mResources!=null)             {                 updateTheme();                 Log.d("yzy",  "onResume--> updateTheme");             }         }           @Override         public  boolean  onCreateOptionsMenu(Menu  menu)  {                 //  Inflate  the  menu;   this  adds  items  to  the  action  bar  if  it  is  present.                 getMenuInflater().inflate(R.menu.main,  menu);                 return  true;         }           @Override         public  boolean  onOptionsItemSelected(MenuItem  item)  {                 int  id  =  item.getItemId();                 if  (id  ==  R.id.action_settings)  {                         //Toast.makeText(this,  "change  skin",  1000).show();                         File  dir=new  File(Environment.getExternalStorageDirectory(),"plugins");                                                   File  skin=new  File(dir,"SkinPlugin.apk");                         if(skin.exists())                         {                                     SkinPackageManager.getInstance(MainActivity.this).loadSkinAsync(skin.getAbsolutePath(),  new  loadSkinCallBack()  {                                                                         @Override                                     public  void  startloadSkin()                                      {                                         Log.d("yzy",  "startloadSkin");                                     }                                                           @Override                                     public  void  loadSkinSuccess()  {                                         Log.d("yzy",  "loadSkinSuccess");                                         MainActivity.this.sendBroadcast(new  Intent(SkinBroadCastReceiver.SKIN_ACTION));                                     }                                                           @Override                                     public  void  loadSkinFail()  {                                         Log.d("yzy",  "loadSkinFail");                                     }                 });                         }                         return  true;                 }                 return  super.onOptionsItemSelected(item);         }           @Override         public  void  updateTheme()          {                 //  TODO  Auto-generated  method  stub                 if(btn_main!=null)                 {                         try  {                                 Resources  mResource=SkinPackageManager.getInstance(this).mResources;                                 Log.d("yzy",  "start  and  mResource  is  null--> "+(mResource==null));                                 int  id1=mResource.getIdentifier("main_btn_color",  "color",  "com.skin.plugin");                                 btn_main.setBackgroundColor(mResource.getColor(id1));                                 int  id2=mResource.getIdentifier("main_background",  "color","com.skin.plugin");                                 main_view.setBackgroundColor(mResource.getColor(id2));                                 //img_skin.setImageDrawable(mResource.getDrawable(mResource.getIdentifier("skin",  "drawable","com.skin.plugin")));                         }  catch  (Exception  e)  {                                 //  TODO  Auto-generated  catch  block                                 e.printStackTrace();                         }                 }         }                   @Override         protected  void  onDestroy()  {                 //  TODO  Auto-generated  method  stub                 SkinApplication.getInstance().mActivitys.remove(this);                 super.onDestroy();         }           @Override         public  void  onClick(View  v)  {                 //  TODO  Auto-generated  method  stub                 if(v.getId()==R.id.btn_main)                 {                         Intent  intent=new  Intent(this,SecondActivity.class);                         this.startActivity(intent);                 }         } }

这段代码里面主要看onOptionsItemSelected,这个方法里面,通过资源apk路径,拿到该资源apk对应Resources对象。我们直接看看SkinPacakgeManager里面做了什么吧
/**   *  解析皮肤资源包   *  com.skin.demo.SkinPackageManager   *  @author  yuanzeyao  < br>   *  create  at  2015年1月3日  下午3:24:16   */ public  class  SkinPackageManager  {     private  static  SkinPackageManager  mInstance;     private  Context  mContext;     /**       *  当前资源包名       */     public  String  mPackageName;           /**       *  皮肤资源       */     public  Resources  mResources;           private  SkinPackageManager(Context  mContext)     {         this.mContext=mContext;     }           public  static  SkinPackageManager  getInstance(Context  mContext)     {         if(mInstance==null)         {             mInstance=new  SkinPackageManager(mContext);         }                   return  mInstance;     }                 /**       *  异步加载皮肤资源       *  @param  dexPath       *                需要加载的皮肤资源       *  @param  callback       *                回调接口       */     public  void  loadSkinAsync(String  dexPath,final  loadSkinCallBack  callback)     {         new  AsyncTask< string,void,resources> ()         {               protected  void  onPreExecute()              {                 if(callback!=null)                 {                     callback.startloadSkin();                 }             };                     @Override             protected  Resources  doInBackground(String...  params)              {                 try  {                     if(params.length==1)                     {                         String  dexPath_tmp=params[0];                         PackageManager  mPm=mContext.getPackageManager();                         PackageInfo  mInfo=mPm.getPackageArchiveInfo(dexPath_tmp,PackageManager.GET_ACTIVITIES);                         mPackageName=mInfo.packageName;                                                                             AssetManager  assetManager  =  AssetManager.class.newInstance();                         Method  addAssetPath  =  assetManager.getClass().getMethod("addAssetPath",  String.class);                         addAssetPath.invoke(assetManager,  dexPath_tmp);                                                   Resources  superRes  =  mContext.getResources();                         Resources  skinResource=new  Resources(assetManager,  superRes.getDisplayMetrics(),  superRes.getConfiguration());                         SkinConfig.getInstance(mContext).setSkinResourcePath(dexPath_tmp);                         return  skinResource;                     }                     return  null;                 }  catch  (Exception  e)  {                     return  null;                 }                                };                           protected  void  onPostExecute(Resources  result)              {                 mResources=result;                                 if(callback!=null)                 {                     if(mResources!=null)                     {                         callback.loadSkinSuccess();                     }else                     {                         callback.loadSkinFail();                     }                 }             };                       }.execute(dexPath);     }           /**       *  加载资源的回调接口       *  com.skin.demo.loadSkinCallBack       *  @author  yuanzeyao  < br>       *  create  at  2015年1月4日  下午1:45:48       */     public  static  interface  loadSkinCallBack     {         public  void  startloadSkin();                   public  void  loadSkinSuccess();                   public  void  loadSkinFail();     }                 }

调用loadSkinAsync后,如果成功,就会发送一个换肤广播,并将当前皮肤apk的路径保存到sp中,便于下次启动app是直接加载该皮肤资源。接受换肤广播是在SkinApplication中注册的,当接收到此广播后,随即调用所有已经启动,并且需要换肤的Activity的updateTheme方法,从而实现换肤。
public  class  SkinApplication  extends  Application  {         private  static  SkinApplication  mInstance=null;                   public  ArrayList< iskinupdate>   mActivitys=new  ArrayList< iskinupdate> ();                   @Override         public  void  onCreate()  {                 //  TODO  Auto-generated  method  stub                 super.onCreate();                 mInstance=this;                 String  skinPath=SkinConfig.getInstance(this).getSkinResourcePath();                 if(!TextUtils.isEmpty(skinPath))                 {                     //如果已经换皮肤,那么第二次进来时,需要加载该皮肤                     SkinPackageManager.getInstance(this).loadSkinAsync(skinPath,  null);                 }                                   SkinBroadCastReceiver.registerBroadCastReceiver(this);         }                   public  static  SkinApplication  getInstance()         {                 return  mInstance;         }                   @Override         public  void  onTerminate()  {                 //  TODO  Auto-generated  method  stub                 SkinBroadCastReceiver.unregisterBroadCastReceiver(this);                 super.onTerminate();         }                   public  void  changeSkin()         {                 for(ISkinUpdate  skin:mActivitys)                 {                         skin.updateTheme();                 }         } }

 


    推荐阅读