高质量Android开发系列之(一)-Android夜间模式最佳实现

由于Android的设置中并没有夜间模式的开关,对于喜欢睡前玩手机的用户,只能简单的调节手机屏幕亮度来改善体检。当前越来越多的应用开始把夜间模式加到自家应用中,相信不久google也会把这项功能用到Android系统中吧。

对当前夜间模式的实现,大概有三种方法如下:
1、通过设置不同的theme来实现夜间模式的UI变化
2、通过id获取资源时,将该id转换为夜间模式对应资源id然后再去获取资源
3、更新Resources的Configuration中的uiMode来更新Resources的夜间模式开关
下面我来简短的描述下前两种方案的实现,及第三种方案的实现原理

一、通过设置不同的theme来实现夜间模式的UI变化

首先对不同的主题需要设置不同的属性,先在attrs.xml中定义属性

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="textColor" format="color|reference" />
<attr name="mainBackground" format="color|reference" />
</resources>

其次定义不同的theme,在theme中对不的同属性设置不同的值,在styles.xml中定义theme

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<resources>

<!-- 默认 -->
<style name="ThemeDefault" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="mainBackground">#ffffff</item>
<item name="textColor">#000000</item>
</style>


<!-- 夜间 -->
<style name="ThemeNight" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="mainBackground">#000000</item>
<item name="textColor">#ffffff</item>
</style>

</resources>

在布局文件中使用对应的值,通过?attr/属性名,来获取不同theme对应的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android="@+id/main_screen"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="?attr/mainBackground">


<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="改变Theme"
android:onClick="changeTheme"
android:textColor="?attr/textColor"/>

</LinearLayout>

在Activity中的changeTheme方法中,其中isNightMode为一个全局变量用来标记当前是否为夜间模式,在设置完theme后还需要调用setContentView重新刷新UI

1
2
3
4
5
6
7
8
9
10
11
public void changeTheme(View view) {

if (isNightMode) {
setTheme(R.style.ThemeDefault);
isNightMode = false;
} else {
setTheme(R.style.ThemeNight);
isNightMode = true;
}
setContentView(R.layout.activity_main);
}

到此即完成了一个简单的夜间模式实现,这种方法比较简单,大多数的应用都是通过setTheme来实现的,但是在比较大的应用,需要随theme变化的属性都需要定义,缺点是设置theme后需要通过setContentView或restartActivity来切换UI,也可以调用如下updateTheme方法,将需要改变颜色的view重新设置背影或改变颜色

1
2
3
4
5
6
7
8
9
private void updateTheme() {

TypedValue typedValue = new TypedValue();
Resources.Theme theme = getTheme();
theme.resolveAttribute(R.attr.textColor, typedValue, true);
findViewById(R.id.button).setBackgroundColor(typedValue.data);
theme.resolveAttribute(R.attr.mainBackground, typedValue, true);
findViewById(R.id.main_screen).setBackgroundColor(typedValue.data);
}

二、通过id获取资源时,将该id转换为夜间模式对应资源id然后再去获取资源

通过id获取资源时,先将其转为夜间模式对应id,再通过Resources来获取对应的资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static Drawable getDrawable(Context context, int id) {

return context.getResources().getDrawable(getResId(id));
}

public static int getResId(int defaultResId) {

if (!isNightMode()) {
return defaultResId;
}

if (sResourceMap == null) {
buildResourceMap();
}

int themedResId = sResourceMap.get(defaultResId);
return themedResId == 0 ? defaultResId : themedResId;
}

这里是通过HashMap的方法来将非夜间模式的resId和夜间模式的resId来一一对应起来的

1
2
3
4
5
6
7
private static void buildResourceMap() {

sResourceMap = new SparseIntArray();

//add drawable
sResourceMap.put(R.drawable.common_background, R.drawable.common_background_night);
}

这个方法非常简单粗暴,麻烦的地方和第一次方法一样,就是每次添加资源需要将其对应起来,刷新UI的方法同第一种方法一样

三、通过修改uiMode来切换夜间模式

首先统一将获取资源的地方统计起来,使用Application对应的Resources在Application的onCreate中调用ResourcesManager的init方法将其初始化

