Android 夜間模式主題切換方案

因爲Android的設置中並無夜間模式的選項,對於喜歡睡前玩手機的用戶,只能簡單的調節手機屏幕亮度來改善體驗。目前愈來愈多的應用開始把夜間模式加到自家應用中,沒準不久google也會把這項功能添加到Android系統中吧。java

業內關於夜間模式的實現,有兩種主流方案,各有其利弊,我較爲推崇第三種方案:android

一、經過切換theme來實現夜間模式。
二、經過修改uiMode來切換夜間模式。
git

三、經過插件方式切換夜間模式。github

值得一提的是,上面提到的幾種方案,都是資源內嵌在Apk中的方案,像新浪微博那種須要經過下載方式實現的夜間模式方案,網上有不少介紹,這裏不去討論。app

下面簡要描述下幾種方案的實現原理:框架

一、經過切換theme來實現夜間模式。ide

首先在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。ui

其次在不一樣的theme中,對屬性設置不一樣的值,在styles.xml中定義theme以下this

<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等資源也是這種方式,這裏就再也不論述

優勢:能夠匹配多套主題,並不侷限於黑白模式

缺點:須要大量定義主題

二、經過修改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種主題。


三、經過插件方式切換夜間模式。


插件換膚具體請參考以下博客:

Android更換皮膚解決方案

Android 插件化開發-主題皮膚更換


題外話:對於插件換膚,咱們還能夠思考定義一套屬於本身的Resource資源加載框架,而不是使用系統的Resource,這樣也是一種切換方案。

相關文章
相關標籤/搜索