zoukankan      html  css  js  c++  java
  • 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="0" />  
        <!-- Not displayed, but taken into account during layout (space is left for it). -->  
        <enum name="inshow" value="1" />  
        <!-- Completely hidden, as if the view had not been added. -->  
        <enum name="hide" value="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="?attr/imageValue" />  
      
      
        <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();
            }
        }
    }
  • 相关阅读:
    苹果的HomeKit协议
    广州出游计划
    Qt学习博客推荐
    Log4Qt使用(三)在DailyRollingFileAppender类中增加属性mMaxBackupIndex
    QT中关于窗口全屏显示与退出全屏的实现
    键盘事件-----按下回车键则触发事件
    窗体显示/编码设置/开机启动/文件选择与复制/对话框等
    设置系统日期时间
    输入内容, 列出可选的项: QComboBox
    如何根据安装时缺失的文件查找对应的包
  • 原文地址:https://www.cnblogs.com/dongweiq/p/6050605.html
Copyright © 2011-2022 走看看