Android夜间模式实现方案

对于一款阅读类的软件,夜间模式是不可缺少的。最初看到这个需求时候觉得无从下手,没有一点头绪。后来通过查阅资料发现Android官方在Support Library 23.2.0中已经加入了夜间主题。也就是只需要通过更换主题便可实现日间模式和夜间模式的切换。下面截取项目实现的夜间模式效果图:
这里写图片描述
效果看起来还比较nice,没有闪屏,过度也比较平滑。那么项目中的这个日间、夜间模式切换效果是如何实现的呢?下面将从以下几个方面来讲解:

  • 一 实现夜间模式需要的配置
  • 二 实现白天和夜间模式的切换
  • 三 实现夜间模式时遇到的问题及解决方案

一、实现夜间模式需要的配置
1.首先在gradel中引入以下依赖

1
compile 'com.android.support:appcompat-v7:25.3.1'

2.让我们项目的主题继承夜间模式主题,在style中设置如下主题:

1
2
3
4
5
6
<style name="AppTheme" parent="Theme.AppCompat.DayNight">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>

3.新建drawable-night和values-night的资源目录。如果要适配不同分辨率的屏幕则可新建drawable-night-hdip、drawable-night-xhdpi等目录来存放不同分辨率的图片资源。values-night目录下存放与夜间模式相关的value文件。本篇文章讲解仅以夜间模式和日间模式的颜色为例,在values-night目录下新建color.xml文件。

(1)新建values-night目录,如下:

新建values-night目录1

新建values-night目录2

(2)在values-night目录下新建colors文件,如下:
新建colors文件1

新建colors文件2
接下来只需要在对应的colors文件下写不同的颜色值(夜间颜色值和白天颜色值)即可。至此关于实现夜间模式的配置已经基本完成。

二、实现白天和夜间模式的切换
1.启动App时检测是否处于夜间模式,如果是则切换至夜间主题。这个需要在自己项目的Application中实现。可在自己项目的Application中添加以下代码:

1
2
3
4
5
6
7
8
/**
* 初始化夜间模式
*/
private void setNightMode() {
boolean nightMode = UserInfoTools.isNightMode(this);
AppCompatDelegate.setDefaultNightMode(nightMode ?
AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
}

这里需要介绍一下有关夜间模式的几个常量值。AppCompatDelegate.setDefaultNightMode(mode),其中mode有一下四个值:

  • MODE_NIGHT_NO: 亮色(light)主题,不使用夜间模式
  • MODE_NIGHT_YES:暗色(dark)主题,使用夜间模式
  • MODE_NIGHT_AUTO:根据当前时间自动切换 亮色(light)/暗色(dark)主题(22:00-07:00时间段内自动切换为夜间模式)
  • MODE_NIGHT_FOLLOW_SYSTEM(默认选项):设置为跟随系统,通常为MODE_NIGHT_NO
    2.接下来需要我们在设置页面点击ToggleButton时切换白天/夜间模式。
    具体实现如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    private void setNightMode() {
    // 获取当前模式
    int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
    // 将是否为夜间模式保存到SharedPreferences
    UserInfoTools.setNightMode(this, currentNightMode == Configuration.UI_MODE_NIGHT_NO);
    // 切换模式
    getDelegate().setDefaultNightMode(currentNightMode == Configuration.UI_MODE_NIGHT_NO ?
    AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
    UserInfoTools.setChangeNightMode(this,true);
    // 重启Activity
    recreate();
    }

    private void setListener() {
    mToggleButton.setOnClickListener((View v) -> {
    setNightMode();
    });
    }
    注意,上面代码中设置白天/夜间模式的代码的最后调用了recreate()方法重启了当前Activity。但这样写切换模式时会有闪屏问题,体验比较差。具体优化将在下一节中实现。

三 、实现夜间模式时遇到的问题及解决方案
利用谷歌官方提供的这个方案实现夜间模式的过程中遇到了不少的问题。且网上资料较少,大多文章讲解仅仅以一个简单的demo为例。但在用到实际项目中时会遇到很多的麻烦。这里主要总结了笔者曾经遇到过的难以解决的几个问题。
1.白天/夜间模式切换时闪屏问题
上一节中已经提到了在调用recreate()方法时会有闪屏问题。其实闪屏问题的解决比较简单。我们大可以不掉用recreate()方法,而是自己重启当前activity并为activity设置启动和退出动画即可!实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void setNightMode() {
// 获取当前模式
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
// 将是否为夜间模式保存到SharedPreferences
UserInfoTools.setNightMode(this, currentNightMode == Configuration.UI_MODE_NIGHT_NO);
// 切换模式
getDelegate().setDefaultNightMode(currentNightMode == Configuration.UI_MODE_NIGHT_NO ?
AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
UserInfoTools.setChangeNightMode(this,true);

//recreate();

startActivity(new Intent(this,SettingActivity.class));
overridePendingTransition(R.anim.animo_alph_close, R.anim.animo_alph_close);
finish();
}

如上代码,我们自行调用startActivity启动了设置页面并为其添加了一个透明渐变的启动动画。最后调用finish结束掉旧的设置页面。这样闪屏问题便迎刃而解。模式切换也变得流畅自然。
2.切换夜间模式后返回MainActivity,MainActivity页面没有更新。解决这个问题可以在切换模式后从设置页面发送一个广播,然后在MainActivity中接收到这个广播后重启MainActivity即可。根据官方的推荐更换夜间模式后需要调用recreate()方法刷新页面。但是recreate()方法巨坑无比,调用recreate()方法引起了诸多问题。详见问题3、4、5。因此解决这个问题笔者并没有在MainActivity调用中调用recreate()方法。而是在SettingActivity中定义了一个boolean值来标记是否切换了夜间模式。然后重写了onKeyDown()方法。如果切换了夜间模式则在返回时发出一个广播结束掉MainActivity,然后调用startActivity()重启了MainActivity并添加了启动动画,让用户感觉是只是返回了主页面。其实思想跟解决问题1有些类似。还是结合代码来看吧。

SettingActivity中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
goBack();
return true;
}
return super.onKeyDown(keyCode, event);
}

private void goBack() {
if (isChangeNightMode) { // 如果改变了夜间模式,则重启MainActivity
EventBus.getDefault().post(new NightModeEvent());
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra("nightMode", true);
startActivity(intent);
overridePendingTransition(R.anim.animo_alph_close, R.anim.activity_close);
}
finish();
}

MainActivity中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 @Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

reStartActivity()
}

