由于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更换皮肤解决方案
题外话:对于插件换肤,我们还可以思考定义一套属于自己的Resource资源加载框架,而不是使用系统的Resource,这样也是一种切换方案。