1
2
3
public static void init(Context context) {
sRes = context.getResources();
}

切换夜间模式时,通过更新uiMode来更新Resources的配置,系统会根据其uiMode读取对应night下的资源,同时在res中给夜间模式的资源后缀添加-night,比如values-night,drawable-night

1
2
3
4
5
6
7
8
public static void updateNightMode(boolean on) {

DisplayMetrics dm = sRes.getDisplayMetrics();
Configuration config = sRes.getConfiguration();
config.uiMode &= ~Configuration.UI_MODE_NIGHT_MASK;
config.uiMode |= on ? Configuration.UI_MODE_NIGHT_YES : Configuration.UI_MODE_NIGHT_NO;
sRes.updateConfiguration(config, dm);
}

至于Android的资源读取我们可以参考老罗的博客Android应用程序资源的查找过程分析来看下资源是怎么被精准找到的

这种方法相对前两种的好处就是资源添加非常简单清晰,但是UI上的更新还是无法做到非常顺滑的切换

下面我描述下,怎么找到上述这种方法的

在Android开发文档中搜索night发现如下,可以通过UiModeManager来实现

Android Document
night: Night time
notnight: Day time
Added in API level 8.
This can change during the life of your application if night mode is left in auto mode (default), in which case the mode changes based on the time of day. You can enable or disable this mode using UiModeManager. See Handling Runtime Changes for information about how this affects your application during runtime.
不幸的是必须在驾驶模式下才有效,那是不是打开驾驶模式再设置呢,实际上是不可行的,驾驶模式下UI有变动,这种情况是不可取的

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Sets the night mode. Changes to the night mode are only effective when
* the car or desk mode is enabled on a device.
*
* The mode can be one of:
* {@link #MODE_NIGHT_NO}- sets the device into notnight
* mode.
* {@link #MODE_NIGHT_YES} - sets the device into night mode.
* {@link #MODE_NIGHT_AUTO} - automatic night/notnight switching
* depending on the location and certain other sensors.
*/

public void setNightMode(int mode)

从源码开始看起UiModeManagerService.java的setNightMode方法中

1
2
3
4
5
6
7
8
9
if (isDoingNightModeLocked() &amp;amp;&amp;amp; mNightMode != mode) {
Settings.Secure.putInt(getContext().getContentResolver(),s
Settings.Secure.UI_NIGHT_MODE, mode);
mNightMode = mode;
updateLocked(0, 0);
}
boolean isDoingNightModeLocked() {
return mCarModeEnabled || mDockState != Intent.EXTRA_DOCK_STATE_UNDOCKED;
}

在 isDoingNightModeLocked中判断了DockState和mCardMode的状态,如果满足条件实际上只修改了mNightMode的值,继续跟踪updateLocked方法,可以看到在updateConfigurationLocked中更新了Configuration的uiMode

让我们转向Configuration的uiMode的描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Bit mask of the ui mode. Currently there are two fields:
*

The {@link #UI_MODE_TYPE_MASK} bits define the overall ui mode of the
* device. They may be one of {@link #UI_MODE_TYPE_UNDEFINED},
* {@link #UI_MODE_TYPE_NORMAL}, {@link #UI_MODE_TYPE_DESK},
* {@link #UI_MODE_TYPE_CAR}, {@link #UI_MODE_TYPE_TELEVISION},
* {@link #UI_MODE_TYPE_APPLIANCE}, or {@link #UI_MODE_TYPE_WATCH}.
*
*

The {@link #UI_MODE_NIGHT_MASK} defines whether the screen
* is in a special mode. They may be one of {@link #UI_MODE_NIGHT_UNDEFINED},
* {@link #UI_MODE_NIGHT_NO} or {@link #UI_MODE_NIGHT_YES}.
*/

public int uiMode;

uiMode为public可以直接设置,既然UiModeManager设置nightMode只改了Configuration的uiMode,那我们是不是可以直接改其uiMode呢
实际上只需要上面一小段代码就可以实现了,但是如果不去查看UiModeManager的夜间模式的实现不会想到只需要更新Configuration的uiMode就可以了

扫描或长按关注我们的微信技术公众号(或搜索 mtydev)

img

留言

本站总访问量

留言