// 改变夜间模式后返回时重启Activity
private void reStartActivity() {
Intent intent = getIntent();
boolean nightMode = intent.getBooleanExtra("nightMode", false);
if(nightMode&&UserInfoTools.getIsLogin(this)){
// 自动切换到“我的”页面
mRbMe.performClick();
}
}

/**
* 接收到夜间模式改变的事件后结束当前Activity
* @param event
*/
@Subscribe
public void setNightMode(NightModeEvent event) {
finish();
}

最后还有点问题需要说明,由问题1我们可以知道,改变模式后,我们重启了SettingActivity。因此在该类中定义的一个标记是否切换了夜间模式的boolean值并不能起到作用。解决办法是将这个值保存到SharedPreference中。然后重启SettingActivity后再取出该值。可以看代码,这点真心有点绕啊。。。
注意问题1中的setNightMode()方法中有一句代码 UserInfoTools.setChangeNightMode(this,true);将改变了夜间模式设置为了true并保存到了SharedPreferences中,然后在onCreate()中有以下代码来初始化isChangeNightMode的值。

1
2
3
4
5
6
7
8
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
isChangeNightMode=UserInfoTools.isChangeNightMode(this);
UserInfoTools.setChangeNightMode(this,false);
}

3.设置白天/夜间模式后出现无故闪退问题
这个问题说来比较奇怪,原因是切换了夜间模式后在MainActivity中调用了recreate()方法。具体原因笔者也没有弄清楚,调试了好一阵子也没有找出个所以然来。后来果断放弃了在MainActivity中调用recreate()方法,而是换成了startActivity()重新启动了MainActivity。之后这个问题便不复存在了。

4.点击ToggleButton切换模式后应用黑屏,随后挂掉。这个问题的最终原因还是因为recreate()方法引起的。如果你用了ToggleButton切换白天/夜间模式,并且为ToggleButton设置了setOnCheckChangedListener()方法,那么你将有很大概率碰到这个问题。引起这个问题的原因是因为调用了recreate()方法后Activity重新启动,但是新启动的Activity保存了之前Activity的状态。因此在重启时候重新设置了TouggleButton,继而调用了又setOnCheckChangedListener()方法,结果悲剧了。。。一个死循环产生了,程序不黑屏才怪。因此最简单的办法是放弃recreate()方法,改用问题1中的方法!(其实细心的小伙伴应该已经发现了,我的代码中仅仅是为ToggleButton设置了setOnClickListener()….机智如我啊)如果你有强迫症必须要使用setOnCheckChangedListener和recreate()方法那么也不是没有解决方案。可以定义一个boolean成员变量,然后在onCreate()方法中判断savedInstanceState是否为null,然后给这个boolean成员变量赋值,并在setOnCheckChangedListener()方法中根据这个boolean成员变量的值去调用设置夜间模式的方法即可。

5.设置夜间模式后MainActivity调用recreate()方法,MainActivity中的”发现“页面没有加载出来。发现页面如下面图片所示,也就是一个Fragment中嵌套了一个ViewPager。调用recreate()后整个ViewPager消失了。。。没有加载出来!!!
这里写图片描述
解决方案,放弃使用recreate(),改用问题1中的方法!

万恶的recreate()方法!难道是我使用的姿势不对?


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!